Skip to content

Commit 6dbd05e

Browse files
committed
fix: tighten acp tool updates
1 parent c37f1cc commit 6dbd05e

7 files changed

Lines changed: 72 additions & 18 deletions

File tree

docs/adr/0008-command-descriptor-registry.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ every consumer table is **derived** by pure, parity-tested projection:
4747

4848
ACP is a consumer of the command surface, not a new command registry. It advertises the same runnable
4949
commands through `available_commands_update` and parses prompt text into CLI-shaped command lines
50-
before executing through the existing command contracts and `AgentDeviceClient` path. Its slash-command
51-
syntax is an ACP transport affordance, not a separate command identity.
50+
before executing through the CLI command reader, existing command contracts, and `AgentDeviceClient`.
51+
Its slash-command syntax is an ACP transport affordance, not a separate command identity.
5252

5353
This **composes with**, and is bound by, ADR 0003's four invariants: daemon-owned declaration (never inlined
5454
into the public surface), the predicate interface unchanged, no leakage of daemon-only traits into public

src/acp/__tests__/prompt-runner.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,14 @@ test('unparseable lines fail individually while runnable lines still execute', a
145145
assert.equal(stopReason, 'end_turn');
146146
assert.deepEqual(executed, ['devices']);
147147
const failedCall = updates[0] as Extract<AcpSessionUpdate, { sessionUpdate: 'tool_call' }>;
148-
assert.equal(failedCall.status, 'failed');
149-
assert.match(JSON.stringify(failedCall.content), /Unknown command: frobnicate/);
148+
assert.equal(failedCall.status, 'in_progress');
149+
const failedUpdate = updates[1] as Extract<
150+
AcpSessionUpdate,
151+
{ sessionUpdate: 'tool_call_update' }
152+
>;
153+
assert.equal(failedUpdate.toolCallId, failedCall.toolCallId);
154+
assert.equal(failedUpdate.status, 'failed');
155+
assert.match(JSON.stringify(failedUpdate.content), /Unknown command: frobnicate/);
150156
});
151157

