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
8 changes: 8 additions & 0 deletions .changeset/activity-log-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@perstack/core": patch
"@perstack/react": patch
"@perstack/tui-components": patch
"perstack": patch
---

Add timestamp and delegation info to activity log header
3 changes: 3 additions & 0 deletions packages/core/src/schemas/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface BaseActivity {
}
/** LLM's reasoning/thinking process before executing this action */
reasoning?: string
/** Unix timestamp (ms) from the originating event */
timestamp: number
}

const baseActivitySchema = z.object({
Expand All @@ -33,6 +35,7 @@ const baseActivitySchema = z.object({
})
.optional(),
reasoning: z.string().optional(),
timestamp: z.number(),
})

/** Query activity - User input that starts a run */
Expand Down
51 changes: 37 additions & 14 deletions packages/core/src/utils/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
const { status, delegateTo, runId, stepNumber } = checkpoint
const expertKey = checkpoint.expert.key
const reasoning = extractReasoning(step.newMessages)
const ts = step.startedAt

// Generate QueryActivity for first step (parity with processRunEventToActivity)
let queryActivity: ActivityOrGroup | undefined
Expand All @@ -88,6 +89,7 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
id: "",
expertKey,
runId,
timestamp: step.startedAt,
text: textPart.text,
}
}
Expand All @@ -99,7 +101,7 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {

// Error status - use checkpoint error information
if (status === "stoppedByError") {
return prependQuery([createErrorActivity(checkpoint, reasoning)])
return prependQuery([createErrorActivity(checkpoint, reasoning, ts)])
}

// Parallel delegate activities - each delegation becomes a separate activity
Expand All @@ -109,11 +111,12 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
createRetryActivity(
step.newMessages,
reasoning,
ts,
"Delegate status but no delegation targets",
),
])
}
const delegateActivities = delegateTo.map((d) => createDelegateActivity(d, reasoning))
const delegateActivities = delegateTo.map((d) => createDelegateActivity(d, reasoning, ts))
return prependQuery(
wrapInGroupIfParallel(delegateActivities, reasoning, expertKey, runId, stepNumber),
)
Expand All @@ -123,10 +126,10 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
if (status === "stoppedByInteractiveTool") {
const toolCalls = step.toolCalls ?? []
if (toolCalls.length === 0) {
return prependQuery([createRetryActivity(step.newMessages, reasoning)])
return prependQuery([createRetryActivity(step.newMessages, reasoning, ts)])
}
const interactiveActivities = toolCalls.map((tc) =>
createInteractiveToolActivity(tc.skillName, tc.toolName, tc, reasoning),
createInteractiveToolActivity(tc.skillName, tc.toolName, tc, reasoning, ts),
)
return prependQuery(
wrapInGroupIfParallel(interactiveActivities, reasoning, expertKey, runId, stepNumber),
Expand All @@ -140,12 +143,12 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
// For completed status with no tool calls, return CompleteActivity only
if (toolCalls.length === 0) {
if (status === "completed") {
return prependQuery([createCompleteActivity(step.newMessages, reasoning)])
return prependQuery([createCompleteActivity(step.newMessages, reasoning, ts)])
}
if (status === "proceeding" || status === "init") {
return prependQuery([])
}
return prependQuery([createRetryActivity(step.newMessages, reasoning)])
return prependQuery([createRetryActivity(step.newMessages, reasoning, ts)])
}

const toolActivities: Activity[] = []
Expand All @@ -157,35 +160,39 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] {
}
const { skillName, toolName } = toolCall
if (skillName.startsWith(BASE_SKILL_PREFIX)) {
toolActivities.push(createBaseToolActivity(toolName, toolCall, toolResult, reasoning))
toolActivities.push(createBaseToolActivity(toolName, toolCall, toolResult, reasoning, ts))
} else {
toolActivities.push(
createGeneralToolActivity(skillName, toolName, toolCall, toolResult, reasoning),
createGeneralToolActivity(skillName, toolName, toolCall, toolResult, reasoning, ts),
)
}
}

