Skip to content

Commit 4d4a659

Browse files
authored
fix(daemon): honor Kiro ACP turn completion (#6268)
1 parent 1da4f9e commit 4d4a659

5 files changed

Lines changed: 49 additions & 0 deletions

File tree

apps/daemon/src/agent-protocol/acp/session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export interface AttachAcpSessionOptions {
7878
stageTimeoutMs?: number;
7979
executionProfile?: ExecutionProfile;
8080
modelUnavailableErrorCode?: 'AMR_MODEL_UNAVAILABLE';
81+
// Some ACP adapters expose an explicit `turn_end` session update as their
82+
// terminal turn signal instead of returning the pending session/prompt RPC.
83+
// Keep this opt-in so standard ACP adapters still require the response.
84+
completePromptOnTurnEnd?: boolean;
8185
// When set, resume an existing upstream session instead of creating a new
8286
// one: the handshake sends `session/load { sessionId }` (the durable handle
8387
// captured from a prior run via `getDurableSessionId()`) rather than
@@ -131,6 +135,7 @@ export function attachAcpSession({
131135
stageTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS,
132136
executionProfile = 'filesystem',
133137
modelUnavailableErrorCode,
138+
completePromptOnTurnEnd = false,
134139
resumeSessionId,
135140
onCliReady,
136141
onSessionInit,
@@ -630,6 +635,14 @@ export function attachAcpSession({
630635
});
631636
emitAcpRawShapeDiagnostic(update);
632637
}
638+
if (
639+
completePromptOnTurnEnd &&
640+
promptRequestId !== null &&
641+
update.sessionUpdate === 'turn_end'
642+
) {
643+
finishCleanPrompt(update.usage);
644+
return;
645+
}
633646
if (update.sessionUpdate === 'agent_thought_chunk') {
634647
emitAcpRawShapeDiagnostic(update);
635648
const text = extractAcpUpdateText(update);

apps/daemon/src/runtimes/defs/kiro.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export const kiroAgentDef = {
1717
fallbackModels: [DEFAULT_MODEL_OPTION],
1818
buildArgs: () => ['acp'],
1919
streamFormat: 'acp-json-rpc',
20+
acpTurnEndCompletesPrompt: true,
2021
externalMcpInjection: 'acp-merge',
2122
} satisfies RuntimeAgentDef;

apps/daemon/src/runtimes/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ export type RuntimeAgentDef = {
224224
// default. Operators can still override per-process via
225225
// `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS` — that env wins.
226226
inactivityTimeoutMs?: number;
227+
// Opt-in compatibility for ACP adapters that terminate a prompt with a
228+
// `turn_end` session update rather than a session/prompt RPC response.
229+
acpTurnEndCompletesPrompt?: boolean;
227230
// Declarative authentication probe. When set, detection spawns
228231
// `<bin> <args>` after the version check and classifies the combined
229232
// stdout/stderr to derive `authStatus`. This replaces the previous

apps/daemon/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7698,6 +7698,7 @@ export async function startServer({
76987698
mcpServers,
76997699
envFormat: def.acpMcpEnvFormat ?? 'array',
77007700
executionProfile,
7701+
completePromptOnTurnEnd: def.acpTurnEndCompletesPrompt === true,
77017702
...(def.id === 'amr' ? { modelUnavailableErrorCode: 'AMR_MODEL_UNAVAILABLE' } : {}),
77027703
// Resume the prior upstream session (drives `session/load`) when the
77037704
// resume-identity guard says it is safe; otherwise a fresh session/new.

apps/daemon/tests/acp.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,37 @@ test('attachAcpSession does not double-kill a child that exits cleanly on stdin.
20592059
}
20602060
});
20612061

2062+
test('attachAcpSession accepts an opted-in ACP turn_end update as prompt completion', () => {
2063+
const child = new FakeAcpChild();
2064+
const events: Array<{ event: string; payload: unknown }> = [];
2065+
2066+
const session = attachAcpSession({
2067+
child: child as never,
2068+
prompt: 'hello',
2069+
cwd: '/tmp/od-project',
2070+
model: null,
2071+
mcpServers: [],
2072+
completePromptOnTurnEnd: true,
2073+
send: (event, payload) => events.push({ event, payload }),
2074+
});
2075+
2076+
writeAcpResult(child, 1, {});
2077+
writeAcpResult(child, 2, { sessionId: 'session-1' });
2078+
writeAcpUpdate(child, {
2079+
sessionUpdate: 'agent_message_chunk',
2080+
content: { type: 'text', text: 'done' },
2081+
});
2082+
writeAcpUpdate(child, {
2083+
sessionUpdate: 'turn_end',
2084+
usage: { inputTokens: 1, outputTokens: 1 },
2085+
});
2086+
child.emit('close', 0, null);
2087+
2088+
assert.equal(session.completedSuccessfully(), true);
2089+
assert.equal(session.hasFatalError(), false);
2090+
assert.equal(events.some((entry) => entry.event === 'error'), false);
2091+
});
2092+
20622093
test('attachAcpSession.completedSuccessfully reflects abort and fatal-error states', () => {
20632094
const child = new FakeAcpChild();
20642095

0 commit comments

Comments
 (0)