From 9aeb9bbd598f091b9f5245cfb6f58efcd5563221 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sat, 4 Apr 2026 12:13:46 -0700 Subject: [PATCH] feat(runtime): add checkpoint summaries to shared and cowork surfaces --- packages/events/src/agent-runtime.test.ts | 33 +++ packages/events/src/agent-runtime.ts | 33 +++ packages/events/src/index.ts | 2 + .../src/modules/agent-runtime/routes.test.ts | 93 ++++++-- .../src/modules/agent-runtime/routes.ts | 211 ++++++++++++++++-- 5 files changed, 344 insertions(+), 28 deletions(-) diff --git a/packages/events/src/agent-runtime.test.ts b/packages/events/src/agent-runtime.test.ts index 6050123d..1d152ce9 100644 --- a/packages/events/src/agent-runtime.test.ts +++ b/packages/events/src/agent-runtime.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { AgentActionLogSchema, + AgentCheckpointSchema, AgentApprovalCheckpointSchema, AgentDispatchRequestSchema, AgentRunSchema, @@ -132,4 +133,36 @@ describe('agent runtime contract baseline', () => { expect(run.status).toBe('queued'); }); + + it('validates checkpoint summaries used for resume review', () => { + const checkpoint = AgentCheckpointSchema.parse({ + checkpointId: 'ckpt_cowork_1', + sessionId: 'sess_cowork_1', + runId: 'run_cowork_1', + productId: 'clawcowork', + userId: 'saravana', + createdAt: '2026-04-04T12:00:00.000Z', + statusAtCapture: 'waiting-approval', + currentTaskId: 'task_cowork_1', + todoIds: ['todo_cowork_1'], + artifactRefs: [], + memoryRefs: [], + approvalRefs: ['approval_1'], + dispatchContext: { + originSurface: 'desktop', + originProductId: 'clawcowork', + dispatchMode: 'interactive', + initiatedAt: '2026-04-04T11:58:00.000Z', + }, + resumeToken: 'task_cowork_1', + stateSummary: { + title: 'Investigate imported roadmap risks', + summary: 'Paused for approval after scanning the repository and proposing changes.', + lastActionAt: '2026-04-04T12:00:00.000Z', + }, + }); + + expect(checkpoint.statusAtCapture).toBe('waiting-approval'); + expect(checkpoint.resumeToken).toBe('task_cowork_1'); + }); }); diff --git a/packages/events/src/agent-runtime.ts b/packages/events/src/agent-runtime.ts index 43813966..a4e38072 100644 --- a/packages/events/src/agent-runtime.ts +++ b/packages/events/src/agent-runtime.ts @@ -62,6 +62,38 @@ export const AgentTodoSchema = z.object({ updatedAt: z.string().datetime(), }); +export const AgentCheckpointStatusSchema = z.enum([ + 'queued', + 'running', + 'paused', + 'waiting-approval', + 'completed', + 'failed', + 'cancelled', +]); + +export const AgentCheckpointSchema = z.object({ + checkpointId: z.string().min(1), + sessionId: z.string().min(1), + runId: z.string().min(1).nullable().optional(), + productId: z.string().min(1), + userId: z.string().min(1), + createdAt: z.string().datetime(), + statusAtCapture: AgentCheckpointStatusSchema, + currentTaskId: z.string().min(1).nullable().optional(), + todoIds: z.array(z.string().min(1)), + artifactRefs: z.array(z.string().min(1)), + memoryRefs: z.array(z.string().min(1)), + approvalRefs: z.array(z.string().min(1)), + dispatchContext: AgentDispatchContextSchema.nullable().optional(), + resumeToken: z.string().min(1).nullable().optional(), + stateSummary: z.object({ + title: z.string().min(1), + summary: z.string().min(1), + lastActionAt: z.string().datetime().nullable().optional(), + }), +}); + export const AgentRunStatusSchema = z.enum([ 'queued', 'running', @@ -122,6 +154,7 @@ export type AgentDispatchContext = z.infer; export type AgentSession = z.infer; export type AgentTask = z.infer; export type AgentTodo = z.infer; +export type AgentCheckpoint = z.infer; export type AgentRun = z.infer; export type AgentApprovalCheckpoint = z.infer; export type AgentDispatchRequest = z.infer; diff --git a/packages/events/src/index.ts b/packages/events/src/index.ts index 35e2911d..2c22a07d 100644 --- a/packages/events/src/index.ts +++ b/packages/events/src/index.ts @@ -5,6 +5,7 @@ export type { DurableEventBusOptions } from './durable.js'; export { PlatformEventSchemas } from './types.js'; export { AgentActionLogSchema, + AgentCheckpointSchema, AgentApprovalCheckpointSchema, AgentDispatchContextSchema, AgentDispatchRequestSchema, @@ -52,6 +53,7 @@ export { } from './timeline.js'; export type { AgentActionLog, + AgentCheckpoint, AgentApprovalCheckpoint, AgentDispatchContext, AgentDispatchRequest, diff --git a/services/cowork-service/src/modules/agent-runtime/routes.test.ts b/services/cowork-service/src/modules/agent-runtime/routes.test.ts index 98639145..2946f2e9 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.test.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.test.ts @@ -154,20 +154,38 @@ describe('agent runtime routes', () => { }); it('projects IPC tasks into shared AgentTodo objects', async () => { - call.mockResolvedValue({ - result: { - tasks: [ - { - id: 'task-1', - goal: 'Review the repository and summarize changes', - status: 'running', - createdAt: '2026-04-04T08:00:00.000Z', - updatedAt: '2026-04-04T08:10:00.000Z', - sessionId: 'sess-1', - }, - ], - }, - }); + call + .mockResolvedValueOnce({ + result: { + tasks: [ + { + id: 'task-1', + goal: 'Review the repository and summarize changes', + status: 'running', + createdAt: '2026-04-04T08:00:00.000Z', + updatedAt: '2026-04-04T08:10:00.000Z', + sessionId: 'sess-1', + }, + ], + }, + }) + .mockResolvedValueOnce({ + result: { + checkpoints: [ + { + task_id: 'task-1', + user_id: 'demo-user', + goal: 'Review the repository and summarize changes', + created_at: '2026-04-04T08:00:00.000Z', + last_updated: '2026-04-04T08:10:00.000Z', + completed: false, + cancelled: false, + completed_tool_calls: 3, + error: null, + }, + ], + }, + }); const res = await app.inject({ method: 'GET', url: '/api/agent-runtime/todos' }); expect(res.statusCode).toBe(200); @@ -180,6 +198,53 @@ describe('agent runtime routes', () => { }); }); + it('projects persisted checkpoints into shared AgentCheckpoint objects', async () => { + call + .mockResolvedValueOnce({ + result: { + tasks: [ + { + id: 'task-1', + goal: 'Review the repository and summarize changes', + status: 'pending', + createdAt: '2026-04-04T08:00:00.000Z', + updatedAt: '2026-04-04T08:10:00.000Z', + sessionId: 'sess-1', + }, + ], + }, + }) + .mockResolvedValueOnce({ + result: { + checkpoints: [ + { + task_id: 'task-1', + user_id: 'demo-user', + goal: 'Review the repository and summarize changes', + created_at: '2026-04-04T08:00:00.000Z', + last_updated: '2026-04-04T08:10:00.000Z', + completed: false, + cancelled: false, + completed_tool_calls: 2, + error: null, + }, + ], + }, + }); + + const res = await app.inject({ method: 'GET', url: '/api/agent-runtime/checkpoints' }); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.payload); + expect(body.checkpoints[0]).toMatchObject({ + checkpointId: 'ckpt_task-1', + sessionId: 'sess-1', + runId: 'task-1', + userId: 'demo-user', + statusAtCapture: 'queued', + resumeToken: 'task-1', + }); + }); + it('projects approval audit records into AgentApprovalCheckpoint objects', async () => { mockFetch .mockResolvedValueOnce({ diff --git a/services/cowork-service/src/modules/agent-runtime/routes.ts b/services/cowork-service/src/modules/agent-runtime/routes.ts index 20af7abe..d4c3e045 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.ts @@ -1,6 +1,7 @@ import type { FastifyInstance, FastifyRequest } from 'fastify'; import { AgentActionLogSchema, + AgentCheckpointSchema, AgentApprovalCheckpointSchema, AgentDispatchRequestSchema, AgentRunSchema, @@ -8,6 +9,7 @@ import { AgentTaskSchema, AgentTodoSchema, type AgentActionLog, + type AgentCheckpoint, type AgentApprovalCheckpoint, type AgentRun, type AgentSession, @@ -27,7 +29,13 @@ function buildAuth(req: FastifyRequest): Record { } function asIsoString(value: unknown, fallback: string): string { - return typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? value : fallback; + if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) { + return value; + } + if (typeof value === 'number' && Number.isFinite(value)) { + return new Date(value * 1000).toISOString(); + } + return fallback; } function mapTaskStatus(status: unknown): AgentRun['status'] { @@ -164,25 +172,137 @@ function mapTodoStatus(status: unknown): AgentTodo['status'] { } } -function toAgentTodo(task: Record, fallbackNow: string): AgentTodo { - const todoId = - typeof task.id === 'string' && task.id.length > 0 ? `todo_${task.id}` : `todo_${fallbackNow}`; - const createdAt = asIsoString(task.createdAt ?? task.startedAt, fallbackNow); - const updatedAt = asIsoString(task.updatedAt ?? task.completedAt ?? createdAt, createdAt); +function toAgentTodo( + task: Record | null, + checkpoint: Record, + fallbackNow: string +): AgentTodo { + const taskId = + typeof checkpoint.task_id === 'string' && checkpoint.task_id.length > 0 + ? checkpoint.task_id + : typeof task?.id === 'string' && task.id.length > 0 + ? task.id + : fallbackNow; + const todoId = `todo_${taskId}`; + const createdAt = asIsoString( + task?.createdAt ?? checkpoint.created_at ?? checkpoint.last_updated, + fallbackNow + ); + const updatedAt = asIsoString(task?.updatedAt ?? checkpoint.last_updated ?? createdAt, createdAt); const text = - typeof task.goal === 'string' && task.goal.trim().length > 0 ? task.goal.trim() : 'Cowork task'; + typeof checkpoint.goal === 'string' && checkpoint.goal.trim().length > 0 + ? checkpoint.goal.trim() + : typeof task?.goal === 'string' && task.goal.trim().length > 0 + ? task.goal.trim() + : 'Cowork task'; + const status = + checkpoint.completed === true + ? 'done' + : checkpoint.cancelled === true + ? 'dropped' + : typeof checkpoint.error === 'string' && checkpoint.error.length > 0 + ? 'dropped' + : mapTodoStatus(task?.status); return AgentTodoSchema.parse({ todoId, sessionId: - typeof task.sessionId === 'string' && task.sessionId.length > 0 ? task.sessionId : todoId, + typeof task?.sessionId === 'string' && task.sessionId.length > 0 ? task.sessionId : taskId, text, - status: mapTodoStatus(task.status), + status, createdAt, updatedAt, }); } +function mapCheckpointStatus( + task: Record | null, + checkpoint: Record +): AgentCheckpoint['statusAtCapture'] { + if (checkpoint.completed === true) return 'completed'; + if (checkpoint.cancelled === true) return 'cancelled'; + if (typeof checkpoint.error === 'string' && checkpoint.error.length > 0) return 'failed'; + + switch (task?.status) { + case 'running': + return 'running'; + case 'completed': + return 'completed'; + case 'failed': + return 'failed'; + case 'cancelled': + return 'cancelled'; + case 'pending': + return 'queued'; + default: + return 'paused'; + } +} + +function toAgentCheckpoint( + task: Record | null, + checkpoint: Record, + fallbackNow: string +): AgentCheckpoint { + const taskId = + typeof checkpoint.task_id === 'string' && checkpoint.task_id.length > 0 + ? checkpoint.task_id + : typeof task?.id === 'string' && task.id.length > 0 + ? task.id + : `checkpoint_${fallbackNow}`; + const createdAt = asIsoString(checkpoint.created_at ?? checkpoint.last_updated, fallbackNow); + const lastActionAt = asIsoString(checkpoint.last_updated, createdAt); + const goal = + typeof checkpoint.goal === 'string' && checkpoint.goal.trim().length > 0 + ? checkpoint.goal.trim() + : typeof task?.goal === 'string' && task.goal.trim().length > 0 + ? task.goal.trim() + : 'Cowork task'; + const completedToolCalls = + typeof checkpoint.completed_tool_calls === 'number' ? checkpoint.completed_tool_calls : 0; + const summary = [ + `${completedToolCalls} tool calls recorded`, + checkpoint.completed === true + ? 'task completed' + : checkpoint.cancelled === true + ? 'task cancelled' + : typeof checkpoint.error === 'string' && checkpoint.error.length > 0 + ? `task failed: ${checkpoint.error}` + : 'checkpoint available for resume', + ].join('; '); + + return AgentCheckpointSchema.parse({ + checkpointId: `ckpt_${taskId}`, + sessionId: + typeof task?.sessionId === 'string' && task.sessionId.length > 0 ? task.sessionId : taskId, + runId: typeof task?.id === 'string' && task.id.length > 0 ? task.id : taskId, + productId: PRODUCT_ID, + userId: + typeof checkpoint.user_id === 'string' && checkpoint.user_id.length > 0 + ? checkpoint.user_id + : 'unknown-user', + createdAt, + statusAtCapture: mapCheckpointStatus(task, checkpoint), + currentTaskId: taskId, + todoIds: [`todo_${taskId}`], + artifactRefs: [], + memoryRefs: [], + approvalRefs: [], + dispatchContext: { + originSurface: 'desktop', + originProductId: PRODUCT_ID, + dispatchMode: 'interactive', + initiatedAt: createdAt, + }, + resumeToken: taskId, + stateSummary: { + title: goal.length > 120 ? `${goal.slice(0, 117)}...` : goal, + summary, + lastActionAt, + }, + }); +} + function mapApprovalStatus(action: unknown): AgentApprovalCheckpoint['status'] { switch (action) { case 'approval_granted': @@ -339,6 +459,30 @@ async function fetchApprovalRecords(req: FastifyRequest): Promise[]> { + const bridge = getIpcBridge(); + if (!bridge?.isRunning) return []; + + const response = await bridge.call('list_tasks', { auth: buildAuth(req) }); + const result = + response.result && typeof response.result === 'object' + ? (response.result as Record) + : {}; + return Array.isArray(result.tasks) ? (result.tasks as Record[]) : []; +} + +async function fetchCheckpoints(req: FastifyRequest): Promise[]> { + const bridge = getIpcBridge(); + if (!bridge?.isRunning) return []; + + const response = await bridge.call('list_checkpoints', { auth: buildAuth(req) }); + const result = + response.result && typeof response.result === 'object' + ? (response.result as Record) + : {}; + return Array.isArray(result.checkpoints) ? (result.checkpoints as Record[]) : []; +} + async function fetchAuditRecords(req: FastifyRequest): Promise[]> { const params = new URLSearchParams({ days: '30', @@ -423,12 +567,25 @@ export async function agentRuntimeRoutes(app: FastifyInstance) { return { todos: [], count: 0 }; } - const resp = await bridge.call('list_tasks', { auth: buildAuth(req) }); - if (resp.error) throw new BadRequestError(resp.error.message); - - const raw = (resp.result as { tasks?: Record[] } | undefined)?.tasks ?? []; + const [rawTasks, checkpoints] = await Promise.all([fetchTasks(req), fetchCheckpoints(req)]); + const taskMap = new Map( + rawTasks + .filter(task => typeof task.id === 'string' && task.id.length > 0) + .map(task => [task.id as string, task]) + ); const now = new Date().toISOString(); - const todos = raw.map(task => toAgentTodo(task, now)); + const todos = + checkpoints.length > 0 + ? checkpoints.map(checkpoint => + toAgentTodo( + typeof checkpoint.task_id === 'string' + ? (taskMap.get(checkpoint.task_id) ?? null) + : null, + checkpoint, + now + ) + ) + : rawTasks.map(task => toAgentTodo(task, { task_id: task.id, goal: task.goal }, now)); return { todos, @@ -436,6 +593,32 @@ export async function agentRuntimeRoutes(app: FastifyInstance) { }; }); + app.get('/api/agent-runtime/checkpoints', async req => { + if (!bridge.isRunning) { + return { checkpoints: [], count: 0 }; + } + + const [rawTasks, checkpoints] = await Promise.all([fetchTasks(req), fetchCheckpoints(req)]); + const taskMap = new Map( + rawTasks + .filter(task => typeof task.id === 'string' && task.id.length > 0) + .map(task => [task.id as string, task]) + ); + const now = new Date().toISOString(); + const projected = checkpoints.map(checkpoint => + toAgentCheckpoint( + typeof checkpoint.task_id === 'string' ? (taskMap.get(checkpoint.task_id) ?? null) : null, + checkpoint, + now + ) + ); + + return { + checkpoints: projected, + count: projected.length, + }; + }); + app.get('/api/agent-runtime/approvals', async req => { const records = await fetchApprovalRecords(req); const now = new Date().toISOString();