Files
Jonathan KirkwoodandClaude Fable 5 506e6c79bd v0.1.8: two-Spark pipeline + kept-warm serving
- Secondary-Spark models now work in air-gapped mode: the LiteLLM proxy is
  dual-homed onto the default bridge (docker network connect) to reach the
  secondary's published vLLM port; grader containers stay on the --internal
  network with zero egress. The head-only enforcement is replaced by a
  secondary-configured check.
- Extraction runs in parallel with grading when the extractor's model and
  every grader model in the wave sit on different Sparks (separate GPUs).
- Keep-warm: single-wave jobs no longer tear the wave down between decks
  (was a ~6-min 31B reload per deck); a kept-warm wave that fails preflight
  is restarted once. Adjudicator reuses the live wave when its model is
  already serving instead of cycling the shared proxy.
- clear_resident_containers (preJobStopContainers) now stops names on every
  configured Spark; health() reports containers on both Sparks.
- Verified with a mocked dry-run of the full job loop (3 decks: one
  bring-up, zero mid-job teardowns, parallel overlap, stale-wave restart).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 15:01:10 -05:00

271 lines
9.6 KiB
TypeScript

import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
bdefOverride: Value.textarea({
name: 'BDEF Rubric Override (optional)',
description:
'Leave EMPTY to grade against the baked-in BDEF v1.1 framework ' +
'(Girdley + Munger/Buffett). Non-empty text replaces the rubric wholesale, ' +
'so include the qualitative categories A-H if you customize it.',
required: false,
default: null,
minRows: 5,
maxRows: 24,
placeholder: '(empty = the built-in BDEF v1.1 rubric)',
}),
extractorModel: Value.text({
name: 'Extractor Model (optional)',
description:
'Catalog alias of the model that runs the stage-1 structured KPI ' +
'extraction over each deck. Empty = use the first model.',
required: false,
default: null,
placeholder: 'grader-a',
}),
networkMode: Value.select({
name: 'Network Mode',
description:
'Air-gapped: graders reach ONLY the on-Spark model proxy — zero internet, ' +
'board decks never leave your hardware (models must be pre-pulled into ' +
'each Spark HF cache; models on either Spark are fine — the proxy routes ' +
'to the second Spark over your LAN). Local services: graders may also ' +
'reach LAN services like SearXNG (this network has egress unless you ' +
'firewall it).',
default: 'airgapped',
values: {
airgapped: 'Air-gapped (no network, recommended)',
local_services: 'Local services (SearXNG / 2nd Spark)',
},
}),
searxngUrl: Value.text({
name: 'SearXNG URL (local-services only)',
description: 'JSON-search endpoint to give graders a web_search tool. Ignored in air-gapped mode. Empty = no web search.',
required: false,
default: null,
placeholder: 'https://searxng.local',
}),
adjudicatorEnabled: Value.toggle({
name: 'Run an Adjudicator',
description:
'After the panel finishes, run a local "lead grader" that reads every ' +
'grade sheet, reconciles disagreements, and settles the qualitative scores ' +
'the deterministic composite uses. No frontier model — stays on the Sparks.',
default: true,
}),
adjudicatorModel: Value.text({
name: 'Adjudicator Model (optional)',
description: 'Catalog alias of the model that adjudicates the panel. Empty = use the first model.',
required: false,
default: null,
placeholder: 'grader-a',
}),
adjudicatorPersona: Value.textarea({
name: 'Adjudicator Instructions (optional)',
description: 'Override how the adjudicator reconciles the panel. Empty = a sensible built-in default.',
required: false,
default: null,
minRows: 3,
maxRows: 14,
}),
wipeRemoteDocs: Value.toggle({
name: 'Wipe Decks From Sparks After Grading',
description:
'Delete the extracted deck text from the Sparks when a job finishes. ' +
'Scorecards and ledgers are always kept on this StartOS box. Recommended ' +
'for confidential board material.',
default: true,
}),
autoRunOnDrop: Value.toggle({
name: 'Auto-grade When Decks Are Dropped',
description:
'Start grading automatically (after a short debounce) whenever new decks ' +
'land in the inbox. Off by default so you trigger grading explicitly.',
default: false,
}),
preJobStopContainers: Value.text({
name: 'Stop These Containers Before Grading (optional)',
description:
'Space- or comma-separated docker container names stopped on the HEAD ' +
'Spark at the start of every grading job, so the job gets the GPU to ' +
'itself (e.g. an always-on vLLM another service runs). They are NOT ' +
'restarted afterwards — the service that owns them reloads its own ' +
'models (e.g. the Gazette\'s Fleet job). Empty = stop nothing.',
required: false,
default: null,
placeholder: 'vllm-gemma4-clerk',
}),
// --- Deterministic-scorer weights (composite = quant 60 + qual 40 - flags) ---
profitabilityKpi: Value.number({
name: 'Weight: Profitability KPIs',
description: 'Points for the profitability KPI attainment bucket — the heaviest slice of the quant 60. Default 30.',
required: true,
default: 30,
integer: false,
min: 0,
max: 100,
}),
otherKpi: Value.number({
name: 'Weight: Other KPIs',
description: 'Points for the non-profitability measurable-KPI bucket. Default 20.',
required: true,
default: 20,
integer: false,
min: 0,
max: 100,
}),
forecastIntegrity: Value.number({
name: 'Weight: Forecast Integrity',
description: 'Points for deck N actuals hitting what deck N-1 promised. Default 10.',
required: true,
default: 10,
integer: false,
min: 0,
max: 100,
}),
qualCategoryMax: Value.number({
name: 'Weight: Max Per Qualitative Category',
description: 'Max points per BDEF category A-H (8 categories x 5 = the qualitative 40). Default 5.',
required: true,
default: 5,
integer: false,
min: 0,
max: 100,
}),
redFlagCap: Value.number({
name: 'Red-flag Penalty Cap',
description: 'Maximum total points red flags can subtract from the composite. Default 15.',
required: true,
default: 15,
integer: false,
min: 0,
max: 100,
}),
kpiCreditFloor: Value.number({
name: 'KPI Credit Floor',
description: 'actual/target ratio below which a KPI earns zero credit (linear credit above it). Default 0.5.',
required: true,
default: 0.5,
integer: false,
min: 0,
max: 1,
}),
droppedKpiPenalty: Value.number({
name: 'Dropped-KPI Penalty',
description: 'Penalty per KPI that silently disappeared from the deck. Default 2.',
required: true,
default: 2,
integer: false,
min: 0,
max: 100,
}),
droppedKpiMax: Value.number({
name: 'Dropped-KPI Flag Limit',
description: 'Count at most this many dropped-KPI flags per deck. Default 3.',
required: true,
default: 3,
integer: true,
min: 0,
max: 50,
}),
evidenceFullCredit: Value.number({
name: 'Evidence Full-credit Threshold',
description: 'Quote characters a qualitative finding needs for full weight (thinner evidence scales down). Default 400.',
required: true,
default: 400,
integer: true,
min: 0,
max: 100000,
}),
singleSourceFlagFactor: Value.number({
name: 'Single-source Flag Damping',
description: 'Multiplier applied to red flags raised by only one grader. Default 0.5.',
required: true,
default: 0.5,
integer: false,
min: 0,
max: 1,
}),
})
export const configureGrading = sdk.Action.withInput(
'configure-grading',
async ({ effects }) => ({
name: 'Configure Grading',
description: 'Set the BDEF rubric, air-gap mode, adjudication, retention, and the scoring weights.',
warning: null,
allowedStatuses: 'any',
group: 'Grading',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return {
bdefOverride: cfg.bdefOverride || undefined,
extractorModel: cfg.extractorModel || undefined,
networkMode: cfg.networkMode,
searxngUrl: cfg.searxngUrl || undefined,
adjudicatorEnabled: cfg.adjudicatorEnabled,
adjudicatorModel: cfg.adjudicatorModel || undefined,
adjudicatorPersona: cfg.adjudicatorPersona || undefined,
wipeRemoteDocs: cfg.wipeRemoteDocs,
autoRunOnDrop: cfg.autoRunOnDrop,
preJobStopContainers: cfg.preJobStopContainers || undefined,
profitabilityKpi: cfg.weights.profitabilityKpi,
otherKpi: cfg.weights.otherKpi,
forecastIntegrity: cfg.weights.forecastIntegrity,
qualCategoryMax: cfg.weights.qualCategoryMax,
redFlagCap: cfg.weights.redFlagCap,
kpiCreditFloor: cfg.weights.kpiCreditFloor,
droppedKpiPenalty: cfg.weights.droppedKpiPenalty,
droppedKpiMax: cfg.weights.droppedKpiMax,
evidenceFullCredit: cfg.weights.evidenceFullCredit,
singleSourceFlagFactor: cfg.weights.singleSourceFlagFactor,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
bdefOverride: input.bdefOverride ?? '',
extractorModel: input.extractorModel ?? '',
networkMode: input.networkMode,
searxngUrl: input.searxngUrl ?? '',
adjudicatorEnabled: input.adjudicatorEnabled,
adjudicatorModel: input.adjudicatorModel ?? '',
adjudicatorPersona: input.adjudicatorPersona ?? '',
wipeRemoteDocs: input.wipeRemoteDocs,
autoRunOnDrop: input.autoRunOnDrop,
preJobStopContainers: input.preJobStopContainers ?? '',
weights: {
profitabilityKpi: input.profitabilityKpi,
otherKpi: input.otherKpi,
forecastIntegrity: input.forecastIntegrity,
qualCategoryMax: input.qualCategoryMax,
redFlagCap: input.redFlagCap,
kpiCreditFloor: input.kpiCreditFloor,
droppedKpiPenalty: input.droppedKpiPenalty,
droppedKpiMax: input.droppedKpiMax,
evidenceFullCredit: input.evidenceFullCredit,
singleSourceFlagFactor: input.singleSourceFlagFactor,
},
})
return {
version: '1',
title: 'Grading Settings Saved',
message:
input.networkMode === 'airgapped'
? 'Saved. Graders will run air-gapped (no internet). Ensure all models are on the head Spark and pre-pulled into its HF cache.'
: 'Saved. Graders run in local-services mode and may reach the network — make sure that is acceptable for these board decks.',
result: { type: 'single', value: input.networkMode, copyable: false, qr: false, masked: false },
}
},
)