152158
test('sticky flags from one line carry into later lines of the same session', async () => {
@@ -213,6 +219,33 @@ test('screenshot results attach an inline image when the artifact is readable',
213219
assert.ok(image, 'expected an inline image content block');
214220
});
215221

222+
test('large ref-issuing command results omit rawOutput from completed tool updates', async () => {
223+
const rawOutputs: unknown[] = [];
224+
const runner = runnerWith({
225+
runCommandLine: async ({ command }) => ({
226+
result:
227+
command === 'snapshot'
228+
? { nodes: [{ ref: 'e1', label: 'Checkout' }], refsGeneration: 7 }
229+
: { ref: '@e1', label: 'Checkout', refsGeneration: 7 },
230+
cliOutput: { data: {}, text: `${command} done` },
231+
}),
232+
});
233+
const { updates, sendUpdate } = collectUpdates();
234+
235+
await runner.runPrompt({
236+
session: makeSession(),
237+
promptText: 'snapshot -i\nfind Checkout',
238+
sendUpdate,
239+
});
240+
241+
for (const update of updates) {
242+
if (update.sessionUpdate === 'tool_call_update' && update.status === 'completed') {
243+
rawOutputs.push(update.rawOutput);
244+
}
245+
}
246+
assert.deepEqual(rawOutputs, [undefined, undefined]);
247+
});
248+
216249
test('daemon progress events stream as in_progress tool call updates', async () => {
217250
let progressSink: RequestProgressSink | undefined;
218251
const runner = runnerWith({

src/acp/prompt-runner.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,7 @@ export function createPromptRunner(deps: PromptRunnerDeps = {}): PromptRunner {
5353
for (const line of lines) {
5454
if (session.cancelled) return 'cancelled';
5555
if (line.kind === 'error') {
56-
sendUpdate({
57-
sessionUpdate: 'tool_call',
58-
toolCallId: nextToolCallId(session),
59-
title: line.line,
60-
kind: 'other',
61-
status: 'failed',
62-
content: [textContent(line.error)],
63-
});
56+
sendParseErrorToolCall(session, line, sendUpdate);
6457
outcomes.push(`✗ ${line.line}${line.error}`);
6558
continue;
6659
}
@@ -124,7 +117,7 @@ async function runCommandToolCall(params: {
124117
toolCallId,
125118
status: 'completed',
126119
content: commandResultContent({ line, result, text: cliOutput?.text ?? undefined, deps }),
127-
rawOutput: result,
120+
...rawOutputUpdate(line, result),
128121
});
129122
return `✓ ${line.line}`;
130123
} catch (error) {
@@ -140,6 +133,32 @@ async function runCommandToolCall(params: {
140133
}
141134
}
142135

136+
function sendParseErrorToolCall(
137+
session: AcpSessionState,
138+
line: { line: string; error: string },
139+
sendUpdate: AcpUpdateSink,
140+
): void {
141+
const toolCallId = nextToolCallId(session);
142+
sendUpdate({
143+
sessionUpdate: 'tool_call',
144+
toolCallId,
145+
title: line.line,
146+
kind: 'other',
147+
status: 'in_progress',
148+
});
149+
sendUpdate({
150+
sessionUpdate: 'tool_call_update',
151+
toolCallId,
152+
status: 'failed',
153+
content: [textContent(line.error)],
154+
});
155+
}
156+
157+
function rawOutputUpdate(line: RunnableLine, result: unknown): { rawOutput?: unknown } {
158+
if (line.command === 'snapshot' || line.command === 'find') return {};
159+
return { rawOutput: result };
160+
}
161+
143162
function commandResultContent(params: {
144163
line: RunnableLine;
145164
result: unknown;

src/utils/cli-command-overrides.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const SCHEMA_ONLY_CLI_COMMAND_SCHEMAS = {
1313
acp: {
1414
helpDescription: `Start the stdio ACP (Agent Client Protocol) agent.
1515
16-
ACP is a deterministic agent interface for ACP-capable editors and agent clients. It does not add LLM behavior: each text prompt line is parsed as one agent-device command line, with optional ACP slash-command syntax such as /devices or /snapshot -i. Commands execute through the same command contracts and AgentDeviceClient path as CLI/MCP commands, while stdout remains newline-delimited JSON-RPC only.`,
16+
ACP is a deterministic agent interface for ACP-capable editors and agent clients. It does not add LLM behavior: each text prompt line is parsed as one agent-device command line, with optional ACP slash-command syntax such as /devices or /snapshot -i. Commands execute through the CLI command reader, shared command contracts, and AgentDeviceClient; stdout remains newline-delimited JSON-RPC only.`,
1717
summary: 'Start ACP agent',
1818
},
1919
cdp: {

website/docs/docs/agent-setup.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: Configure Cursor, Codex, Claude Code, Windsurf, Cline, Goose, Zed,
99

1010
Use this page to wire Cursor, Codex, Claude Code, Windsurf, Cline, Goose, Zed, ACP clients, or another coding agent into mobile, TV, desktop, and web app verification. It covers skills, project rules, MCP setup, and ACP setup for React Native QA, Expo app verification, iOS Simulator automation, Android Emulator automation, tvOS checks, Android TV checks, web browser sessions, debugging, profiling, and exploratory QA.
1111

12-
The short version: install the CLI, make the agent read version-matched help, and let the agent use CLI commands, MCP tools, or the ACP agent depending on what its client supports. MCP tools and ACP command turns use command contracts backed by the same `AgentDeviceClient` execution path as the CLI adapters.
12+
The short version: install the CLI, make the agent read version-matched help, and let the agent use CLI commands, MCP tools, or the ACP agent depending on what its client supports. MCP tools and ACP command turns share command contracts and the same daemon/client implementation; ACP reads CLI-shaped prompt lines while MCP receives structured tool input.
1313

1414
## Prerequisite: install the CLI
1515

@@ -92,10 +92,12 @@ Registry metadata uses MCP name `io.github.callstackincubator/agent-device`, npm
9292

9393
## ACP agent (Zed and other ACP clients)
9494

95-
`agent-device acp` starts a stdio [ACP](https://agentclientprotocol.com/) (Agent Client Protocol) agent, so ACP clients such as Zed can drive devices from the agent panel. It is a deterministic agent, not an LLM: each line of a prompt is one agent-device command in CLI syntax (ACP slash commands such as `/devices` and an optional leading `agent-device` are accepted), executed through the same command contracts and `AgentDeviceClient` path as MCP tools. Command executions stream back as ACP tool calls with daemon progress updates, screenshots attach as inline images, and available commands are advertised per session.
95+
`agent-device acp` starts a stdio [ACP](https://agentclientprotocol.com/) (Agent Client Protocol) agent, so ACP clients such as Zed can drive devices from the agent panel. It is a deterministic agent, not an LLM: each line of a prompt is one agent-device command in CLI syntax (ACP slash commands such as `/devices` and an optional leading `agent-device` are accepted), executed through the CLI command reader, shared command contracts, and `AgentDeviceClient`. Command executions stream back as ACP tool calls with daemon progress updates, screenshots attach as inline images, and available commands are advertised per session.
9696

9797
Target selection sticks within a session: after `open com.example.app --platform ios`, later lines such as `snapshot -i` reuse the same `--platform`, `--device`, `--udid`, `--session`, and `--state-dir` values until a line overrides them. Project config resolution uses the working directory the ACP client passes on `session/new`.
9898

99+
Refs follow CLI semantics in ACP prompts. MCP tools auto-pin plain refs across tool calls, but ACP prompt lines do not add MCP ref-generation pins; after another `snapshot` or `find`, use the current refs from the latest output or durable selectors.
100+
99101
Zed `settings.json`:
100102

101103
```json

website/docs/docs/commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ For ACP-aware clients that support Agent Client Protocol agents, run:
3838
agent-device acp
3939
```
4040

41-
The ACP agent exposes installed commands as a deterministic prompt-line interface. ACP clients send prompt text such as `/devices` or `snapshot -i`; `agent-device` parses one command per line, executes through the same command contracts and `AgentDeviceClient` path, streams ACP tool-call updates, and refuses natural-language prompts instead of guessing.
41+
The ACP agent exposes installed commands as a deterministic prompt-line interface. ACP clients send prompt text such as `/devices` or `snapshot -i`; `agent-device` parses one command per line, executes through the CLI command reader, shared command contracts, and `AgentDeviceClient`, streams ACP tool-call updates, and refuses natural-language prompts instead of guessing.
4242

4343
## Navigation
4444

website/docs/docs/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ It complements scripted test frameworks such as Appium, Maestro, Detox, XCTest,
6363

6464
MCP support exposes direct structured tools for installed `agent-device` commands. Tools use structured input contracts through `AgentDeviceClient`, so MCP clients can call device workflows directly while the daemon remains the execution source of truth.
6565

66-
ACP support exposes a deterministic stdio agent for ACP-capable editors and agent clients. The ACP agent interprets prompt text as explicit `agent-device` command lines, including advertised slash commands such as `/devices`, and executes them through the same command contracts and `AgentDeviceClient` path. It is not an LLM and does not guess natural-language actions.
66+
ACP support exposes a deterministic stdio agent for ACP-capable editors and agent clients. The ACP agent interprets prompt text as explicit `agent-device` command lines, including advertised slash commands such as `/devices`, and executes them through the CLI command reader, shared command contracts, and `AgentDeviceClient`. It is not an LLM and does not guess natural-language actions.
6767

6868
## Next steps
6969

0 commit comments

Comments
 (0)