[Feature request] Request-only assistant prefill for agent/pre-step (header-logged, never a session message) #2407
royenheart
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Background
My plugin dsh-plugin-opencode-omo replicates opencode + omo over dsh seams. On the budget-limited last step, opencode ends the request with an assistant-role
MAX_STEPS_PROMPTcontinuation (prompt.ts, llm.ts, max-steps.ts). The role is the feature: the text reads as "the assistant already speaking", so the model keeps writing and stops calling tools.dsh cannot produce that message.
agent/pre-steplisteners may only returnUserMessage[], the loop records the batch asuser/message, and by the timeagent/requestruns the messages are already derived and frozen, so roles cannot be rewritten. The plugin's current workaround injects the same text as a synthetic user message — the role semantics change and the fidelity gap is observable.Current state
PreStepDecisionis user-only (runtime-types.ts):The loop hardcodes the event role (agent.ts):
This mirrors the inbox, not an anti-assistant guard: the entered batch preserves the claimed
UserMessages'id/source, and every feeding queue accepts completeUserMessages (core.md, claimed-pre-step note).assistant/messageexists but is the step's one assembled model response, not a free assistant-role append. Wideningmessagesand appendingassistant/messageevents therefore breaks consumers that depend on that invariant:llmMs/ttftMs/decodeMs/decodeTokens.assistant/messageas the finalized stream, so the UI would end the step early.assistant/message— a prefill would fake a successful response.sourceEventSeqsagainstassistant/chunk; injected text has no chunk provenance.The session-prefix note supplies the correct pattern: request-only, header-logged message material — the prefix never enters the surface, its durable record is the
request/header, and the invariant companion recomputes it against every request. That note also dropped a request-only tail slot because an every-stepafterslot re-pays tokens and busts cache. This proposal reuses the pattern but bounds the cost: the tail exists only when a listener returns one (opencode returns it once, on the final step), and header participation keeps it accounted.Proposal
step()→buildRequest(). The canonical header records it and the request messages become[...boundaryMessages, assistantPrefill]:Add
assistantPrefill?: AssistantMessagetoEpochHeader, include it incanonicalHeader/headerEquals, and make the loop's invariant companion expectderived messages + header.assistantPrefill. "Model-visible ⟺ durably referenced" then holds: the header is the durable non-history record, exactly as for the prefix seam.Make the token-meter price the prefill from the header, and add a regression test: no
assistant/messagebefore the firstassistant/chunk, prefill present at the request tail and in the logged header, andundefinedkeeps requests byte-for-byte unchanged.Why this is safe:
request/header, folded by the invariant companion.undefinedmeans identical behavior and headers.change(opencode: one final step).Appendix: patch
Have tested and checked in my plugin:
Also required (same bookkeeping as the prefix seam):
request-header.tsequality, the loop's invariant companion, token-meter header estimation, and the interception test from step 4.Questions to confirm
assistantPrefillfit dsh's existing vocabulary (symmetric withsession-prefix'smessagePrefix), or wouldrequestTail/messageSuffixbetter express "request tail, not a session message"?assistant-role. ReusingAssistantMessagestampssource.kind: 'model'on text the model did not produce, breaking source-as-provenance; should the channel define its own envelope type (minimal role/content/source) so the type boundary and source semantics are explicit?deriveMessages()"?EpochHeader.assistantPrefill+canonicalHeader/headerEqualsthe right durable home under reconstructable-requests? Beyond the invariant companion and token-meter, are there other request-envelope consumers (compaction / pressure / session export) that must fold in this tail?agent/pre-stepcurrently passesmessagesthrough untouched. Should this tail likewise get only type-level validation (non-empty content, no tool-call blocks), or does the loop need any structural restriction — for reasons unrelated to any particular budget policy, is there a reason to limit it to a specific class of step?request/headersufficient as the only durable trace, or does dsh's UI/session export need a read-only marker (not a message bubble) for debugging and audit?Related
UserMessage[].assistant/messagemust not be reused for injected prefill.agent/pre-step.All reactions