feat(runtime): add checkpoint summaries to shared and cowork surfaces

This commit is contained in:
Saravana Achu Mac 2026-04-04 12:13:46 -07:00
parent 3330ca55cd
commit 9aeb9bbd59
5 changed files with 344 additions and 28 deletions

View File

@ -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');
});
});

View File

@ -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<typeof AgentDispatchContextSchema>;
export type AgentSession = z.infer<typeof AgentSessionSchema>;
export type AgentTask = z.infer<typeof AgentTaskSchema>;
export type AgentTodo = z.infer<typeof AgentTodoSchema>;
export type AgentCheckpoint = z.infer<typeof AgentCheckpointSchema>;
export type AgentRun = z.infer<typeof AgentRunSchema>;
export type AgentApprovalCheckpoint = z.infer<typeof AgentApprovalCheckpointSchema>;
export type AgentDispatchRequest = z.infer<typeof AgentDispatchRequestSchema>;

View File

@ -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,

View File

@ -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({

View File

@ -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<string, unknown> {
}
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<string, unknown>, 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<string, unknown> | null,
checkpoint: Record<string, unknown>,
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<string, unknown> | null,
checkpoint: Record<string, unknown>
): 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<string, unknown> | null,
checkpoint: Record<string, unknown>,
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<Record<string,
return records;
}
async function fetchTasks(req: FastifyRequest): Promise<Record<string, unknown>[]> {
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<string, unknown>)
: {};
return Array.isArray(result.tasks) ? (result.tasks as Record<string, unknown>[]) : [];
}
async function fetchCheckpoints(req: FastifyRequest): Promise<Record<string, unknown>[]> {
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<string, unknown>)
: {};
return Array.isArray(result.checkpoints) ? (result.checkpoints as Record<string, unknown>[]) : [];
}
async function fetchAuditRecords(req: FastifyRequest): Promise<Record<string, unknown>[]> {
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<string, unknown>[] } | 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();