From 1427b858416ca6b10343f7a2221f47f9edd50b65 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Sun, 18 Jan 2026 15:52:58 +0000 Subject: [PATCH 1/2] fix(core): process tool calls when checkpoint status is completed Previously, getActivities() would immediately return only CompleteActivity when status === "completed", ignoring any tool calls in the step. This fix ensures tool calls are processed for completed status, then appends CompleteActivity at the end. Closes #420 Co-Authored-By: Claude Opus 4.5 --- packages/core/src/utils/activity.test.ts | 85 ++++++++++++++++++++++++ packages/core/src/utils/activity.ts | 21 ++++-- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/core/src/utils/activity.test.ts b/packages/core/src/utils/activity.test.ts index 433be556..8aa3d55f 100644 --- a/packages/core/src/utils/activity.test.ts +++ b/packages/core/src/utils/activity.test.ts @@ -339,6 +339,91 @@ describe("getActivities", () => { expect(activities[0].text).toBe("All done!") } }) + + it("returns both tool activities and complete activity when completed with tool calls", () => { + const checkpoint = createBaseCheckpoint({ + status: "completed", + }) + const step = createBaseStep({ + newMessages: [ + { + id: "m-1", + type: "expertMessage", + contents: [ + { type: "thinkingPart", id: "tp-1", thinking: "Reading file before completing" }, + { type: "textPart", id: "tp-2", text: "Task completed!" }, + ], + }, + ], + toolCalls: [createToolCall({ toolName: "readTextFile", args: { path: "/test.txt" } })], + toolResults: [ + createToolResult({ + toolName: "readTextFile", + result: [{ type: "textPart", id: "tp-1", text: '{"content": "file content"}' }], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(2) + expect(activities[0].type).toBe("readTextFile") + expect(activities[0].reasoning).toBe("Reading file before completing") + expect(activities[1].type).toBe("complete") + expect(activities[1].reasoning).toBeUndefined() // CompleteActivity appended without reasoning + if (activities[1].type === "complete") { + expect(activities[1].text).toBe("Task completed!") + } + }) + + it("returns ParallelActivitiesGroup plus complete activity for completed with parallel tool calls", () => { + const checkpoint = createBaseCheckpoint({ + status: "completed", + }) + const step = createBaseStep({ + newMessages: [ + { + id: "m-1", + type: "expertMessage", + contents: [ + { type: "thinkingPart", id: "tp-1", thinking: "Reading multiple files" }, + { type: "textPart", id: "tp-2", text: "All files read, done!" }, + ], + }, + ], + toolCalls: [ + createToolCall({ id: "tc-1", toolName: "readTextFile", args: { path: "/file1.txt" } }), + createToolCall({ id: "tc-2", toolName: "readTextFile", args: { path: "/file2.txt" } }), + ], + toolResults: [ + createToolResult({ + id: "tc-1", + toolName: "readTextFile", + result: [{ type: "textPart", id: "tp-1", text: '{"content": "content 1"}' }], + }), + createToolResult({ + id: "tc-2", + toolName: "readTextFile", + result: [{ type: "textPart", id: "tp-2", text: '{"content": "content 2"}' }], + }), + ], + }) + + const result = getActivities({ checkpoint, step }) + + expect(result).toHaveLength(2) + expect(result[0].type).toBe("parallelGroup") + const group = result[0] as ParallelActivitiesGroup + expect(group.reasoning).toBe("Reading multiple files") + expect(group.activities).toHaveLength(2) + expect(group.activities[0].type).toBe("readTextFile") + expect(group.activities[1].type).toBe("readTextFile") + // Complete activity should be appended + expect(result[1].type).toBe("complete") + if (result[1].type === "complete") { + expect(result[1].text).toBe("All files read, done!") + } + }) }) describe("parallel tool calls", () => { diff --git a/packages/core/src/utils/activity.ts b/packages/core/src/utils/activity.ts index 7e029787..0ad8be65 100644 --- a/packages/core/src/utils/activity.ts +++ b/packages/core/src/utils/activity.ts @@ -76,11 +76,6 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] { const expertKey = checkpoint.expert.key const reasoning = extractReasoning(step.newMessages) - // Completed run - final result generation (after attemptCompletion) - if (status === "completed") { - return [createCompleteActivity(step.newMessages, reasoning)] - } - // Error status - use checkpoint error information if (status === "stoppedByError") { return [createErrorActivity(checkpoint, reasoning)] @@ -117,7 +112,11 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] { const toolCalls = step.toolCalls ?? [] const toolResults = step.toolResults ?? [] + // For completed status with no tool calls, return CompleteActivity only if (toolCalls.length === 0) { + if (status === "completed") { + return [createCompleteActivity(step.newMessages, reasoning)] + } return [createRetryActivity(step.newMessages, reasoning)] } @@ -139,10 +138,20 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] { } if (activities.length === 0) { + if (status === "completed") { + return [createCompleteActivity(step.newMessages, reasoning)] + } return [createRetryActivity(step.newMessages, reasoning)] } - return wrapInGroupIfParallel(activities, reasoning, expertKey, runId, stepNumber) + const result = wrapInGroupIfParallel(activities, reasoning, expertKey, runId, stepNumber) + + // Append CompleteActivity for completed status + if (status === "completed") { + result.push(createCompleteActivity(step.newMessages, undefined)) + } + + return result } function createCompleteActivity(newMessages: Message[], reasoning: string | undefined): Activity { From 0d743c616a5747befd37e03ebf709c0300f63150 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Sun, 18 Jan 2026 15:53:26 +0000 Subject: [PATCH 2/2] chore: add changeset for getActivities fix Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-get-activities-completed-status.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-get-activities-completed-status.md diff --git a/.changeset/fix-get-activities-completed-status.md b/.changeset/fix-get-activities-completed-status.md new file mode 100644 index 00000000..56557150 --- /dev/null +++ b/.changeset/fix-get-activities-completed-status.md @@ -0,0 +1,5 @@ +--- +"@perstack/core": patch +--- + +Fixed getActivities() to process tool calls when checkpoint status is "completed"