Skip to content

Add headless dispatch arbiter - #409

Draft
m-aebrer wants to merge 1 commit into
feature/issue-394-routing-guide-thinkingfrom
feature/issue-394-dispatch-arbiter
Draft

Add headless dispatch arbiter#409
m-aebrer wants to merge 1 commit into
feature/issue-394-routing-guide-thinkingfrom
feature/issue-394-dispatch-arbiter

Conversation

@m-aebrer

Copy link
Copy Markdown
Collaborator

Closes #394

Stage 2 builds on the routing guide and thinking metadata from stage 1 to add the fully headless, fail-closed Dispatch Arbiter before every subagent spawn, with typed observability across dreb's interfaces.

This is a stacked PR based on stage 1 (PR 405). Implementation plan posted as a comment below.

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Implementation Plan — Stage 2

Problem and approach

Stage 2 adds the actual Dispatch Arbiter: a fully headless background decision-maker that runs in the existing subagent background lifecycle after the parent requests a child but before any child process is spawned. It consumes the routing guide and thinking metadata delivered by stage 1, then may change only the selected agent type, canonical provider/model, and thinking level.

The implementation will stay close to the existing tab/session-title setter rather than introducing a general routing framework. A small DispatchArbiter helper will reuse the same proven shape: a bounded RollingContextBuffer, labeled message/tool activity, a direct completeSimple() call with an injected model registry and API key, an abort/timeout signal, and no tools. Unlike title generation, arbitration is control-path work: it has no parent-model fallback, is never fire-and-forget, and fails closed.

The launch path will split today's routing into three clear phases inside the shared executeSingle() path:

  1. Normalize the immutable task/cwd and resolve the parent's current proposal to a concrete agent/model/thinking tuple using existing precedence and fallback behavior.
  2. When enabled, invoke the headless arbiter once with bounded, scrubbed context and strictly validate its structured decision.
  3. Apply the selected existing agent definition verbatim, exact-validate the final scoped model and thinking capability, then spawn the child.

Single and parallel tasks already converge on executeSingle(). Chain steps reach it only after {previous} substitution, so this placement gives one arbitration per actual spawn and lets each chain decision see the real substituted task.

External prior art reinforces the narrow design: RouteLLM treats routing as a quality/cost choice between adequate and stronger models, while official OpenAI and Anthropic guidance emphasizes schema-constrained outputs. Because dreb's generic provider layer does not expose one uniform native structured-output API, the host will accept only a strict JSON object, validate every field itself, optionally retry malformed output once, and never treat model text as executable policy.

Deliverables

1. Global-only arbiter configuration

Add subagentArbiter settings with enabled, model, thinking, and guidePath fields.

  • Arbitration remains disabled by default.
  • Runtime reads come only from the global settings object. Project settings cannot enable, disable, or reconfigure it.
  • Enabling requires an exact resolvable arbiter model and a non-empty current session model scope. Missing, malformed, or unsupported configuration fails the affected launch before inference or spawn.
  • The configured thinking level is capability-validated against the arbiter model.
  • The standard guide path expands from ~/.dreb/agent/model-routing-guide.md; an explicit global path may replace it.
  • Existing settings read/write and RPC settings surfaces expose the global configuration without allowing project shadowing.

2. Deterministic routing-guide loading and validation

Add a small parser/validator for the stage 1 guide contract.

  • Read the guide once per arbitration attempt, apply a hard size bound, parse its YAML frontmatter, and use those same bytes for the model call so validation and inference cannot observe different files.
  • Require schema version 1, valid metadata, unique canonical covered IDs, exactly one model section per covered ID, all required subsections, and the routing-safeguards section.
  • Compare covered_model_ids and model headings exactly with the current live AgentSession.scopedModels canonical set. An empty scope, stale/missing model, duplicate, or extra model fails closed.
  • Return bounded validated guide content/sections for prompt construction; do not silently repair malformed Markdown or substitute a different guide.

