fix(listen): expose interrupted tool state for reconnect hydration (#1295)

This commit is contained in:
Charles Packer
2026-03-05 23:17:43 -08:00
committed by GitHub
parent 52f2cc9924
commit 8a657ad87d
2 changed files with 85 additions and 0 deletions

View File

@@ -261,6 +261,51 @@ describe("listen-client controlResponseCapable latch", () => {
}); });
}); });
describe("listen-client state_response pending interrupt snapshot", () => {
test("includes queued interrupted tool returns for refresh hydration", () => {
const runtime = __listenClientTestUtils.createRuntime();
__listenClientTestUtils.populateInterruptQueue(runtime, {
lastExecutionResults: null,
lastExecutingToolCallIds: ["call-running-1"],
lastNeedsUserInputToolCallIds: [],
agentId: "agent-1",
conversationId: "conv-1",
});
const snapshot = __listenClientTestUtils.buildStateResponse(runtime, 17);
expect(snapshot.pending_interrupt).toEqual({
agent_id: "agent-1",
conversation_id: "conv-1",
interrupted_tool_call_ids: ["call-running-1"],
tool_returns: [
{
tool_call_id: "call-running-1",
status: "error",
tool_return: INTERRUPTED_BY_USER,
},
],
});
});
test("does not expose pending approval denials as interrupted tool state", () => {
const runtime = __listenClientTestUtils.createRuntime();
__listenClientTestUtils.populateInterruptQueue(runtime, {
lastExecutionResults: null,
lastExecutingToolCallIds: [],
lastNeedsUserInputToolCallIds: ["call-awaiting-approval"],
agentId: "agent-1",
conversationId: "conv-1",
});
const snapshot = __listenClientTestUtils.buildStateResponse(runtime, 18);
expect(snapshot.pending_interrupt).toBeNull();
});
});
describe("listen-client capability-gated approval flow", () => { describe("listen-client capability-gated approval flow", () => {
test("control_response with allow + updatedInput rewrites tool args", async () => { test("control_response with allow + updatedInput rewrites tool args", async () => {
const runtime = __listenClientTestUtils.createRuntime(); const runtime = __listenClientTestUtils.createRuntime();

View File

@@ -212,6 +212,12 @@ interface StateResponseMessage {
request_id: string; request_id: string;
request: ControlRequest["request"]; request: ControlRequest["request"];
}>; }>;
pending_interrupt: {
agent_id: string;
conversation_id: string;
interrupted_tool_call_ids: string[];
tool_returns: InterruptToolReturn[];
} | null;
queue: { queue: {
queue_len: number; queue_len: number;
pending_turns: number; pending_turns: number;
@@ -747,6 +753,7 @@ function buildStateResponse(
started_at: runtime.activeRunStartedAt, started_at: runtime.activeRunStartedAt,
}, },
pending_control_requests: pendingControlRequests, pending_control_requests: pendingControlRequests,
pending_interrupt: buildPendingInterruptState(runtime),
queue: { queue: {
queue_len: runtime.queueRuntime.length, queue_len: runtime.queueRuntime.length,
pending_turns: runtime.pendingTurns, pending_turns: runtime.pendingTurns,
@@ -944,6 +951,38 @@ function asToolReturnStatus(value: unknown): "success" | "error" | null {
return null; return null;
} }
function buildPendingInterruptState(
runtime: ListenerRuntime,
): StateResponseMessage["pending_interrupt"] {
const context = runtime.pendingInterruptedContext;
const approvals = runtime.pendingInterruptedResults;
const interruptedToolCallIds = runtime.pendingInterruptedToolCallIds;
if (
!context ||
!approvals ||
approvals.length === 0 ||
!interruptedToolCallIds ||
interruptedToolCallIds.length === 0
) {
return null;
}
const interruptedSet = new Set(interruptedToolCallIds);
const toolReturns = extractInterruptToolReturns(approvals).filter(
(toolReturn) => interruptedSet.has(toolReturn.tool_call_id),
);
if (toolReturns.length === 0) {
return null;
}
return {
agent_id: context.agentId,
conversation_id: context.conversationId,
interrupted_tool_call_ids: [...interruptedToolCallIds],
tool_returns: toolReturns,
};
}
function normalizeToolReturnValue(value: unknown): string { function normalizeToolReturnValue(value: unknown): string {
if (typeof value === "string") { if (typeof value === "string") {
return value; return value;
@@ -3381,6 +3420,7 @@ export function stopListenerClient(): void {
export const __listenClientTestUtils = { export const __listenClientTestUtils = {
createRuntime, createRuntime,
stopRuntime, stopRuntime,
buildStateResponse,
emitToWS, emitToWS,
rememberPendingApprovalBatchIds, rememberPendingApprovalBatchIds,
resolvePendingApprovalBatchId, resolvePendingApprovalBatchId,