diff --git a/.changeset/activity-log-header.md b/.changeset/activity-log-header.md new file mode 100644 index 00000000..015fdf83 --- /dev/null +++ b/.changeset/activity-log-header.md @@ -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 diff --git a/packages/core/src/schemas/activity.ts b/packages/core/src/schemas/activity.ts index a395749e..72230e65 100644 --- a/packages/core/src/schemas/activity.ts +++ b/packages/core/src/schemas/activity.ts @@ -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({ @@ -33,6 +35,7 @@ const baseActivitySchema = z.object({ }) .optional(), reasoning: z.string().optional(), + timestamp: z.number(), }) /** Query activity - User input that starts a run */ diff --git a/packages/core/src/utils/activity.ts b/packages/core/src/utils/activity.ts index ba09a583..85ae256c 100644 --- a/packages/core/src/utils/activity.ts +++ b/packages/core/src/utils/activity.ts @@ -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 @@ -88,6 +89,7 @@ export function getActivities(params: GetActivitiesParams): ActivityOrGroup[] { id: "", expertKey, runId, + timestamp: step.startedAt, text: textPart.text, } } @@ -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 @@ -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), ) @@ -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), @@ -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[] = [] @@ -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") @@ -194,6 +201,7 @@ function createCompleteActivity(newMessages: Message[], reasoning: string | unde id: "", expertKey: "", runId: "", + timestamp, reasoning, text: textPart?.text ?? "", } @@ -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, @@ -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, @@ -235,6 +247,7 @@ function createInteractiveToolActivity( function createRetryActivity( newMessages: Message[], reasoning: string | undefined, + timestamp: number, customError?: string, ): Activity { const lastMessage = newMessages[newMessages.length - 1] @@ -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, @@ -269,11 +288,12 @@ export function createBaseToolActivity( toolCall: ToolCall, toolResult: ToolResult, reasoning: string | undefined, + timestamp = 0, ): Activity { const args = toolCall.args as Record 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": { @@ -423,6 +443,7 @@ export function createBaseToolActivity( toolCall, toolResult, reasoning, + timestamp, ) } } @@ -433,6 +454,7 @@ export function createGeneralToolActivity( toolCall: ToolCall, toolResult: ToolResult, reasoning: string | undefined, + timestamp = 0, ): Activity { const errorText = getErrorFromResult(toolResult.result) return { @@ -440,6 +462,7 @@ export function createGeneralToolActivity( id: "", expertKey: "", runId: "", + timestamp, reasoning, skillName, toolName, diff --git a/packages/react/src/utils/event-to-activity.ts b/packages/react/src/utils/event-to-activity.ts index 1e009570..f4587949 100644 --- a/packages/react/src/utils/event-to-activity.ts +++ b/packages/react/src/utils/event-to-activity.ts @@ -27,6 +27,7 @@ export function toolToActivity( runId: string previousActivityId?: string delegatedBy?: { expertKey: string; runId: string } + timestamp: number }, ): Activity { const { skillName, toolName } = toolCall @@ -43,6 +44,7 @@ export function toolToActivity( runId: meta.runId, previousActivityId: meta.previousActivityId, delegatedBy: meta.delegatedBy, + timestamp: meta.timestamp, } as Activity } @@ -267,6 +269,7 @@ export function processRunEventToActivity( runId: event.runId, previousActivityId: runState.lastActivityId, delegatedBy: runState.delegatedBy, + timestamp: event.timestamp, text: queryText, }) runState.lastActivityId = activityId @@ -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: "", @@ -323,6 +327,7 @@ export function processRunEventToActivity( runId: event.runId, previousActivityId: runState.lastActivityId, delegatedBy: runState.delegatedBy, + timestamp: event.timestamp, reasoning: runState.completedReasoning, text, }) @@ -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, @@ -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, @@ -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, @@ -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 diff --git a/packages/tui-components/src/execution/components/activity-log-panel.tsx b/packages/tui-components/src/execution/components/activity-log-panel.tsx index 2e657126..b23ebae8 100644 --- a/packages/tui-components/src/execution/components/activity-log-panel.tsx +++ b/packages/tui-components/src/execution/components/activity-log-panel.tsx @@ -14,7 +14,7 @@ type ActivityLogItemProps = { */ function getActivityProps( activityOrGroup: ActivityOrGroup, -): Pick { +): Pick { if (activityOrGroup.type === "parallelGroup") { const group = activityOrGroup as ParallelActivitiesGroup const firstActivity = group.activities[0] @@ -22,24 +22,38 @@ function getActivityProps( 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 ( - - - [{expertKey}] - - - - ) + parts.push(`⎇ ${delegatedBy.expertKey}`) } - return + return ( + + + {parts.join(", ")} + + + + ) }