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" 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 {