feat(dispatch): wire autoLoop to DispatchLayer via SF_INLINE_DISPATCH (M010/S03)

run-unit.js: new tryInlineDispatch helper routes inline-eligible unit
types through the M010/S02 DispatchLayer when env SF_INLINE_DISPATCH=1.
Safe by default — without the env var OR for non-eligible unit types
(any unit not in INLINE_ELIGIBLE_UNITS), behavior is byte-identical
to before. With the env var set on validate-milestone / complete-milestone
/ reassess-roadmap, the autoLoop reaches runUnitInline → runSubagent
in-process, no spawn.

The helper translates DispatchLayer's {ok, output, exitCode, stderr}
into the UnitResult shape that autoLoop's resolveAgentEnd/finalize
chain expects, so downstream handling works unchanged.

13/13 M010 tests still pass. M010/S03 marked complete.

R049 added: Multi-Provider Parallel Routing — different concurrent units
route to different LLM providers based on quota/specialty/cost/failover.
Builds on R046 + R017 + model-router scoring. Future M036.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-17 03:46:48 +02:00
parent 1b3e09d527
commit bbce6827aa
2 changed files with 119 additions and 0 deletions

View file

@ -612,3 +612,14 @@ ADR-0000 declares SF a **purpose-to-software compiler**. R036R040 codify that
- **T-level**: every task plan opens with "Implements S<x>'s success-criterion <N> via concrete artifact <path>."
- **Code-level**: every SF-authored commit message footer carries `purposeAnchor: <R-id>` (cross-cuts R036)
- **Doctor**: a new `purpose-chain-integrity` check walks the chain and refuses any link missing its anchor (cross-cuts R031 ADR enforcement)
### R049 — Multi-Provider Parallel Routing
- Class: differentiator
- Status: active
- Description: Across-unit parallel dispatch (R046) routes different concurrent units to different LLM providers based on (a) provider per-minute quota, (b) model specialty (kimi-for-coding for code-heavy units, minimax for synthesis, gemini for long-context research), (c) cost optimization (cheaper models for low-stakes units), (d) failover (avoid the provider that just rate-limited). The scheduler reads model-router config + live quota state and picks per-unit. Different models work simultaneously on different slices, spreading load across providers.
- Why it matters: Single-provider parallel maxes out at that provider's per-minute quota; multi-provider parallel scales N× as we add providers. Combined with R046, this is what makes the 2-4 week horizon collapse to days for embarrassingly parallel work (e.g. plan-slice across 20 independent slices).
- Source: spec
- Primary owning slice: unmapped (future "M036 Multi-Provider Parallel Routing")
- Supporting slices: none
- Validation: unmapped
- Notes: Builds on existing model-router.js scoring + R017's tool-failure demotion + R046's autonomous parallel dispatch. The new piece is the scheduler-level multi-model assignment per dispatch slot.

View file

@ -27,7 +27,98 @@ import {
countChangedFiles,
resetRunawayGuardState,
} from "../uok/auto-runaway-guard.js";
import { DispatchLayer } from "../dispatch/dispatch-layer.js";
import { isInlineEligible } from "../dispatch/run-unit-inline.js";
import { swarmDispatchAndWait } from "../uok/swarm-dispatch.js";
/**
* #M010/S03: Try inline-scope dispatch via DispatchLayer.
*
* Returns a UnitResult-shaped object if the inline path was taken; null if
* the unit isn't inline-eligible (caller falls through to swarm/legacy).
*
* On failure, returns a structured `{status: "cancelled", errorContext: {...}}`
* matching the contract that runUnitViaSwarm produces, so the autoLoop's
* downstream handling (resolveAgentEnd, finalize, etc.) works unchanged.
*
* Safe by default only fires when env SF_INLINE_DISPATCH=1 AND the unit
* type is in INLINE_ELIGIBLE_UNITS.
*/
async function tryInlineDispatch(ctx, s, unitType, unitId, _prompt, options) {
if (!isInlineEligible(unitType)) return null;
const basePath = s.basePath ?? ctx.basePath ?? process.cwd();
debugLog("runUnit", {
phase: "inline-route",
unitType,
unitId,
basePath,
});
let layer;
try {
layer = new DispatchLayer(basePath);
} catch (err) {
debugLog("runUnit", {
phase: "inline-route-construct-failed",
unitType,
unitId,
error: err instanceof Error ? err.message : String(err),
});
return null; // fall through to swarm
}
const dispatchOpts = {
isolation: "full",
coordination: "managed",
scope: "inline",
mode: "single",
unitType,
unitId,
...(options?.model ? { model: options.model } : {}),
...(options?.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
...(options?.noOutputTimeoutMs
? { noOutputTimeoutMs: options.noOutputTimeoutMs }
: {}),
...(options?.signal ? { signal: options.signal } : {}),
...(options?.extras ? { extras: options.extras } : {}),
};
const result = await layer.dispatch(dispatchOpts);
if (result.ok) {
debugLog("runUnit", {
phase: "inline-route-completed",
unitType,
unitId,
outputLength: (result.output ?? "").length,
});
return {
status: "completed",
event: {
messages: [
{
role: "assistant",
content: [{ type: "text", text: result.output ?? "" }],
},
],
},
_via: "inline",
};
}
// Inline path returned a structured failure. Surface as cancelled UnitResult.
debugLog("runUnit", {
phase: "inline-route-failed",
unitType,
unitId,
exitCode: result.exitCode,
stderr: result.stderr,
});
return {
status: "cancelled",
errorContext: {
message: result.stderr ?? "inline dispatch failed",
category: "inline-failure",
isTransient: false,
},
_via: "inline",
};
}
import { logWarning } from "../workflow-logger.js";
import {
_clearCurrentResolve,
@ -1116,6 +1207,23 @@ async function runUnitViaSwarm(ctx, _pi, s, unitType, unitId, prompt, options) {
* Default: false (each new unit starts with a clean session).
*/
export async function runUnit(ctx, pi, s, unitType, unitId, prompt, options) {
// #M010/S03: Feature-flagged inline-scope path (env opt-in:
// SF_INLINE_DISPATCH=1). Routes inline-eligible unit types through
// DispatchLayer (M010/S02) → runUnitInline (M010/S01). Falls back to
// swarm/legacy paths when the env var isn't set OR the unit type isn't
// in INLINE_ELIGIBLE_UNITS. Safe by default — existing flows untouched.
if (process.env.SF_INLINE_DISPATCH === "1") {
const inline = await tryInlineDispatch(
ctx,
s,
unitType,
unitId,
prompt,
options,
);
if (inline) return inline;
}
// Feature-flagged swarm path — default on in headless mode, opt-in elsewhere.
if (shouldRouteRunUnitViaSwarm(options)) {
return runUnitViaSwarm(ctx, pi, s, unitType, unitId, prompt, options);