fix: use ACP protocol stopReason for proper agent completion detection

Replace unreliable 2-second timeout heuristic with proper protocol-based completion detection using PromptResponse.stopReason from ACP SDK.

Changes:
- Capture and handle PromptResponse.stopReason in sendPrompt()
- Set agent status based on stop reason: end_turn → completed, refusal → failed, cancelled → ready
- Remove 2-second timeout completion heuristic (was causing drift)
- Update SessionStateMessageSchema to match AgentInfo null types

This ensures agent completion is detected via the protocol spec, not manual polling/timing.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Mohamed Boudra
2025-10-21 16:49:23 +02:00
parent d883436395
commit cba8780bca
2 changed files with 23 additions and 27 deletions

View File

@@ -225,7 +225,7 @@ export class AgentManager {
this.notifySubscribers(agentId);
try {
await agent.connection.prompt({
const response = await agent.connection.prompt({
sessionId: agent.sessionId!,
prompt: [
{
@@ -234,6 +234,23 @@ export class AgentManager {
},
],
});
// Handle completion based on stopReason from the protocol
console.log(`[Agent ${agentId}] Prompt completed with stopReason: ${response.stopReason}`);
if (response.stopReason === "end_turn") {
agent.status = "completed";
} else if (response.stopReason === "refusal") {
agent.status = "failed";
agent.error = "Agent refused to process the prompt";
} else if (response.stopReason === "cancelled") {
agent.status = "ready";
} else {
// max_tokens, max_turn_requests - still completed but may be truncated
agent.status = "completed";
}
this.notifyStatusChange(agentId);
} catch (error) {
this.handleAgentError(
agentId,
@@ -435,29 +452,8 @@ export class AgentManager {
console.log(`[Agent ${agentId}] Mode changed to: ${agent.currentModeId}`);
}
// Track completion based on final message state
// ACP protocol indicates completion when the last agent_message_chunk arrives
// We detect this by tracking a completion timer that fires if no more updates arrive
const updateType = update.update.sessionUpdate;
// Clear and reset completion timer on any update during processing
if (agent.status === "processing") {
if ((agent as any).completionTimer) {
clearTimeout((agent as any).completionTimer);
}
// Set a new timer to mark as completed if no more updates arrive
// This is a heuristic - if no updates for 2 seconds, consider complete
(agent as any).completionTimer = setTimeout(() => {
if (agent.status === "processing") {
console.log(`[Agent ${agentId}] No more updates, marking as completed`);
agent.status = "completed";
this.notifyStatusChange(agentId);
}
}, 2000);
}
// Log update for debugging
const updateType = update.update.sessionUpdate;
console.log(
`[Agent ${agentId}] Session update:`,
updateType

View File

@@ -166,14 +166,14 @@ export const SessionStateMessageSchema = z.object({
status: z.string(),
createdAt: z.date(),
type: z.literal("claude"),
sessionId: z.string().optional(),
error: z.string().optional(),
currentModeId: z.string().optional(),
sessionId: z.string().nullable(),
error: z.string().nullable(),
currentModeId: z.string().nullable(),
availableModes: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
})).optional(),
})).nullable(),
})
),
commands: z.array(