3. Fully headless Dispatch Arbiter

Create a mode-independent DispatchArbiter owned by AgentSession and feed it parent message_end and tool_execution_end events, mirroring the title setter's rolling-context pattern.

For each attempted dispatch it will build a structured, explicitly untrusted package containing:

  • the immutable child task and cwd;
  • the concrete proposed agent/model/thinking tuple;
  • names, descriptions, effective tool sets, and model defaults for every agent definition available to the parent's subagent tool (never editable definitions or extra capabilities);
  • canonical live candidate models and the validated guide;
  • bounded first/latest user intent plus recent labeled assistant/tool activity from RollingContextBuffer;
  • parent model and session title when present;
  • repository basename, cwd, branch, and a bounded metadata-only git-status summary;
  • parent/child lineage identifiers where available.

The task passed to the real child remains byte-for-byte unchanged. Before remote inference, the serialized arbiter package is secret-scrubbed with the existing built-in and configured extra patterns. Optional conversation and git context receive deterministic bounds and truncation markers; required task, guide, scope, and agent metadata are never silently dropped. If required content cannot fit the defined safety bounds, arbitration fails before inference.

The arbiter call will:

  • resolve only the configured arbiter model, obtain its API key through ModelRegistry, and call completeSimple() directly with no tools;
  • use the configured thinking level and one total timeout/abort path;
  • require exactly { "agent": string, "model": "provider/model", "thinking": ThinkingLevel } after strict host parsing;
  • permit at most one bounded retry for malformed structured output, with no fallback to the original proposal;
  • never expose or persist the prompt, raw response, reasoning, or arbitrary generated text.

Injected completion and clock/timeout dependencies will keep all automated tests offline.

4. Fail-closed pre-spawn integration

Integrate arbitration in the common subagent execution path.

  • Disabled mode is a true passthrough: no arbiter inference, guide read, event, or change to current child routing.
  • Enabled mode runs exactly once before each single spawn, once per parallel item after the existing concurrency gate, and once per chain step after substitution.
  • The final agent must be a key in the already-discovered agent map. Selecting it applies that definition's system prompt and filtered tools verbatim; the arbiter cannot rewrite either.
  • The final model must exactly match a canonical model in the active live scope. No fuzzy matching, out-of-scope fallback, or provider collapsing is allowed.
  • Final thinking is validated through the shared stage 1 capability validator.
  • Task, cwd, substituted chain content, parent linkage, and all non-routing request fields remain immutable.
  • Configuration, guide, scope, inference, timeout, parse, agent, model, thinking, auth/availability, and abort failures return an actionable failed SubagentResult and prove that spawn() was never called.

5. Typed arbitration observability and identity updates

Add one host-generated arbitration event for every enabled attempt, including unchanged decisions and failures.

The event will include an agent/operation ID, optional chain step, status, proposed tuple, nullable final tuple, changed-field list, and a bounded host error code/message. It will never contain model-authored explanations or raw inference data.

  • AgentSession attaches the background agent ID, persists the safe event as a non-context custom JSONL entry, and emits it to listeners.
  • A successful agent change updates the background registry atomically so later lifecycle state, TUI labels, RPC snapshots, and dashboard cards use the final agent identity rather than the requested one.
  • The TUI displays concise host-formatted changed/unchanged/failure action information and refreshes background status.
  • JSON and RPC receive the typed event through the existing event stream.
  • Dashboard server/client state records the decision on the matching background agent and visibly renders the final route and changed fields, including chain-step decisions.
  • background_agent_end and child lifecycle metadata continue to report what the spawned child actually used; arbitration metadata remains a separate pre-spawn record.

6. Documentation

