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", () => {
|
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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user