- 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>
271 lines
9.6 KiB
TypeScript
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 },
|
|
}
|
|
},
|
|
)
|