if (toolActivities.length === 0) {
if (status === "completed") {
return prependQuery([createCompleteActivity(step.newMessages, reasoning)])
return prependQuery([createCompleteActivity(step.newMessages, reasoning, ts)])
}
if (status === "proceeding" || status === "init") {
return prependQuery([])
}
return prependQuery([createRetryActivity(step.newMessages, reasoning)])
return prependQuery([createRetryActivity(step.newMessages, reasoning, ts)])
}

const result = wrapInGroupIfParallel(toolActivities, reasoning, expertKey, runId, stepNumber)

// Append CompleteActivity for completed status
if (status === "completed") {
result.push(createCompleteActivity(step.newMessages, undefined))
result.push(createCompleteActivity(step.newMessages, undefined, ts))
}

return prependQuery(result)
}

function createCompleteActivity(newMessages: Message[], reasoning: string | undefined): Activity {
function createCompleteActivity(
newMessages: Message[],
reasoning: string | undefined,
timestamp: number,
): Activity {
// Extract final text from the last expertMessage's textPart
const lastExpertMessage = [...newMessages].reverse().find((m) => m.type === "expertMessage")
const textPart = lastExpertMessage?.contents.find((c) => c.type === "textPart")
Expand All @@ -194,6 +201,7 @@ function createCompleteActivity(newMessages: Message[], reasoning: string | unde
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
text: textPart?.text ?? "",
}
Expand All @@ -202,12 +210,14 @@ function createCompleteActivity(newMessages: Message[], reasoning: string | unde
function createDelegateActivity(
delegate: DelegationTarget,
reasoning: string | undefined,
timestamp: number,
): Activity {
return {
type: "delegate",
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
delegateExpertKey: delegate.expert.key,
query: delegate.query,
Expand All @@ -219,12 +229,14 @@ function createInteractiveToolActivity(
toolName: string,
toolCall: ToolCall,
reasoning: string | undefined,
timestamp: number,
): Activity {
return {
type: "interactiveTool",
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
skillName,
toolName,
Expand All @@ -235,6 +247,7 @@ function createInteractiveToolActivity(
function createRetryActivity(
newMessages: Message[],
reasoning: string | undefined,
timestamp: number,
customError?: string,
): Activity {
const lastMessage = newMessages[newMessages.length - 1]
Expand All @@ -244,19 +257,25 @@ function createRetryActivity(
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
error: customError ?? "No tool call or result found",
message: textPart?.text ?? "",
}
}

function createErrorActivity(checkpoint: Checkpoint, reasoning: string | undefined): Activity {
function createErrorActivity(
checkpoint: Checkpoint,
reasoning: string | undefined,
timestamp: number,
): Activity {
const error = checkpoint.error
return {
type: "error",
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
error: error?.message ?? "Unknown error",
errorName: error?.name,
Expand All @@ -269,11 +288,12 @@ export function createBaseToolActivity(
toolCall: ToolCall,
toolResult: ToolResult,
reasoning: string | undefined,
timestamp = 0,
): Activity {
const args = toolCall.args as Record<string, unknown>
const resultContents = toolResult.result
const errorText = getErrorFromResult(resultContents)
const baseFields = { id: "", expertKey: "", runId: "", reasoning }
const baseFields = { id: "", expertKey: "", runId: "", reasoning, timestamp }

switch (toolName) {
case "attemptCompletion": {
Expand Down Expand Up @@ -423,6 +443,7 @@ export function createBaseToolActivity(
toolCall,
toolResult,
reasoning,
timestamp,
)
}
}
Expand All @@ -433,13 +454,15 @@ export function createGeneralToolActivity(
toolCall: ToolCall,
toolResult: ToolResult,
reasoning: string | undefined,
timestamp = 0,
): Activity {
const errorText = getErrorFromResult(toolResult.result)
return {
type: "generalTool",
id: "",
expertKey: "",
runId: "",
timestamp,
reasoning,
skillName,
toolName,
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/utils/event-to-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function toolToActivity(
runId: string
previousActivityId?: string
delegatedBy?: { expertKey: string; runId: string }
timestamp: number
},
): Activity {
const { skillName, toolName } = toolCall
Expand All @@ -43,6 +44,7 @@ export function toolToActivity(
runId: meta.runId,
previousActivityId: meta.previousActivityId,
delegatedBy: meta.delegatedBy,
timestamp: meta.timestamp,
} as Activity
}

Expand Down Expand Up @@ -267,6 +269,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
text: queryText,
})
runState.lastActivityId = activityId
Expand Down Expand Up @@ -302,6 +305,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
reasoning: runState.completedReasoning,
error: retryEvent.reason,
message: "",
Expand All @@ -323,6 +327,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
reasoning: runState.completedReasoning,
text,
})
Expand All @@ -347,6 +352,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
errorName: errorEvent.error.name,
error: errorEvent.error.message,
isRetryable: errorEvent.error.isRetryable,
Expand Down Expand Up @@ -388,6 +394,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
delegateExpertKey: delegation.expert.key,
query: delegation.query,
reasoning,
Expand Down Expand Up @@ -432,6 +439,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
skillName: toolCall.skillName,
toolName: toolCall.toolName,
args: toolCall.args as Record<string, unknown>,
Expand Down Expand Up @@ -471,6 +479,7 @@ export function processRunEventToActivity(
runId: event.runId,
previousActivityId: runState.lastActivityId,
delegatedBy: runState.delegatedBy,
timestamp: event.timestamp,
})
toolActivities.push(activity)
runState.lastActivityId = activityId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,46 @@ type ActivityLogItemProps = {
*/
function getActivityProps(
activityOrGroup: ActivityOrGroup,
): Pick<Activity, "runId" | "expertKey" | "delegatedBy"> {
): Pick<Activity, "runId" | "expertKey" | "delegatedBy" | "timestamp"> {
if (activityOrGroup.type === "parallelGroup") {
const group = activityOrGroup as ParallelActivitiesGroup
const firstActivity = group.activities[0]
return {
runId: group.runId,
expertKey: group.expertKey,
delegatedBy: firstActivity?.delegatedBy,
timestamp: firstActivity?.timestamp ?? Date.now(),
}
}
return activityOrGroup
}

function formatTimestamp(ts: number): string {
const d = new Date(ts)
const yyyy = d.getFullYear()
const MM = String(d.getMonth() + 1).padStart(2, "0")
const dd = String(d.getDate()).padStart(2, "0")
const hh = String(d.getHours()).padStart(2, "0")
const mm = String(d.getMinutes()).padStart(2, "0")
const ss = String(d.getSeconds()).padStart(2, "0")
const ms = String(d.getMilliseconds()).padStart(3, "0")
return `${yyyy}/${MM}/${dd} ${hh}:${mm}:${ss}.${ms}`
}

export const ActivityLogItem = ({ activity }: ActivityLogItemProps): React.ReactNode => {
const { delegatedBy, expertKey } = getActivityProps(activity)
const { expertKey, delegatedBy, timestamp } = getActivityProps(activity)

const parts = [formatTimestamp(timestamp), expertKey]
if (delegatedBy) {
return (
<Box flexDirection="column">
<Text dimColor bold>
[{expertKey}]
</Text>
<CheckpointActionRow action={activity} />
</Box>
)
parts.push(`⎇ ${delegatedBy.expertKey}`)
}

return <CheckpointActionRow action={activity} />
return (
<Box flexDirection="column">
<Text dimColor bold wrap="truncate">
{parts.join(", ")}
</Text>
<CheckpointActionRow action={activity} />
</Box>
)
}