Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,79 @@ describe("AcpRegistryAdapterV2", () => {
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("follows the work state of any agent that negotiates ACP v2", () =>
Effect.gen(function* () {
const gate = makeProviderReplayGate(["elsewhere"]);
const { update, idle, prompt } = claudeFrames;
const reply = (messageId: string, text: string) =>
update(CLAUDE_ROOT_SESSION, {
sessionUpdate: "agent_message_chunk",
messageId,
content: { type: "text", text },
});
const session = yield* openClaudeReplaySession(
[
...claudeFrames.open,
...prompt(1),
reply("reply-1", "Working on it."),
idle("end_turn"),
claudeMarker("foreground idle"),
{
...update(CLAUDE_ROOT_SESSION, {
sessionUpdate: "user_message",
messageId: "elsewhere-1",
content: [{ type: "text", text: "Sent from another client" }],
}),
label: "elsewhere",
},
update(CLAUDE_ROOT_SESSION, { sessionUpdate: "state_update", state: "running" }),
reply("reply-2", "Answered elsewhere."),
idle("end_turn"),
claudeMarker("elsewhere idle"),
],
gate,
true,
"fixture-agent",
);
yield* session.runtime.startTurn(session.turnInput(1));
const foreground = yield* session.until((event) => event.type === "turn.terminal");
const terminal = foreground.at(-1)!;
if (terminal.type !== "turn.terminal") throw new Error("Missing terminal");
assert.equal(terminal.status, "completed");
assert.isTrue(
foreground.some(
(event) => event.type === "message.updated" && event.message.text === "Working on it.",
),
"an accepted prompt is not the end of the turn; idle is",
);
assert.isFalse(
foreground.some(
(event) => event.type === "message.updated" && event.message.text === "test prompt",
),
"the agent's echo of the submitted prompt only signals acceptance",
);
yield* session.throughMarker("foreground idle");

gate.release("elsewhere");
yield* Queue.take(session.offered);
yield* session.throughMarker("elsewhere idle");
assert.lengthOf(session.offers, 1);
yield* session.offers[0]!.dispatchIfCurrent!(Effect.void);
yield* session.runtime.startTurn(session.turnInput(2, { continuation: true }));
const continuation = yield* session.until((event) => event.type === "turn.terminal");
const continued = continuation.at(-1)!;
if (continued.type !== "turn.terminal") throw new Error("Missing continuation terminal");
assert.equal(continued.status, "completed");
assert.isTrue(
continuation.some(
(event) =>
event.type === "message.updated" && event.message.text === "Answered elsewhere.",
),
"work the agent started on its own reaches the thread",
);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("keeps stopped child tools interrupted when a late output chunk arrives", () =>
Effect.gen(function* () {
const { update } = claudeFrames;
Expand Down
4 changes: 3 additions & 1 deletion docs/user/providers-acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Pi, and [Antigravity](./providers-antigravity.md), don't appear in the Registry
T3 Code prefers the current ACP v2 preview protocol and uses its richer messages, usage, plans,
configuration, compaction, and agent-terminal updates when the agent supports them. It also
negotiates ACP v1 for Registry agents that have not migrated yet, so they continue to work through
the same generic integration.
the same generic integration. On ACP v2, T3 Code follows the agent's reported work state: a turn
ends when the agent goes idle, and work the agent starts on its own, such as answering a message
sent from another client, appears in the thread.

Claude Agent uses the ACP v2 preview by default for Registry and local `claude-agent-acp`
commands. If you encounter preview issues, set `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=0` in the
Expand Down
4 changes: 3 additions & 1 deletion packages/provider-acp-registry/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,11 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu
...(registryAgentId === "mistral-vibe"
? { registerExtensions: registerMistralVibeAcpExtensions }
: {}),
// ACP v2 reports turn completion, and work the agent starts on its own, through
// `state_update`; the adapter follows it only once an agent negotiates v2.
followAgentWorkState: true,
...(isClaude
? {
followAgentWorkState: true,
extractSubagentUpdate: extractClaudeSubagentUpdate,
isProvisionalSubagentTool: isClaudeProvisionalSubagentTool,
extractSubagentSessionEvent: extractClaudeSubagentSessionEvent,
Expand Down
Loading