Contract version: 0.1.0-experimental
Stability: provisional
This contract set is intentionally minimal and revision-friendly.
It is normative for current implementation work in this repository, but it is not yet declared stable for external package consumers. Fields may change as additional integration surfaces validate or falsify assumptions.
These contracts define the control-plane boundary for router decisions.
They are designed to keep core routing semantics vendor-neutral while allowing vendor-specific execution behavior to be mapped in appendices.
- Route execution targets, not abstract models.
- Keep hard constraints separate from soft preferences.
- Keep durable session mode separate from latest-turn task type.
- Do not allow manual overrides to bypass hard constraints.
- Capture enough evidence for explainability and replay.
This document is aligned to the current phase plan with these constraints:
- Contract v0.1 stays minimal, provisional, and revision-friendly.
- Handoff/context-transfer is represented in core contracts, not only vendor appendices.
- Policy inputs include named hard and soft constraints, even where implementation depth is staged.
- Explainability and outcome attribution are first-class before broader policy tuning work.
- Multi-surface validation is conditional and should not force premature schema expansion.
The following choices are intentionally not frozen in 0.1.0-experimental.
-
Final task taxonomy and mode taxonomy. Interim stance: keep enums practical for current scope and evolve via schema versioning.
-
Continuity-cost scoring precision. Interim stance: categorical continuity cost is the default; numeric scoring is deferred.
-
Risk signal ownership. Interim stance: allow optional risk fields and record provenance in logs when available.
-
Strict hard-constraint enforcement policy. Interim stance: represent privacy, availability, and client compatibility in contracts now; stage enforcement depth.
-
Stable outcome attribution ontology. Interim stance: minimal attribution labels first, expansion only after replay evidence validates usefulness.
-
Cross-surface contract freeze. Interim stance: Claude mapping is evidence-backed but non-universal; core schemas remain revision-friendly.
{
"schemaVersion": "0.1.0-experimental",
"sessionId": "string",
"threadId": "string",
"mode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"currentTargetId": "string|null",
"turnCount": "number",
"routingOverride": "auto|stronger|cheaper|stay",
"riskLevel": "low|medium|high|null",
"failureSignals": {
"recentToolFailures": "number",
"recentTestFailures": "number"
},
"updatedAt": "ISO-8601 timestamp"
}Notes:
modeis durable session phase.routingOverrideis user preference input, not authority to bypass constraints.riskLevelandfailureSignalsare optional in early implementations.
{
"schemaVersion": "0.1.0-experimental",
"taskType": "string",
"proposedMode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"confidence": "number",
"reason": "string",
"signals": ["string"]
}Notes:
taskTypedescribes latest-turn work intent.proposedModemay differ from current session mode.
{
"schemaVersion": "0.1.0-experimental",
"id": "string",
"label": "string",
"targetClass": "cheap_fast|medium_reasoning|strong_reasoning|strong_coding",
"provider": "string",
"clientSurface": "string",
"modelProfile": "string",
"effortProfile": "string|null",
"capabilities": ["string"],
"constraints": {
"availability": "available|degraded|unavailable",
"privacyTier": "local|standard|restricted|unknown"
}
}Notes:
- Execution target is
model + provider + client surface + capability/constraint behavior. availabilityandprivacyTiercan be coarse initially.
{
"schemaVersion": "0.1.0-experimental",
"status": "ok|refused",
"mode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"taskType": "string",
"requiredCapabilities": ["string"],
"hardConstraintResults": {
"eligibleTargetIds": ["string"],
"blocked": [
{
"targetId": "string",
"missingCapabilities": ["string"],
"constraintReasons": ["string"]
}
]
},
"softConstraintInputs": {
"userPreference": "auto|stronger|cheaper|stay|null",
"projectOverride": "string|null"
},
"targetClass": "cheap_fast|medium_reasoning|strong_reasoning|strong_coding|null",
"selectedTargetId": "string|null",
"shouldSwitch": "boolean|null",
"continuityCost": "low|medium|high|null",
"continuityDecision": "stay_on_current_target|select_target_without_current_context|avoid_switch_due_to_continuity_cost|switch_target|no_selected_target|null",
"continuityReason": "string|null",
"routingOverride": {
"requested": "auto|stronger|cheaper|stay",
"applied": "boolean",
"reason": "string|null"
},
"modeResolution": {
"previousMode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain|null",
"proposedMode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"resolvedMode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"transitionReason": "string"
}|null,
"policyInputs": {
"hardConstraints": {
"privacy": "enforced|advisory|off",
"availability": "enforced|advisory|off",
"clientCompatibility": "enforced|advisory|off",
"requiredPrivacyTier": "external|local|standard|restricted|unknown|null",
"clientSurface": "string|null"
},
"softConstraints": {
"userPreference": "auto|stronger|cheaper|stay",
"projectOverride": "string|object|null"
}
}|null,
"explanation": "string",
"refusalReason": "string|null"
}Notes:
status=refusedrequiresrefusalReasonand no selected target.shouldSwitchandcontinuityCostmay benullwhen status is refused.softConstraintInputsrecords preference inputs for explainability even when policy depth is staged.routingOverridematches current implementation output naming.continuityDecision,continuityReason,modeResolution, andpolicyInputsare currently emitted by the implementation and treated as additive experimental fields in0.1.0-experimental.
{
"schemaVersion": "0.1.0-experimental",
"sessionId": "string",
"threadId": "string",
"turnIndex": "number",
"routeLabel": "string",
"targetId": "string",
"mode": "plan|implement|debug|review|summarize|agent_workflow|out_of_domain",
"wrapperContext": "{ kind: string, text: string }|null",
"handoffSummary": "string|null",
"createdAt": "ISO-8601 timestamp"
}Notes:
- This is the minimal handoff/context contract for current scope.
wrapperContextmaps to current structured wrapper context evidence (for examplekindandtext).handoffSummarymay remain null until explicit handoff flows are implemented.
{
"schemaVersion": "0.1.0-experimental",
"routesByTaskType": {
"<taskType>": "cheap_fast|medium_reasoning|strong_reasoning|strong_coding"
},
"routesByMode": {
"plan": "medium_reasoning",
"implement": "strong_coding",
"debug": "strong_coding",
"review": "medium_reasoning",
"summarize": "cheap_fast",
"agent_workflow": "medium_reasoning",
"out_of_domain": "medium_reasoning"
},
"capabilityRequirements": {
"<mode>": ["string"]
},
"hardConstraintInputs": {
"privacy": "enforced|advisory|off",
"availability": "enforced|advisory|off",
"clientCompatibility": "enforced|advisory|off"
},
"softConstraintInputs": {
"userPreferences": "enabled|disabled",
"projectOverrides": "enabled|disabled"
},
"escalationRules": {
"<signal>": "cheap_fast|medium_reasoning|strong_reasoning|strong_coding"
},
"continuityPolicy": {
"switchThreshold": "low|medium|high"
}
}Notes:
- Keep this config deterministic and fixture-testable.
- Hard and soft constraint inputs should be named even when implementations are staged.
- Unknown fields should be ignored by default in early versions.
Normalized event emitted for every routing decision. This is the primary log format for explainability and replay.
{
"schemaVersion": "0.1.0-experimental",
"ts": "ISO-8601 timestamp",
"source": "switchboard_wrapper|adapter_anthropic|adapter_gemini|adapter_openai|gateway_X",
"sessionId": "string",
"threadId": "string",
"turnIndex": "number",
"userPrompt": "string|null",
"sessionState": { ... },
"routingDecision": { ... },
"contextPackage": { ... },
"outcome": {
"executionStatus": "executed|failed|planned",
"exitCode": "number|null",
"errorSignal": "null|tool_failure|test_failure|execution_timeout|auth_failure|hook_correlation_miss|target_not_available|low_confidence_escalation|user_correction_needed",
"durationMs": "number|null"
},
"attribution": {
"decisionId": "string",
"decisionConfidence": "number",
"switchingReason": "null|continuity_cost|capability_gap|user_override|escalation|availability",
"escalationApplied": "boolean",
"policyVersion": "string"
},
"hookCorrelation": { "status": "matched|unmatched|unavailable", ... }|null,
"wrapperContext": { "kind": "string", "text": "string" }|null,
"legacy": { ... }|null
}Notes:
schemaVersionmatches core contracts for consistency.sessionState,routingDecision,contextPackageare the canonical contract shapes (see sections 1, 4, 5).outcomecaptures execution results for replay and evaluation.attributionstores decision metadata for queryability and outcome attribution.decisionIdis unique per decision (UUID or deterministic hash) for correlating with hooks and feedback.policyVersiontracks which policy made this decision (for policy comparison replay).hookCorrelationrecords whether this decision correlated with hook events (optional).wrapperContextis the user-facing context message (if available).legacyfield is present for backward compatibility; new readers should prefer canonical shapes.
- Changes that alter meanings of existing fields require a schema version bump.
- New optional fields can be added in the same experimental minor version.
- Existing readers should ignore unknown fields.
- Existing writers should not delete known fields without migration notes.
This appendix maps the current Claude workflow to the core contracts without claiming that all clients behave this way.
Current workflow signals in the switchboard implementation map as follows:
threadId->SessionState.threadId- persisted
claudeSessionId->SessionState.sessionId - stored
turnCount->SessionState.turnCount - current route mode ->
SessionState.mode routingOverride->SessionState.routingOverride
Current workflow persistence now records a contract-backed sessionState object alongside compatibility fields in route-context storage.
Current route labels map to target classes in the Claude integration:
quick->cheap_fastbalanced->medium_reasoningbest coder->strong_coding
Claude launch flags are an execution detail in the workflow layer and should not alter core contract semantics.
Current continuity semantics are:
- first routed turn uses
--session-id - subsequent routed turns use
--resume <session-id> - stale resume recovery may retry with a fresh
--session-id
These semantics belong to workflow execution behavior, not router decision semantics.
Current workflow evidence now separates router-decision data from Claude execution data in wrapper logs while preserving continuity semantics and stale-resume recovery behavior.
Current hook evidence maps to RoutingLogEvent.hookCorrelation:
- matched route context ->
routeContextMatched=true - unmatched route context ->
routeContextMatched=false - hook event identity ->
hookEventId
Hook timing and correlation quality remain surface-specific and should not be treated as universal across clients.
Current route-context persistence also records a contract-backed ContextPackage and claudeExecution block so hook correlation can consume stable handoff fields without depending only on legacy flat fields.
Routed decisions are tracked with minimal outcome signals to support policy evaluation without a full telemetry infrastructure.
Error Signals (populated in RoutingLogEvent.outcome.errorSignal):
null— no error signal (useRoutingLogEvent.outcome.executionStatusto distinguishplannedvs successfulexecuted)tool_failure— tool call failed during executiontest_failure— test run failedexecution_timeout— execution did not complete in allowed timeauth_failure— authentication/authorization failed (e.g., Claude auth)hook_correlation_miss— decision did not correlate with expected hook eventstarget_not_available— selected target became unavailable after decisionlow_confidence_escalation— router escalated due to confidence < 0.7user_correction_needed— user had to correct or re-issue after decision
Switching Reasons (populated in RoutingLogEvent.attribution.switchingReason):
null— no switch occurred (stayed on current target)continuity_cost— switched due to continuity cost evaluationcapability_gap— switched because current target lacks required capabilityuser_override— switched due to user preference overrideescalation— switched due to escalation policy (e.g., low confidence, repeated failure)availability— switched because current target became unavailable
The normalized event structure enables offline policy evaluation against stored evidence.
Storage:
~/.model-switchboard/switchboard-turns.ndjson— normalized RoutingLogEvent records~/.model-switchboard/attributions/{sessionId}.ndjson— outcome attribution records (keyed bydecisionId)
Evaluation Functions:
loadSessionEvidence(sessionId)— load all events for a sessionreplayRoutingDecision(evidence, policyVersion)— re-evaluate one decision under alternate policyevaluatePolicyOnEvidence(evidenceSet, policyVersion)— batch evaluation across all turns
- Should
riskLevelbe router-owned or adapter-provided in v0.2? - Should continuity cost remain categorical or evolve to a weighted score?
- Which minimal handoff payload fields are required before adding multi-surface handoff support?
- What is the smallest stable taxonomy for outcome attribution that is still useful for policy tuning?