Skip to content

Commit c513fa2

Browse files
committed
Include interrupted stop reason on in-flight wait results
interrupt_agent flipped wait to interrupted while the run was still in flight, so wait JSON omitted stop_reason. Skywalker classifies interrupted-incomplete only on that field.
1 parent f5862ed commit c513fa2

5 files changed

Lines changed: 33 additions & 8 deletions

File tree

‎docs/TELEMETRY.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ a truthy value (`1`, `true`, …) to restore per-call spans for debugging.
111111
worker ending during an active parent turn carries that turn's `parent_trace_id`.
112112
Pre-progress operator aborts settle with `status=cancelled` and
113113
`stop_reason=cancelled` even when the worker promise rejects. An interrupt that
114-
keeps a worker resumable settles with `status=interrupted` and the same
115-
`stop_reason=cancelled`; terminal events never report a still-running status.
114+
keeps a worker resumable settles with `status=interrupted` and
115+
`stop_reason=interrupted`; terminal events never report a still-running status.
116116

117117
A deterministic synthetic fixture captures 10 parent generations, 80 parent
118118
tool spans, and 4 worker start/end pairs. The comparable former shape is 98

‎src/subagent/agent-fleet.test.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -999,10 +999,15 @@ describe("interrupt_agent unblocks wait_agents", () => {
999999

10001000
const waited = await waiting;
10011001
expect(waited.timed_out).toBe(false);
1002-
const results = waited.results as { agent_id: string; status: string }[];
1003-
expect(results).toEqual([{ agent_id: id, status: "interrupted" }]);
1002+
const results = waited.results as {
1003+
agent_id: string;
1004+
status: string;
1005+
stop_reason?: string;
1006+
}[];
1007+
expect(results).toEqual([{ agent_id: id, status: "interrupted", stop_reason: "interrupted" }]);
10041008
expect(deps.sessions.get(id)?.lifecycleStatus).toBe("interrupted");
10051009
expect(deps.sessions.get(id)?.status).toBe("running");
1010+
expect(deps.sessions.get(id)?.stopReason).toBe("interrupted");
10061011
});
10071012

10081013
test("an interrupted run result terminalizes a still-running fleet record", async () => {
@@ -1135,8 +1140,12 @@ describe("interrupt_agent unblocks wait_agents", () => {
11351140

11361141
const waited = await callTool(wait, { targets: [id], timeout_ms: 5000 });
11371142
expect(waited.timed_out).toBe(false);
1138-
const results = waited.results as { agent_id: string; status: string }[];
1139-
expect(results).toEqual([{ agent_id: id, status: "interrupted" }]);
1143+
const results = waited.results as {
1144+
agent_id: string;
1145+
status: string;
1146+
stop_reason?: string;
1147+
}[];
1148+
expect(results).toEqual([{ agent_id: id, status: "interrupted", stop_reason: "interrupted" }]);
11401149
expect(deps.fleetRecords.peek(id)?.status).toBe("interrupted");
11411150
expect(deps.fleetRecords.peek(id)?.collected).toBe(true);
11421151

‎src/subagent/agent-fleet.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ class FleetMailbox {
337337
overlay.tombstoned !== true && session !== undefined && sessionWait === status
338338
? session
339339
: undefined;
340+
const stopReason =
341+
overlay.tombstoned !== true && !isLiveWaitStatus(status)
342+
? (payload?.stopReason ?? session?.stopReason)
343+
: payload?.stopReason;
340344
const ask = status === "awaiting_director" ? this.sessions.peekAsk(id) : undefined;
341345
return {
342346
status,
@@ -345,7 +349,7 @@ class FleetMailbox {
345349
...(overlay.hint !== undefined ? { hint: overlay.hint } : {}),
346350
...(payload?.report !== undefined ? { report: payload.report } : {}),
347351
...(payload?.error !== undefined && status === "failed" ? { error: payload.error } : {}),
348-
...(payload?.stopReason !== undefined ? { stopReason: payload.stopReason } : {}),
352+
...(stopReason !== undefined ? { stopReason } : {}),
349353
...(overlay.providerFailure === true ? { providerFailure: true } : {}),
350354
...(ask !== undefined ? { question: ask.question, questionId: ask.questionId } : {}),
351355
...(status === "awaiting_director" && session !== undefined
@@ -466,7 +470,8 @@ export const waitAgentsToolDefinition: ToolDefinition = {
466470
`clamped to a ${MAX_WAIT_TIMEOUT_MS}ms max. A timeout or parent-turn abort is NOT an error and never touches ` +
467471
`the workers — they keep running and remain waitable. Live wait status includes "queued" (waiting for a burst ` +
468472
`slot), "running", and "awaiting_director". interrupt_agent and close_agent unblock this wait immediately with ` +
469-
`status "interrupted". awaiting_director is not terminal: re-wait while still pending re-delivers the same question. ` +
473+
`status "interrupted". Terminal JSON includes stop_reason when the session recorded one ` +
474+
`(interrupted, cancelled, incomplete-report, and similar). awaiting_director is not terminal: re-wait while still pending re-delivers the same question. ` +
470475
`Answer with send_input (soft). Do not call this in a tight zero-progress loop: a timeout means the targets are still ` +
471476
`queued, running, or awaiting a director answer, not "try again right away" — do other work, reply to the operator, or change the brief. Calling again with the ` +
472477
`same targets is a real timed wait, not a spin, but wastes turns if nothing has changed.`,

‎src/subagent/session-store.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ describe("terminal stop reasons", () => {
299299
store.cancel(bare.id);
300300
expect(store.get(bare.id)?.stopReason).toBe("cancelled");
301301
});
302+
303+
test("interruptOne records stopReason interrupted", () => {
304+
const store = createSubAgentSessionStore();
305+
const session = store.start({ description: "d", agentId: "a", brief: "b" });
306+
store.markRunning(session.id);
307+
store.registerInterrupt(session.id, () => {});
308+
expect(store.interruptOne(session.id).ok).toBe(true);
309+
expect(store.get(session.id)?.stopReason).toBe("interrupted");
310+
});
302311
});
303312

304313
describe("CL-6943 reusable worker sessions", () => {

‎src/subagent/session-store.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,7 @@ export function createSubAgentSessionStore(
13821382
...(s.report !== undefined ? { report: s.report } : {}),
13831383
};
13841384
s.finishedAt = s.finishedAt ?? now();
1385+
s.stopReason = "interrupted";
13851386
});
13861387
pruneRetained();
13871388
return { ok: true };
@@ -1396,6 +1397,7 @@ export function createSubAgentSessionStore(
13961397
...(s.report !== undefined ? { report: s.report } : {}),
13971398
};
13981399
s.finishedAt = s.finishedAt ?? now();
1400+
s.stopReason = "interrupted";
13991401
});
14001402
pruneRetained();
14011403
return { ok: true };

0 commit comments

Comments
 (0)