From 218c78558305bcd43ce064771d00e3237d943e02 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 24 Sep 2026 01:58:50 -0600 Subject: [PATCH] feat(prime): show child progress notes in Agents view --- .../prime/PrimeAgentDaemonAdapter.test.ts | 42 ++++++++++++++++++- .../provider/prime/PrimeAgentDaemonAdapter.ts | 3 ++ .../prime/PrimeAgentDaemonEvents.test.ts | 20 +++++++++ .../provider/prime/PrimeAgentDaemonEvents.ts | 9 ++++ .../PrimeAgentDaemonRuntimeEvents.test.ts | 8 +++- .../prime/PrimeAgentDaemonRuntimeEvents.ts | 8 +++- .../src/state/subagentRuntime.test.ts | 23 ++++++++++ 7 files changed, 108 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts index ca3c2ae7d6..15db0b597b 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts @@ -6661,6 +6661,39 @@ describe("PrimeAgentDaemonAdapter", () => { }, }); } + yield* offer(captures, { + _tag: "ChildUpdated", + child: { + id: "child-private-progress", + parentId: "parent", + activeSessionId: "native-private-100", + model: "child-model", + label: "private-safe-label", + status: "running", + tokenCount: 10, + toolUseCount: 1, + activity: { kind: "executing", toolName: "bash" }, + progressNote: "Checking the failing test", + lastActivityAt: 1_000, + }, + }); + yield* awaitObservedType(subscription.observed, "task.progress"); + yield* offer(captures, { + _tag: "ChildUpdated", + child: { + id: "child-private-progress", + parentId: "parent", + activeSessionId: "native-private-100", + model: "child-model", + label: "private-safe-label", + status: "running", + tokenCount: 10, + toolUseCount: 1, + activity: { kind: "executing", toolName: "bash" }, + progressNote: "Checking the failing test", + lastActivityAt: 2_000, + }, + }); captures.agentRoster = [ { id: "child-private-progress", @@ -6676,6 +6709,7 @@ describe("PrimeAgentDaemonAdapter", () => { activity: { kind: "executing", toolName: "bash" }, answerPreview: "private-answer-roster", recap: "private-recap-roster", + progressNote: "Checking the failing test", }, ]; expect( @@ -6718,17 +6752,21 @@ describe("PrimeAgentDaemonAdapter", () => { yield* awaitObservedType(subscription.observed, "session.goal.updated"); const durableEvents = subscription.events.slice(durableStart); - expect(durableEvents.filter((event) => event.type === "task.progress")).toEqual([]); + expect(durableEvents.filter((event) => event.type === "task.progress")).toEqual([ + expect.objectContaining({ + payload: expect.objectContaining({ summary: "Checking the failing test" }), + }), + ]); const completed = durableEvents.filter((event) => event.type === "task.completed"); expect(completed).toHaveLength(1); expect(completed[0]).toMatchObject({ payload: { taskId: "child-private-progress", status: "failed", + summary: "Failed", typedUsage: { totalTokens: 12, toolUses: 1, durationMs: 1_500 }, }, }); - expect(completed[0]?.payload).not.toHaveProperty("summary"); const persisted = encodeUnknownJson(durableEvents); expect(persisted).not.toContain("private-answer"); expect(persisted).not.toContain("private-recap"); diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts index 18a0fb462b..3ecc76033b 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts @@ -531,6 +531,7 @@ type PrimeAgentDaemonChildDurableProjection = { readonly messageable: boolean; readonly waiting: boolean; readonly lastToolName: string | undefined; + readonly progressNote: string | undefined; readonly tokenCount: number | undefined; readonly toolUseCount: number | undefined; readonly terminalDurationMs: number | undefined; @@ -560,6 +561,7 @@ function durableChildProjection( child.status === "running" && child.activity?.kind !== "waiting" ? durableChildText(child.activity?.toolName) : undefined, + progressNote: active ? durableChildText(child.progressNote) : undefined, tokenCount: durableChildCount(child.tokenCount), toolUseCount: durableChildCount(child.toolUseCount), terminalDurationMs: active ? undefined : durableChildCount(child.durationMs), @@ -581,6 +583,7 @@ function durableChildChanged( left.messageable !== right.messageable || left.waiting !== right.waiting || left.lastToolName !== right.lastToolName || + left.progressNote !== right.progressNote || left.tokenCount !== right.tokenCount || left.toolUseCount !== right.toolUseCount || left.terminalDurationMs !== right.terminalDurationMs diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts index 1a7e865f87..7829ea5431 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts @@ -819,6 +819,26 @@ describe("PrimeAgentDaemonEvents", () => { _tag: "ChildUpdated", child: { id: "child-1", status: "running", activity: { toolName: "ipython" } }, }); + expect( + decodePrimeAgentDaemonEvent( + sessionEvent({ + type: "rlm_child_update", + child: { + ...child, + progressNote: "Checking tests", + lastActivityAt: 1_700_000_000_000, + activityStaleMs: 600_000, + }, + }), + ), + ).toMatchObject({ + _tag: "ChildUpdated", + child: { + progressNote: "Checking tests", + lastActivityAt: 1_700_000_000_000, + activityStaleMs: 600_000, + }, + }); const queueChanged = decodePrimeAgentDaemonEvent( sessionEvent({ type: "session_action_update", actions }), ); diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts index de46966fe7..335895f01d 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts @@ -387,6 +387,9 @@ const rlmChild = Schema.Struct({ toolUseCount: Schema.optional(Schema.Number), tokenCount: Schema.optional(Schema.Number), recap: Schema.optional(Schema.String), + progressNote: Schema.optional(Schema.String), + lastActivityAt: Schema.optional(Schema.Finite), + activityStaleMs: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), sessionDir: Schema.String, activity: Schema.optional( Schema.Struct({ @@ -1309,6 +1312,9 @@ export type PrimeDaemonEvent = ( readonly toolUseCount?: number | undefined; readonly tokenCount?: number | undefined; readonly recap?: string | undefined; + readonly progressNote?: string | undefined; + readonly lastActivityAt?: number | undefined; + readonly activityStaleMs?: number | undefined; readonly activity?: | { readonly kind: "waiting" | "writing" | "executing"; @@ -1656,6 +1662,9 @@ function mapChild( toolUseCount: value.toolUseCount, tokenCount: value.tokenCount, recap: optionalBounded(value.recap, MAX_PREVIEW_LENGTH), + progressNote: optionalBounded(value.progressNote, 512), + lastActivityAt: value.lastActivityAt, + activityStaleMs: value.activityStaleMs, activity: value.activity, error: optionalBounded(value.error, MAX_PREVIEW_LENGTH), }; diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts index a4f078db40..4b42987701 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.test.ts @@ -660,6 +660,9 @@ describe("mapPrimeAgentDaemonRuntimeEventDrafts", () => { tokenCount: 90, answerPreview: "private live answer", recap: "private recap", + progressNote: "Checking the failing test", + lastActivityAt: 1_700_000_000_000, + activityStaleMs: 600_000, error: "private transient error", activity: { kind: "executing", toolName: "bash" }, }, @@ -676,6 +679,7 @@ describe("mapPrimeAgentDaemonRuntimeEventDrafts", () => { taskId: RuntimeTaskId.make("child-1"), description: "Review tests", status: "running", + summary: "Checking the failing test", typedUsage: { totalTokens: 90, toolUses: 4, durationMs: 250 }, lastToolName: "bash", taskType: "subagent", @@ -693,6 +697,8 @@ describe("mapPrimeAgentDaemonRuntimeEventDrafts", () => { expect(JSON.stringify(running)).not.toContain("private live answer"); expect(JSON.stringify(running)).not.toContain("private recap"); expect(JSON.stringify(running)).not.toContain("private transient error"); + expect(JSON.stringify(running)).not.toContain("activityStaleMs"); + expect(JSON.stringify(running)).not.toContain("lastActivityAt"); const terminal = mapPrimeAgentDaemonRuntimeEventDrafts({ ...context, @@ -716,12 +722,12 @@ describe("mapPrimeAgentDaemonRuntimeEventDrafts", () => { payload: { taskId: RuntimeTaskId.make("child-1"), status: "failed", + summary: "Failed", typedUsage: { totalTokens: 100 }, messageable: false, timelineBypass: true, }, }); - expect(terminal[0]?.payload).not.toHaveProperty("summary"); expect(JSON.stringify(terminal)).not.toContain("private terminal"); }); diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.ts b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.ts index b2912965b9..566f6f90c3 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonRuntimeEvents.ts @@ -307,7 +307,8 @@ function childDraft( }; case "running": { const lastToolName = boundedNonEmpty(child.activity?.toolName, MAX_SCALAR_LENGTH); - if (child.activity?.kind === "waiting") { + const progressNote = boundedNonEmpty(child.progressNote?.trim(), 512); + if (child.activity?.kind === "waiting" && progressNote === undefined) { return { ...runtimeBase(input), type: "task.updated", @@ -320,7 +321,8 @@ function childDraft( payload: { taskId, description, - status: "running", + status: child.activity?.kind === "waiting" ? "waiting" : "running", + ...(progressNote === undefined ? {} : { summary: progressNote }), ...(typedUsage === undefined ? {} : { typedUsage }), ...(lastToolName === undefined ? {} : { lastToolName }), ...linkage, @@ -337,6 +339,8 @@ function childDraft( taskId, status: child.status === "done" ? "completed" : child.status === "error" ? "failed" : "stopped", + summary: + child.status === "done" ? "Completed" : child.status === "error" ? "Failed" : "Stopped", ...(typedUsage === undefined ? {} : { typedUsage }), ...linkage, }, diff --git a/packages/client-runtime/src/state/subagentRuntime.test.ts b/packages/client-runtime/src/state/subagentRuntime.test.ts index ff066e045f..e1aa979c2e 100644 --- a/packages/client-runtime/src/state/subagentRuntime.test.ts +++ b/packages/client-runtime/src/state/subagentRuntime.test.ts @@ -71,6 +71,29 @@ function fold(rows: ReadonlyArray) { } describe("foldSubagentActivities", () => { + it("shows a Prime child note while active and its outcome after completion", () => { + const [agent] = fold([ + activity("task.progress", { + taskId: "prime-child-1", + taskType: "subagent", + title: "Review tests", + status: "running", + summary: "Checking the failing test", + }), + activity("task.completed", { + taskId: "prime-child-1", + taskType: "subagent", + status: "completed", + summary: "Completed", + }), + ]); + expect(agent).toMatchObject({ + status: "completed", + progress: "Checking the failing test", + result: "Completed", + }); + }); + it("shows the batch status limit after its parent turn ends without claiming a result", () => { const running = activity("task.progress", { taskId: "batch-1",