Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-get-activities-completed-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@perstack/core": patch
---

Fixed getActivities() to process tool calls when checkpoint status is "completed"
85 changes: 85 additions & 0 deletions packages/core/src/utils/activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/utils/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
}

Expand All @@ -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 {
Expand Down