Document:

  • the global-only settings, disabled default, exact model/scope/guide requirements, and fail-closed behavior;
  • the title-setter-style headless architecture and the precise context sent to the arbiter provider;
  • secret scrubbing, bounds, immutable child fields, no-tools guarantee, and raw-output prohibition;
  • routing precedence before and after arbitration;
  • typed JSON/RPC/session/dashboard event shapes and UI behavior;
  • the relationship between the stage 1 research skill's settings/argument scope and the arbiter's exact live runtime scope.

Files to create

  • packages/coding-agent/src/core/dispatch-arbiter.ts — rolling context, prompt package, direct tool-less inference, strict decision parsing, validation, timeout, and safe event formatting
  • packages/coding-agent/src/core/model-routing-guide.ts — bounded stage 1 guide parser, schema/section validation, and exact live-scope coverage check
  • packages/coding-agent/test/dispatch-arbiter.test.ts — isolated headless inference, context, privacy, output, retry, and failure tests
  • packages/coding-agent/test/subagent-arbiter.test.ts — all-mode pre-spawn integration and no-spawn-on-failure tests

Files to modify

  • packages/coding-agent/src/core/settings-manager.ts — global-only arbiter settings types, validated access, and persistence methods
  • packages/coding-agent/src/core/tools/subagent.ts — proposal/final routing phases, arbiter callback seam, all-mode integration, registry identity updates, and result failures
  • packages/coding-agent/src/core/agent-session.ts — own/feed the arbiter, provide live scope/context, persist and emit typed arbitration events, update background identity
  • packages/coding-agent/src/modes/interactive/interactive-mode.ts — render safe arbitration actions and refresh routed agent status
  • packages/coding-agent/src/modes/rpc/rpc-types.ts and rpc-mode.ts — global arbiter settings snapshots/updates and typed event coverage
  • packages/dashboard/src/shared/protocol.ts — arbitration metadata on background-agent DTOs
  • packages/dashboard/src/server/runtime-pool.ts — project safe routing events into runtime snapshots
  • packages/dashboard/src/client/state/reducer.ts — apply changed, unchanged, and failed decisions deterministically
  • packages/dashboard/src/client/screens/session.tsx and screens/subagent.tsx — show final route and per-step action metadata
  • packages/coding-agent/test/settings-manager.test.ts — global/project isolation and malformed-config behavior
  • packages/coding-agent/test/rpc-settings-commands.test.ts — global settings API validation and persistence
  • packages/coding-agent/test/agent-session-guardrails.test.ts — event persistence, safe parent delivery, and final identity behavior
  • packages/coding-agent/test/rpc-background-agents.test.ts — typed arbitration relay and snapshot metadata
  • packages/dashboard/test/reducer.test.ts, runtime-pool.test.ts, and client/screens.test.tsx — dashboard state, hydration, ordering, and visible route coverage
  • README.md
  • packages/coding-agent/README.md
  • packages/coding-agent/docs/settings.md
  • packages/coding-agent/docs/skills.md
  • packages/coding-agent/docs/agent-models.md
  • packages/coding-agent/docs/session.md
  • packages/coding-agent/docs/json.md
  • packages/coding-agent/docs/rpc.md
  • packages/coding-agent/docs/dashboard.md

If implementation shows an existing adjacent test or UI file is a clearer home, cases may be consolidated there, but no behavior or affected documentation layer above may be dropped.

Acceptance criteria

  • Arbitration is disabled by default and only global settings can configure it.
  • A valid explicit arbiter model, non-empty live session candidate scope, and guide with exact scope coverage are required when enabled.
  • The arbiter is a fully headless direct model call with a rolling parent-context buffer and zero tools or child-process capability.
  • Exactly one successful or failed arbitration event occurs before every actual child spawn in single, parallel, and post-substitution chain paths.
  • The arbiter can change only agent, scoped canonical model, and supported thinking; all other child inputs remain unchanged.
  • Selecting another agent uses an already-available definition verbatim and cannot grant capabilities outside the existing subagent tool.
  • Every listed failure prevents spawn() and surfaces a safe actionable error; no original-choice or parent-model fallback bypasses arbitration.
  • Structured unchanged/changed/failure decisions persist in parent JSONL and reach TUI, JSON, RPC, and dashboard without raw model content.
  • Secret scrubbing and deterministic bounds apply before inference, while child task/cwd remain untouched.
  • Automated tests use injected calls and no live APIs.

