Skip to content

feat(fusion): batch trim by turn policy, claims need evidence, worker limits and hand-back, reasoning caps, cloud worker cap, pinned request (F8, F9, F19–F22) - #434

Open
plombeer31 wants to merge 8 commits into
harness/08-providersfrom
harness/09-loop
Open

plombeer31 wants to merge 8 commits into
harness/08-providersfrom
harness/09-loop

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

What

Before: the batch trim kept the first approval-gated call in emit order without asking the fusion gate, so [os.shell.run mkdir, fusion.delegate] kept the refused mkdir and dropped the delegation (9 minutes of local generation redone); a reply could claim "node --check on all files (all passed)" after a one-file check or no check at all; every worker had 40 steps and 45 minutes regardless of model speed, and a worker that wrote nothing spent all of it; no reasoning effort or output cap was ever sent for workers (87 % of one fan-out's output was hidden reasoning); the cloud worker pool was infinite and /runmode status printed the default rather than what runs; an earlier task's request could fall out of the prompt during a repair.

After: wouldRefuse / refusedToolNames on the fusion gate drive the trim (refused calls dropped first, fusion.delegate preferred in an orchestrator turn) and the notice names why each call was dropped; a shell node --check a b c warns that only the first file was checked, and a final reply claiming a check with no matching call gets one ### notice and one more step; the worker time limit is derived from measured tok/s and the declared output (clamped 10–45 min, cloud keeps 45), and a task with declared files and no write by half its budget returns needs_orchestrator with what it found; llm.runMode.fusion.workerReasoning and workerMaxOutputTokens reach the provider body (reasoning: {effort} on OpenRouter, reasoning_effort on openai-compatible) and the status table shows the fan-out's cloud spend; llm.runMode.fusion.cloudWorkers caps a cloud fan-out, the machine facts use the live slot count in external mode, and /runmode status says what will run; a ### request section (≤ 16,000 chars) is rendered right before ### conversation once the packer has dropped the turn that carried it. Config bump for the new fields.

Why

Design call D4 (early hand-back). Keep one shared predicate for the gate and the trim or they disagree and drop the wrong call. The claim check is one notice per turn, never a blocked reply, and matches only claims about this turn. Includes the reconcile commit that removes a duplicated wouldRefuse / FusionGateContext declaration, a duplicated reasoningEffort field on CompletionRequest, and renames the OpenRouter subclass's kind option to providerKind.

How it was verified

  • npm run lint clean
  • npx vitest run --minWorkers=1 --maxWorkers=3 src/agent src/config src/llm src/llm/provider src/llm/provider/openai src/llm/provider/openrouter src/llm/run-mode src/prompt src/runtime/bootstrap.test.ts src/runtime/llm-fallback-seam.test.ts src/runtime/llm-link-attempt.test.ts src/tools/fusion src/tools/os src/tracing/trace src/tui/agent-event-reducer.test.ts — 244 files / 3516 tests green, 3 skipped (baseline failures: none)
  • verified live: a worker that declared files and wrote nothing by half its budget came back needs_orchestrator instead of ok (the F19 hand-back, later tuned in PR 14)

Stacked on #433; merge in order.

Before choosing the survivor of an approval-gated batch, drop the calls
plan mode or the fusion orchestrator gate would refuse; on an
orchestrator turn prefer fusion.delegate over an earlier mutation. One
shared predicate — wouldRefuse(toolName, ctx) on both gates — so the
trim and the dispatch gates cannot disagree. The next-step notice names
refused calls with their gate and separates them from the calls to
retry; the batch_trimmed event and its trace row carry refused[].
(a) os.shell.run: a command that runs node --check / node -c over more
than one path (globs expanded as the subshell would) prepends a line
naming the one file node actually checked and asking for one command
per file — exit 0 says nothing about the rest.

(b) A final reply that claims a check ran (node --check, tests pass,
verified, ran the tests, lint passes, builds cleanly) with no matching
call this turn — a shell command containing the claimed check, or any
verify.* call — is held once with a notice and one more step, the same
shape as the invented-transcript rejection; the forced final step is
exempt. A second claiming reply is delivered with
details.unverifiedClaims on its result, which the trace records.
…honest /runmode status

llm.runMode.fusion.cloudWorkers (default 4) bounds a fan-out whose
worker leg has no slot pool; a higher maxWorkers is clamped and the
result names the knob. SlotManager.observedPoolSize() reports the pool
only once a /props answer sized it, and that number is threaded
bootstrap -> loop deps -> step deps -> buildPrompt so the ### fusion
machine facts state an external server's slots when observed, null
only when nothing is known. describeRunMode takes the worker facts and
/runmode status prints what will run: up to N local slots / up to N
cloud workers.
The request the runtime already records per turn for the workers'
briefs (pickOriginalRequest) now reaches the main loop's own prompt:
RunTurnOptions.originalRequest -> step context -> BuildPromptInput.
buildPrompt renders it as ### request (<= 16,000 chars) immediately
before ### conversation, only when the packer has dropped the user
turn that carried it — a combined follow-up record is matched by the
earlier message it was taken from — and then takes its room out of the
conversation cap with a second pack so the tail still fits. While the
carrier is in view nothing is rendered.
…, spend in the status table

llm.runMode.fusion.workerReasoning (low|medium|high) and
workerMaxOutputTokens (both unset by default) reach every worker
completion: ResolvedRunMode -> runWorkerTasks -> RunTurnOptions
{reasoningEffort, maxOutputTokens} -> step context -> LlmStreamParams
-> CompletionRequest on both transports. The OpenAI body builder takes
the provider family: OpenRouter sends reasoning: {effort},
openai-compatible sends reasoning_effort, others omit it;
maxOutputTokens fills max_tokens under a per-step maxTokens and over
the provider ceiling, and caps llama-server's n_predict the same way.
The delegate result's head line states the fan-out's spend when the
worker model is priced (resolveModelPricingFor), never for a local leg.
… nothing is written

LlamaServerClient keeps a rolling mean of timings.predicted_per_second
(predicted_n / predicted_ms on an older server) as
measuredTokensPerSecond(); fusion.delegate reads it per fan-out for a
slot-affine leg and each local worker's time limit becomes
clamp((briefChars/4 + 2000 x declaredFiles) / tokPerSec x 3, 10 min,
workerTimeoutMs) — the configured value stays the ceiling and a cloud
leg keeps it whole. D4: a task with declared files that has no
successful os.fs.write/edit/patch by half its step budget or half its
time limit is stopped and comes back needs_orchestrator with a forced
summary (steps and minutes used, tool tally, last results), never as
cancelled or failed.
…ng, workerMaxOutputTokens

v66. All three are additive; an older file parses with them absent.
Bring the tree to the state the package has once the other packages are merged around it: shared seams (config version, prompt tail order, fusion facts) resolved the same way as in the integrated build.
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