Repository navigation
feat(acp): steer an agent's running turn through _lazurio/steer instead of cancelling it - #141
immakermatty wants to merge 4 commits into
Conversation
…ad of cancelling it
A message sent while an ACP agent works is steered by cancelling the
running turn (session/cancel) and sending the message as the next
prompt, because ACP has no steering request. For Buddy (Lazurio Hermes
over ACP) every such message stopped his work and he started it again.
An agent that lists "lazurio.activeSteer": 1 in its initialize
capabilities' _meta now gets supportsActiveSteering for its session, and
steerTurn sends the extension request _lazurio/steer {sessionId, text}
with the message's text and its attachments as saved paths. The turn
goes on. An answer other than steered waits up to 10 s for the prompt
to settle and fails: a turn that ended leaves the message to the next
turn; a prompt the agent has not started yet is steered by the effect's
retry. Agents without the meta keep steering by interrupt and restart.
effect-acp's initialize normalization now keeps capabilities._meta.
Tests: the mock ACP agent advertises the meta under
T3_ACP_LAZURIO_ACTIVE_STEER=1 and answers _lazurio/steer. The adapter
test steers a running turn, which completes with the steer's text and
without session/cancel, and a steer after the turn fails. A second test
keeps restart steering for an agent without the meta.
Claude Opus 5.5 in Claude Code (T3 Code).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fork's CI refused the steer change: apps/server/scripts/acp-mock-agent.ts and packages/effect-acp/src/client.ts were not on the overlay's allowlist. Both are allowed now with their reason, and the release runbook lists the change as row 32 (#138) with its files; the next rebuild folds this into the row. Claude Opus 5.5 in Claude Code (T3 Code). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es is Buddy's own; he keeps pace and overview, reasons at medium in the conversation, and a message while he works steers him (DEV-6676) A task Buddy promised for 15 minutes took 50: bot results waited behind his own turn, other topics mixed into the promised block, and even a 37-second QR payment went through a bot. The Organization Admin decided on 2026-10-28: 1. a simple task of about two minutes, by feel, Buddy does himself, also in another Environment; what outgrows it goes to a bot and he returns to the user. This changes point 2 of the addendum "Buddy koordinuje a jeho pomocníci jsou boti"; he still implements nothing and never waits for a bot; 2. pace and overview: answer first, keep the promised time, stay on an agreed block's topic, one question at a time, say at once what he cannot see and ask for a screenshot, another app's buttons in its language; 3. reasoning effort medium in the conversation (default profile Důkladný); the installer writes it until Settings -> Buddy profiles; 4. a message while Buddy works steers him in the Lazurio chat and on Telegram (/busy stays the user's), and his messages to threads go with `auto`. AGENTS.md and manual/buddy.md say the same in one sentence each. Realization: Lazurio/LazurioPlatform#711, Lazurio/hermes-agent#22, Lazurio/t3code#141. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — Round 1, exact head abb60dc against 6f04174. CHANGES_REQUESTED.
P1 — A steer can reach the next turn. In packages/provider-acp/src/server/adapter.ts:7722–7741 we check activeTurn/providerTurnId once, then asynchronously resolve attachment paths and send only {sessionId,text}. startTurn can replace activeTurn while this effect is suspended (startTurn holds runtimeTransitionPermit, steerActiveTurn does not). If turn A settles and B starts before Hermes handles this request, Hermes #22 looks up the current admission for sessionId, not A’s turn ID, and can answer steered:true for B. T3 then marks A’s steering effect successful, so the message is neither delivered to A nor offered as A’s next-turn follow-up; it instead steers B. The 10-second not-steered fallback cannot protect this case because B acknowledges success. Please bind admission to the requested turn across the send/ack boundary (e.g. serialize with turn transition and reject a stale context before sending, or pass/verify an admission/generation token end-to-end), and add a deterministic A→B race test. The current two tests cover only an in-progress A and a late request after A with no B.
P2 — A retry can deliver the same steer twice. The extension request carries no message/effect id, and Hermes #22 enqueues session.steer without deduplication. If the owner accepts it but the ACP reply is lost or the request times out, ProviderTurnControlService reports failure and EffectWorker retries up to five times (apps/server/src/orchestration-v2/EffectWorker.ts:748–795). A second successful request inserts the identical guidance again. Please make acceptance idempotent with a stable message ID/receipt, or otherwise distinguish acknowledged acceptance from a retryable refusal; test an accepted request with a lost response.
Good: capability is negotiated on providerSession and CommandPolicy/Orchestrator read that session capability; non-opt-in agents retain interrupt/restart. Effect.fn span and the explained fnUntraced wrapper follow convention; failures are wrapped as typed ProviderAdapterSteerRunError. The success path, _meta normalization and release allowlist look aligned. Exact-head CI run 40772341696 passed server/web and CLI archives including darwin-arm64; git diff --check clean. In this isolated clone, release-contract tests reached 46/47 but the remaining test could not resolve @t3tools/shared because dependencies were not installed; I did not claim a local full pass. No merge or push.
|
Review of P1, a steer reaching the next turn. The fix binds the steer to its turn end to end, not only on T3's side:
P2, a duplicate after a lost reply. Greptile's case, a refusal while the prompt runs: a test in which the mock answers The Hermes half lands as a fork PR next to hermes-agent#22's row, and both come back to you for review. |
…nly that turn and only once Review of #141 found two holes: - a steer checked against turn A could reach turn B, if A ended and B started while the steer was on its way: the agent steered whatever prompt of the session ran, and T3 then took that for A's success, so the message became neither A's steer nor its follow-up; - a retry after a lost answer could steer the same message twice. An agent that negotiated lazurio.activeSteer now gets its turn's id in each prompt's _meta (lazurio.turnId). _lazurio/steer carries {sessionId, turnId, messageId, text}. The agent steers only the turn the request names, and each message once; Lazurio/hermes-agent#26 is that side. T3 also fails the steer without sending it when the turn ended while the message's attachments resolved. Tests: the mock agent models the agent side. The first prompt works until the test finishes it and answers with the steers it took: - the steered turn: the prompt names the turn, the steer names the turn and the message, no session/cancel; - a refusal while the prompt runs fails the steer only once the turn completes, so the orchestrator sends the message as the next turn; - a first steer that was taken but whose answer was lost: the retry with the same message steers once. 4 of 4 mutations are caught. Overlay row 32 says the same. Claude Opus 5.5 in Claude Code (T3 Code). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 1 answered in
The effect-worker step itself ( |
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty — Round 2, CHANGES_REQUESTED on exact head 3738f7c (base 6f04174). Reviewed with Lazurio/hermes-agent#26 at 4191139fe9c1218476bf63a5fef2f0d8cc9dfcd1.
P2 — Accepted-but-unacknowledged steer can become a duplicate follow-up after A completes. The stable messageId survives a retry while the same prompt is active, but ProviderTurnControlService.ts:130-137 declines before contacting Hermes when A is now completed. EffectWorker.ts:248-299 treats this as undelivered and dispatches the same message as a new prompt. Hermes #26 also forgets its accepted receipt once B starts (lazurio_active_steer.py:73-83). Scenario: A accepts steer M, ACP reply is lost or a lease-expired worker has not committed success, A completes, B starts, then the effect retries. M was delivered to A and is sent again as a follow-up; the same risk exists if the first attempt succeeds but its outbox settlement loses the lease. I reproduced the Hermes half with a temporary review probe (accepted A, settle A, start B, retry M => not_running). Persist/consult an exact turn+message acceptance outcome across completion before invoking the completed-turn follow-up; test via the real effect worker including lost reply/lease overlap, and ensure a genuinely refused steer still produces exactly one follow-up.
Also resolve the still-open review thread: the current refused-steer test AcpAdapterV2.test.ts:4151-4181 asserts the adapter error and terminal status, not the worker's subsequent one-follow-up dispatch. The owner-RPC lost-answer path in Hermes #26 can also steer twice within A because its failed-future cleanup cannot distinguish a refused steer from an accepted steer with an unacknowledged owner reply.
The new prompt _meta/steer parameters and stable T3 message id address the original later-turn routing while A is active; attachment-resolution recheck is sensible. The unreleased capability may stay at version 1. Diff check clean and current CI checks green. I did not run the local T3 suite in this sparse dependency-free review clone; no merge/push/release.
… refused it Review round 2 of #141: when the agent took a steered message but its answer was lost, and the turn completed before the effect's retry, the turn control service declined the retry without asking the agent, and the effect worker sent the message again as the next turn. Buddy got it twice. A runtime can now declare confirmsSteers: its agent answers a steer by turn and message, also after that turn ended, and never steers an ended turn. For such a runtime the service still sends the steer of a completed turn, and makes the message the next turn only when the agent refused it (ProviderAdapterSteerRunError.refused); a lost answer is retried. The ACP adapter declares it where the agent negotiated lazurio.activeSteer, sends _lazurio/steer for the turn the steer names whether or not it still runs, and marks the agent's steered:false as refused. Tests: the orchestrator and its effect worker with a confirming agent whose first answer is held while the worker's lease passes and the turn completes (taken and lost: no next turn; refused: exactly one; lost and not taken: exactly one), and Buddy's production runtime with the mock agent ending the turn before it answers (lost: one prompt with the message; refused: exactly one next prompt; no session/cancel). Both fail on the previous head with the message delivered twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turn (review of Lazurio/t3code#141) Review round 2 of Lazurio/t3code#141: the owner applied a steer of turn A, its answer to the viewer was lost, A ended and B started. The viewer had dropped the failed steer, so the client's retry for A met the turn check (B is the latest prompt) and read not_running; T3 Code then sent the message again as a new turn. A steer whose owner call failed now keeps the admission it was for. Asked again for the same turn and message, the viewer asks the owner about that admission under the same idempotency key, even after A ended and B started: the owner answers its receipt, or its fence (stale_generation, not_running) where it never applied the steer. B is never asked. A refusal is still not kept. Test: tests/acp_adapter/test_lazurio_active_steer.py, the lost owner answer asked again after the next turn started; it fails on the previous head with not_running. Overlay row updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty Exact-head QA for b08a805, reviewed with Lazurio/hermes-agent#26 at 4c2c29e22a5529f935b6cc98d48b2bab067b055c: APPROVED. A confirming runtime asks the agent about the named turn even after completion, and only an explicit refusal causes one follow-up; other providers retain the old completion path. The ACP request names the persisted turn/message, and the Hermes viewer fences it to the original admission or replays its receipt, never to the next turn. The orchestrator/effect-worker TestClock scenarios cover accepted/lost, refused, and not-taken/lost responses and the 31-second lease overlap; the production Buddy runtime/mock ACP scenario covers late answers and no cancel. Exact-head server/web and linux CI passed, zero unresolved threads, isolated diff --check passed; the darwin-arm64 archive job is still in progress, so this approval is not a claim that all CI is green or that the PR is ready to merge. No real client Organization, Module, or Machine names introduced in the scoped change. No merge/release performed.
Plan DEV-6676, the T3 half of #138. Base:
lazurio/rebuild-v0.1.3(#137, head6f04174a), until it replacesmain. Head:b08a8057, four commits. The Hermes half is Lazurio/hermes-agent#22 (merged) and #26 (the review fixes below).Problem
ACP has no steering request. When a message arrives while an ACP agent works, T3 Code cancels the running turn (
session/cancel) and sends the message as the next prompt. Buddy, Lazurio's Hermes agent, runs over ACP. So every message his person sent while he worked stopped his turn ("Operation interrupted: waiting for model response"), and he started the work again. The product owner's requirement of 2026-10-28: a message sent while Buddy works must never stop him, and must reach him once.Fix
The capability. An ACP agent can say it steers: it lists
"lazurio.activeSteer": 1in the_metaof itsinitializeagent capabilities. For such an agent:supportsActiveSteeringfor its session, so the orchestrator pickssteer_active;_meta(lazurio.turnId);steerTurnsends the extension request_lazurio/steer{sessionId, turnId, messageId, text}.textis the message with its attachments as saved paths, the same text a prompt of that message would carry.messageIdis the T3 message id, so it is stable across the effect worker's retries. The turn goes on;Which message becomes the next turn. Such an agent's session runtime declares
confirmsSteers(ProviderAdapter.ts). For it:steered: false, aProviderAdapterSteerRunErrorwithrefused) makes the message the next turn (turnCompleted, as for other providers);Agents without the meta keep steering by interrupt and restart.
effect-acp. Its
initializenormalization now keepscapabilities._meta, which it used to drop.Overlay bookkeeping. The fork's CI allows the upstream files this change touches, each with a reason:
apps/server/scripts/acp-mock-agent.ts,packages/effect-acp/src/client.ts, and since commit 4packages/provider-core/src/server/ProviderAdapter.tsandapps/server/src/orchestration-v2/ProviderTurnControlService.ts. The release runbook lists the change as row 32. The new tests live inapps/server/src/lazurio/.Review rounds
Round 1 (commit 3,
3738f7c9). Pablo found two holes.The fix binds the steer to its turn and message end to end: the turn id in the prompt's
_meta,turnIdandmessageIdin the steer, and an agent that steers only that turn and each message once.Round 2 (commit 4,
b08a8057). The agent took message M into turn A, but its answer was lost, and A completed before the effect's retry.ProviderTurnControlServicedeclined the retry without asking the agent, and the effect worker sent M again as a new prompt. Buddy got M twice. Fixed byconfirmsSteersabove. Hermes #26 keeps the matching receipts: the viewer answers a repeat after B started, and after a lost owner answer it asks the owner about A's admission under the same key.Lease overlap. The effect worker never runs a second attempt while the first one holds the effect:
claimNextclaims onlypendingeffects;The new integration test lets the 30 s lease pass while the first attempt holds the steer, and drains the worker: no second attempt. A retry comes only after the first attempt failed, which is the lost-answer case. Even then, a second follow-up would reuse the effect's command id,
command:steer-follow-up:<effect>, so it is deduplicated.What does not change
session/cancel.confirmsSteersand keep inferring the follow-up from the turn's end.Known limitations
not_running, for example a prompt queued behind another turn of the same Hermes session. The effect worker retries up to 5 times; each attempt waits at most 10 s for the prompt and uses 100–800 ms backoff. If the prompt never starts, the steer fails instead of reaching the turn. Tracked in acp: a steer sent while this editor's prompt is still queued never reaches its turn hermes-agent#23.Verified
On the Mac, with this worktree's own
vp i, headb08a8057.apps/server/src/lazurio/steerConfirmation.integration.test.ts(new): the orchestrator and its effect worker with a confirming agent. The agent's first answer is held while the worker's lease passes and the turn completes. 3 pass:None of them interrupts the turn.
apps/server/src/lazurio/buddySteer.orchestrator.test.ts(new): Buddy's production runtime and the effect worker daemon. The mock agent ends the turn before it answers the steer. 2 pass:There is no
session/cancelin either.On commit 3, both new files fail with the message delivered twice:
['message:first', 'message:steer'], and three prompts. The 5 new tests passed 5 runs in a row, 25/25.AcpAdapterV2.test.ts, the 4 Lazurio steer tests:confirmsSteers;refused, and its request names the ended turn;refused, and its retry after the turn ended succeeds;Related suites:
AcpAdapterV2.test.ts,SteeringCompletion.integration.test.tsandapps/server/src/lazurio/: 40 files, 373 pass.vp run check:changed:apps/*/src/lazurio/that are already on the base.Contract:
scripts/lazurio-release-contract.test.mjs, 47 pass.Full suite: CI on Linux owns it. On commit 3, the Mac's 22 failures were all outside ACP.
The Hermes side is proven in Lazurio/hermes-agent#22 and #26 against a real gateway and
hermes acp.Rollout
Claude Opus 5.5 in Claude Code (T3 Code).
🤖 Generated with Claude Code