Testing approach

  • Guide parser: valid cold-start/history guides, malformed YAML, wrong schema, duplicate/missing/extra IDs, missing headings/subsections, over-size files, exact provider identity, and validate/read consistency.
  • Headless arbiter: disabled path, complete bounded package, title-style rolling context, secret/custom-pattern redaction, no tools, configured model/thinking/API key, timeout/abort, provider/auth failure, strict exact-object parsing, one malformed retry, and no raw response in errors/events.
  • Routing integration: unchanged and each changed field alone/together; inappropriate Explore corrected to feature-dev; routine lookup moved from a frontier model to a cheaper scoped model; unknown agent, out-of-scope provider/model, unsupported thinking, missing scope/guide/model, and all failures asserting zero child spawns.
  • Mode coverage: single once, each parallel item once under the existing semaphore, each chain step once after literal {previous} substitution, chain stop on failed arbitration, cwd rejection before arbitration, and cancellation during arbitration.
  • Immutability/security: byte-for-byte child task/cwd/context preservation, agent definition applied verbatim, normal tool exclusions retained, malicious instructions in task/guide/agent descriptions treated as data, and no prompt/response in transcript or JSONL.
  • Observability: changed, unchanged, and failed event types; per-step chain identity; parent custom-entry persistence without LLM-context reconstruction; registry/TUI/RPC/dashboard final identity; snapshot/hydration behavior; and child-effective completion metadata remaining distinct.
  • Regression and QA: targeted Vitest files, full npm test, npm run build, and npm run verify-workspace-links. After the required build, use the real dreb -p binary with a controlled guide/scope to verify disabled passthrough, unchanged routing, both priority corrections, one chain, and representative fail-closed configuration/guide/output failures.

Risks and boundaries

  • Guide, task, conversation, and agent metadata are all prompt-injection inputs. The arbiter system prompt must label them as data, the host must validate the only accepted fields, and no model-authored rationale may cross the control boundary.
  • The stage 1 guide is human-readable Markdown. Deterministic parsing and exact live-scope coverage are necessary, but this remains a small validator rather than a new guide service or policy engine.
  • The immediate background-start event necessarily begins with the requested identity. The subsequent typed arbitration transition must update the registry and every consumer before child events arrive, preventing stale labels without delaying the tool's immediate background acknowledgement.
  • Parallel arbitration can add provider pressure, but the existing background concurrency semaphore already bounds active calls; no second routing scheduler is needed.
  • Native structured-output features differ by provider. Strict host parsing and validation preserve provider portability; one malformed-output retry is the only model-output recovery.

No blocking design question remains. The implementation should prefer the title setter's small dependency-injected pattern and the existing subagent convergence point over new orchestration abstractions.


Plan created by mach6

@m-aebrer

Copy link
Copy Markdown
Collaborator Author

Acceptance Gap — Arbiter User Controls

The current implementation exposes the global Dispatch Arbiter policy through settings.json and raw RPC settings, and displays arbitration decisions, but it does not yet provide normal user controls in either settings UI.

This is a hard acceptance criterion, not optional follow-up work. Before this implementation is pushed as complete, both control surfaces must expose:

  • enable/disable;
  • exact arbiter model selection;
  • arbiter thinking level;
  • routing-guide path;
  • loud validation/readiness feedback rather than silently accepting an unusable configuration.

Required surfaces:

  1. Interactive TUI /settings.
  2. Web dashboard Settings.

The controls must write the global-only policy. Project settings must remain unable to enable, disable, or reconfigure arbitration.


Acceptance gap tracked by mach6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant