fix(listen): expose interrupted tool state for reconnect hydration (#1295)
This commit is contained in:
@@ -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", () => {
|
||||
test("control_response with allow + updatedInput rewrites tool args", async () => {
|
||||
const runtime = __listenClientTestUtils.createRuntime();
|
||||
|
||||
@@ -212,6 +212,12 @@ interface StateResponseMessage {
|
||||
request_id: string;
|
||||
request: ControlRequest["request"];
|
||||
}>;
|
||||
pending_interrupt: {
|
||||
agent_id: string;
|
||||
conversation_id: string;
|
||||
interrupted_tool_call_ids: string[];
|
||||
tool_returns: InterruptToolReturn[];
|
||||
} | null;
|
||||
queue: {
|
||||
queue_len: number;
|
||||
pending_turns: number;
|
||||
@@ -747,6 +753,7 @@ function buildStateResponse(
|
||||
started_at: runtime.activeRunStartedAt,
|
||||
},
|
||||
pending_control_requests: pendingControlRequests,
|
||||
pending_interrupt: buildPendingInterruptState(runtime),
|
||||
queue: {
|
||||
queue_len: runtime.queueRuntime.length,
|
||||
pending_turns: runtime.pendingTurns,
|
||||
@@ -944,6 +951,38 @@ function asToolReturnStatus(value: unknown): "success" | "error" | 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 {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
@@ -3381,6 +3420,7 @@ export function stopListenerClient(): void {
|
||||
export const __listenClientTestUtils = {
|
||||
createRuntime,
|
||||
stopRuntime,
|
||||
buildStateResponse,
|
||||
emitToWS,
|
||||
rememberPendingApprovalBatchIds,
|
||||
resolvePendingApprovalBatchId,
|
||||
|
||||
Reference in New Issue
Block a user