Skip to content

Commit ae5e3b3

Browse files
committed
fix: surface long-running goose subagent status
1 parent 4ef0ad0 commit ae5e3b3

File tree

4 files changed

+214
-8
lines changed

4 files changed

+214
-8
lines changed

packages/agents/goose/session-state.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,40 @@ export type GooseInspectorToolState = StreamToolState;
5959

6060
export type GooseStreamStateMaps = StreamStateMaps<GooseInspectorToolState>;
6161

62+
function resolveGooseToolResponseId(block: {
63+
id?: string;
64+
tool_use_id?: string;
65+
}): string {
66+
if (typeof block.id === "string" && block.id.trim()) {
67+
return block.id;
68+
}
69+
if (typeof block.tool_use_id === "string" && block.tool_use_id.trim()) {
70+
return block.tool_use_id;
71+
}
72+
return "";
73+
}
74+
75+
function extractGooseToolResponseText(value: unknown): string {
76+
if (typeof value === "string") {
77+
return value.trim();
78+
}
79+
if (!Array.isArray(value)) {
80+
return "";
81+
}
82+
return value
83+
.filter((entry) => entry && typeof entry === "object")
84+
.map((entry) => {
85+
const record = entry as { type?: string; text?: string };
86+
if (record.type === "text" && typeof record.text === "string") {
87+
return record.text;
88+
}
89+
return "";
90+
})
91+
.filter((text) => text.length > 0)
92+
.join("\n")
93+
.trim();
94+
}
95+
6296
function parseTodosFromGooseToolInput(toolName: string, input: Record<string, unknown> | undefined): SessionTodo[] | undefined {
6397
const direct = parseTodosFromToolInput(toolName, input);
6498
if (direct) return direct;
@@ -97,6 +131,9 @@ export function applyGooseRecordToState(
97131
const blocks = record.message?.content ?? [];
98132

99133
if (role === "assistant") {
134+
const messageCreatedAtMs = typeof record.message?.created === "number"
135+
? record.message.created * 1000
136+
: Date.now();
100137
for (const block of blocks) {
101138
if (block?.type === "text") {
102139
const chunk = typeof block.text === "string" ? block.text : "";
@@ -138,7 +175,12 @@ export function applyGooseRecordToState(
138175
output: existing?.output,
139176
error: existing?.error,
140177
title: existing?.title,
141-
metadata: existing?.metadata,
178+
metadata: {
179+
...(existing?.metadata ?? {}),
180+
startedAtMs: typeof existing?.metadata?.startedAtMs === "number"
181+
? existing.metadata.startedAtMs
182+
: messageCreatedAtMs,
183+
},
142184
};
143185
toolById.set(callId, tool);
144186
updateTool(state, tool);
@@ -150,16 +192,12 @@ export function applyGooseRecordToState(
150192
if (role === "user") {
151193
for (const block of blocks) {
152194
if (block?.type !== "toolResponse") continue;
153-
const callId = typeof block.id === "string" && block.id.trim() ? block.id : "";
195+
const callId = resolveGooseToolResponseId(block);
154196
if (!callId) continue;
155197
const existing = toolById.get(callId);
156198
if (!existing) continue;
157199
const result = block.toolResult?.value;
158-
const output = (result?.content ?? [])
159-
.filter((entry) => entry?.type === "text")
160-
.map((entry) => entry.text ?? "")
161-
.join("\n")
162-
.trim();
200+
const output = extractGooseToolResponseText(result?.content);
163201
const hasError = result?.isError === true || block.toolResult?.status === "error";
164202
const updated: GooseInspectorToolState = {
165203
...existing,

packages/live-status-harness/test/render-status.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,94 @@ describe("live status harness renderer", () => {
6969
expect(joined).toContain("Finished tool: Read");
7070
});
7171

72+
it("renders goose subagent completion when tool response uses tool_use_id", () => {
73+
const now = Date.now();
74+
const meta: HarnessRunMeta = {
75+
runId: "run-goose-subagent-id",
76+
provider: "goose",
77+
prompt: "test",
78+
promptHash: "hash",
79+
cwd: "/tmp/repo",
80+
channelId: "C1",
81+
threadId: "T1",
82+
sessionId: "goose_s1",
83+
startedAt: now,
84+
eventCount: 2,
85+
};
86+
87+
const events: HarnessCapturedEvent[] = [
88+
{
89+
runId: "run-goose-subagent-id",
90+
sessionId: "goose_s1",
91+
provider: "goose",
92+
timestamp: now,
93+
index: 0,
94+
event: {
95+
type: "goose.raw.message",
96+
properties: {
97+
record: {
98+
type: "message",
99+
message: {
100+
role: "assistant",
101+
content: [
102+
{
103+
type: "toolRequest",
104+
id: "call-subagent-1",
105+
toolCall: {
106+
value: {
107+
name: "subagent",
108+
arguments: { instructions: "inspect repo" },
109+
},
110+
},
111+
},
112+
],
113+
},
114+
},
115+
},
116+
},
117+
},
118+
{
119+
runId: "run-goose-subagent-id",
120+
sessionId: "goose_s1",
121+
provider: "goose",
122+
timestamp: now + 1,
123+
index: 1,
124+
event: {
125+
type: "goose.raw.message",
126+
properties: {
127+
record: {
128+
type: "message",
129+
message: {
130+
role: "user",
131+
content: [
132+
{
133+
type: "toolResponse",
134+
tool_use_id: "call-subagent-1",
135+
toolResult: {
136+
status: "success",
137+
value: {
138+
content: [
139+
{ type: "text", text: "subagent complete" },
140+
],
141+
isError: false,
142+
},
143+
},
144+
},
145+
],
146+
},
147+
},
148+
},
149+
},
150+
},
151+
];
152+
153+
const statuses = renderStatusesFromRun(meta, events);
154+
const joined = statuses.map((status) => status.text).join("\n\n");
155+
156+
expect(joined).toContain("Running tool: subagent");
157+
expect(joined).toContain("Finished tool: subagent");
158+
});
159+
72160
it("renders gemini live status from synthetic fixture", () => {
73161
const now = Date.now();
74162
const meta: HarnessRunMeta = {

packages/utils/status.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { buildStatusMessageByProvider, type StatusRequest } from "./status";
3+
import type { SessionMessageState } from "./session-inspector";
4+
5+
describe("status message formatting", () => {
6+
const request: StatusRequest = {
7+
channelId: "C1",
8+
threadId: "T1",
9+
statusMessageTs: "S1",
10+
startedAt: Date.now() - 60_000,
11+
currentText: "",
12+
};
13+
14+
it("shows waiting subagent hint after threshold", () => {
15+
const state: SessionMessageState = {
16+
sessionTitle: "Goose is running...",
17+
phaseStatus: "Running tool: subagent",
18+
currentText: "",
19+
tools: [
20+
{
21+
id: "tool-1",
22+
name: "subagent",
23+
status: "running",
24+
metadata: { startedAtMs: Date.now() - 35_000 },
25+
},
26+
],
27+
todos: [],
28+
startedAt: Date.now() - 60_000,
29+
};
30+
31+
const text = buildStatusMessageByProvider("goose", request, "/tmp/repo", state, "medium");
32+
expect(text).toContain("Waiting for subagent output");
33+
});
34+
35+
it("keeps regular running status before threshold", () => {
36+
const state: SessionMessageState = {
37+
sessionTitle: "Goose is running...",
38+
phaseStatus: "Running tool: subagent",
39+
currentText: "",
40+
tools: [
41+
{
42+
id: "tool-1",
43+
name: "subagent",
44+
status: "running",
45+
metadata: { startedAtMs: Date.now() - 5_000 },
46+
},
47+
],
48+
todos: [],
49+
startedAt: Date.now() - 60_000,
50+
};
51+
52+
const text = buildStatusMessageByProvider("goose", request, "/tmp/repo", state, "medium");
53+
expect(text).toContain("_Running tool: subagent_");
54+
expect(text).not.toContain("Waiting for subagent output");
55+
});
56+
});

packages/utils/status.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type StatusTodo = {
3434
};
3535

3636
const PLAN_TODO_LIMIT = 15;
37+
const SUBAGENT_WAIT_THRESHOLD_MS = 30_000;
3738

3839
export function formatElapsedTime(startedAt: number): string {
3940
const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1000);
@@ -169,6 +170,26 @@ function formatTodoLines(todos: StatusTodo[], limit = PLAN_TODO_LIMIT): string[]
169170
return lines;
170171
}
171172

173+
function resolveLongRunningSubagentPhase(state: SessionMessageState): string | undefined {
174+
const runningSubagent = [...state.tools]
175+
.reverse()
176+
.find((tool) => {
177+
const name = typeof tool.name === "string" ? tool.name.trim().toLowerCase() : "";
178+
return (tool.status === "running" || tool.status === "pending") && name === "subagent";
179+
});
180+
181+
if (!runningSubagent) return undefined;
182+
const startedAtMs = typeof runningSubagent.metadata?.startedAtMs === "number"
183+
? runningSubagent.metadata.startedAtMs
184+
: undefined;
185+
if (!startedAtMs) return undefined;
186+
187+
const elapsedMs = Date.now() - startedAtMs;
188+
if (elapsedMs < SUBAGENT_WAIT_THRESHOLD_MS) return undefined;
189+
190+
return `Waiting for subagent output (${formatElapsedTime(startedAtMs)})`;
191+
}
192+
172193
function normalizeToolName(name: string): string {
173194
switch (name) {
174195
case "read_file":
@@ -313,7 +334,10 @@ export function buildLiveStatusMessage(
313334
lines.push(`(${headerDetails})`);
314335
}
315336

316-
if (state.phaseStatus) {
337+
const longRunningSubagentPhase = resolveLongRunningSubagentPhase(state);
338+
if (longRunningSubagentPhase) {
339+
lines.push(`_${longRunningSubagentPhase}_`);
340+
} else if (state.phaseStatus) {
317341
lines.push(`_${state.phaseStatus}_`);
318342
}
319343

0 commit comments

Comments
 (0)