feat(runtime): add Cowork checkpoint drill-in

This commit is contained in:
Saravana Achu Mac 2026-04-04 17:29:12 -07:00
parent 9cc4bbe906
commit 82a44c249f
10 changed files with 597 additions and 39 deletions

View File

@ -21,6 +21,7 @@ NEXT_PUBLIC_GOOGLE_CLIENT_ID=
# ── Microservice URLs (consolidated platform-service) ──
PLATFORM_SERVICE_URL=http://localhost:4003
ACTIONTRAIL_SERVICE_URL=http://localhost:4018
COWORK_SERVICE_URL=http://localhost:4009
BILLING_INTERNAL_KEY=
# ── Stripe ──

View File

@ -32,6 +32,7 @@ API_BASE_URL=http://localhost:8000
# Microservice URLs (consolidated platform-service)
PLATFORM_SERVICE_URL=http://localhost:4003
COWORK_SERVICE_URL=http://localhost:4009
BILLING_INTERNAL_KEY=<billing-internal-key>
# Azure Blob Storage
@ -41,4 +42,3 @@ AZURE_BLOB_ACCOUNT_KEY=<blob-account-key>
# Perplexity AI (admin docs chatbot / RAG)
PERPLEXITY_API_KEY=pplx-...

View File

@ -71,6 +71,57 @@ describe('agent runtime proxy route', () => {
);
});
it('proxies Cowork checkpoint list requests to cowork-service', async () => {
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
const fetchMock = vi.fn().mockResolvedValue({
status: 200,
json: async () => ({ checkpoints: [{ checkpointId: 'ckpt_task-1' }], count: 1 }),
});
vi.stubGlobal('fetch', fetchMock);
const res = await callGet('?section=checkpoints&productId=clawcowork&limit=10');
expect(res.status).toBe(200);
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4009/api/agent-runtime/checkpoints?limit=10',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'x-user-id': 'usr_admin',
'x-product-id': 'clawcowork',
}),
})
);
});
it('proxies Cowork checkpoint detail requests to cowork-service', async () => {
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
const fetchMock = vi.fn().mockResolvedValue({
status: 200,
json: async () => ({
checkpoint: { checkpointId: 'ckpt_task-1' },
detail: { goal: 'Review the repository' },
}),
});
vi.stubGlobal('fetch', fetchMock);
const res = await callGet(
'?section=checkpoint-detail&productId=clawcowork&checkpointId=ckpt_task-1'
);
expect(res.status).toBe(200);
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4009/api/agent-runtime/checkpoints/ckpt_task-1',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'x-user-id': 'usr_admin',
'x-product-id': 'clawcowork',
}),
})
);
});
it('proxies POST dispatch validation requests to platform-service', async () => {
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
const fetchMock = vi.fn().mockResolvedValue({

View File

@ -1,7 +1,7 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { BadgeCheck, PlayCircle, RefreshCw, Send, TimerReset } from 'lucide-react';
import { BadgeCheck, RefreshCw, Send, TimerReset } from 'lucide-react';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@ -34,6 +34,54 @@ type AgentRun = {
correlationId?: string | null;
};
type AgentCheckpoint = {
checkpointId: string;
sessionId: string;
runId?: string | null;
productId: string;
userId: string;
createdAt: string;
statusAtCapture: string;
currentTaskId?: string | null;
checkpointArtifactId?: string | null;
artifactRefs: string[];
memoryRefs: string[];
approvalRefs: string[];
resumeToken?: string | null;
stateSummary: {
title: string;
summary: string;
lastActionAt?: string | null;
};
};
type AgentCheckpointDetail = {
checkpoint: AgentCheckpoint;
detail: {
goal: string;
folder?: string | null;
model?: string | null;
createdAt: string;
updatedAt: string;
completedToolCalls: number;
completed: boolean;
cancelled: boolean;
error?: string | null;
eventId?: string | null;
correlationId?: string | null;
source: {
observationKind: string;
projectionKind: string;
};
observedRefs: {
checkpointArtifactId?: string | null;
artifactRefs: string[];
memoryRefs: string[];
approvalRefs: string[];
};
};
};
const apiFetch = createProxyFetch('/api/agent-runtime');
function formatDate(iso: string | null | undefined) {
@ -49,6 +97,10 @@ function formatDate(iso: string | null | undefined) {
export default function AgentRuntimePage() {
const [sessions, setSessions] = useState<AgentSession[]>([]);
const [runs, setRuns] = useState<AgentRun[]>([]);
const [checkpoints, setCheckpoints] = useState<AgentCheckpoint[]>([]);
const [selectedCheckpointId, setSelectedCheckpointId] = useState<string | null>(null);
const [checkpointDetail, setCheckpointDetail] = useState<AgentCheckpointDetail | null>(null);
const [checkpointLoading, setCheckpointLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [userId, setUserId] = useState('');
const [limit, setLimit] = useState('20');
@ -83,20 +135,54 @@ export default function AgentRuntimePage() {
const runParams = new URLSearchParams({ section: 'runs', limit });
const [sessionData, runData] = await Promise.all([
const checkpointParams = new URLSearchParams({
section: 'checkpoints',
productId: 'clawcowork',
limit,
});
const [sessionData, runData, checkpointData] = await Promise.all([
apiFetch(`?${sessionParams.toString()}`),
apiFetch(`?${runParams.toString()}`),
apiFetch(`?${checkpointParams.toString()}`),
]);
setSessions(Array.isArray(sessionData?.sessions) ? sessionData.sessions : []);
setRuns(Array.isArray(runData?.runs) ? runData.runs : []);
const nextCheckpoints = Array.isArray(checkpointData?.checkpoints)
? checkpointData.checkpoints
: [];
setCheckpoints(nextCheckpoints);
setSelectedCheckpointId(current =>
nextCheckpoints.some((checkpoint: AgentCheckpoint) => checkpoint.checkpointId === current)
? current
: (nextCheckpoints[0]?.checkpointId ?? null)
);
setLoading(false);
}, [limit, userId]);
const loadCheckpointDetail = useCallback(async (checkpointId: string | null) => {
if (!checkpointId) {
setCheckpointDetail(null);
return;
}
setCheckpointLoading(true);
const detail = await apiFetch(
`?section=checkpoint-detail&productId=clawcowork&checkpointId=${encodeURIComponent(checkpointId)}`
);
setCheckpointDetail(detail?.checkpoint ? detail : null);
setCheckpointLoading(false);
}, []);
useEffect(() => {
void loadData();
}, [loadData]);
useEffect(() => {
void loadCheckpointDetail(selectedCheckpointId);
}, [loadCheckpointDetail, selectedCheckpointId]);
async function validateDispatch() {
try {
const parsed = JSON.parse(dispatchPayload);
@ -171,6 +257,16 @@ export default function AgentRuntimePage() {
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Cowork Checkpoints
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{checkpoints.length}</div>
</CardContent>
</Card>
</div>
<div className="grid gap-3 md:grid-cols-3">
@ -190,6 +286,7 @@ export default function AgentRuntimePage() {
<TabsList>
<TabsTrigger value="sessions">Sessions</TabsTrigger>
<TabsTrigger value="runs">Runs</TabsTrigger>
<TabsTrigger value="checkpoints">Cowork Checkpoints</TabsTrigger>
<TabsTrigger value="dispatch">Dispatch Validation</TabsTrigger>
</TabsList>
@ -296,6 +393,226 @@ export default function AgentRuntimePage() {
</Card>
</TabsContent>
<TabsContent value="checkpoints">
<div className="grid gap-4 xl:grid-cols-[1.2fr,0.8fr]">
<Card>
<CardHeader>
<CardTitle className="text-base">Checkpoint Summaries</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{loading ? (
<div className="text-sm text-muted-foreground">Loading checkpoints...</div>
) : checkpoints.length === 0 ? (
<div className="text-sm text-muted-foreground">
No Cowork checkpoints are currently available.
</div>
) : (
checkpoints.map(checkpoint => (
<button
key={checkpoint.checkpointId}
type="button"
onClick={() => setSelectedCheckpointId(checkpoint.checkpointId)}
className={`w-full rounded-lg border p-3 text-left transition ${
selectedCheckpointId === checkpoint.checkpointId
? 'border-emerald-500 bg-emerald-50/60'
: 'hover:bg-muted/20'
}`}
>
<div className="mb-2 flex flex-wrap items-center gap-2">
<div className="font-medium">{checkpoint.stateSummary.title}</div>
<Badge variant="outline">{checkpoint.statusAtCapture}</Badge>
<Badge variant="outline">clawcowork</Badge>
</div>
<div className="mb-3 text-sm text-muted-foreground">
{checkpoint.stateSummary.summary}
</div>
<div className="grid gap-2 text-sm md:grid-cols-3">
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Checkpoint
</div>
<div className="font-mono text-xs">{checkpoint.checkpointId}</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Run
</div>
<div className="font-mono text-xs">{checkpoint.runId || '—'}</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Last Action
</div>
<div>
{formatDate(
checkpoint.stateSummary.lastActionAt || checkpoint.createdAt
)}
</div>
</div>
</div>
</button>
))
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Checkpoint Drill-In</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{checkpointLoading ? (
<div className="text-sm text-muted-foreground">Loading checkpoint detail...</div>
) : !checkpointDetail ? (
<div className="text-sm text-muted-foreground">
Select a checkpoint to inspect direct Cowork observations and projected runtime
summary fields.
</div>
) : (
<>
<div className="rounded-lg border bg-muted/20 p-3">
<div className="mb-2 flex flex-wrap items-center gap-2">
<Badge className="bg-sky-50 text-sky-700 border-0">
direct observation
</Badge>
<Badge variant="outline">
{checkpointDetail.detail.source.observationKind}
</Badge>
</div>
<div className="grid gap-3 text-sm md:grid-cols-2">
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Goal
</div>
<div>{checkpointDetail.detail.goal}</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Model
</div>
<div>{checkpointDetail.detail.model || '—'}</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Folder
</div>
<div className="font-mono text-xs">
{checkpointDetail.detail.folder || '—'}
</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Tool Calls
</div>
<div>{checkpointDetail.detail.completedToolCalls}</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Event ID
</div>
<div className="font-mono text-xs">
{checkpointDetail.detail.eventId || '—'}
</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Correlation
</div>
<div className="font-mono text-xs">
{checkpointDetail.detail.correlationId || '—'}
</div>
</div>
</div>
</div>
<div className="rounded-lg border bg-muted/20 p-3">
<div className="mb-2 flex flex-wrap items-center gap-2">
<Badge className="bg-emerald-50 text-emerald-700 border-0">
projected summary
</Badge>
<Badge variant="outline">
{checkpointDetail.detail.source.projectionKind}
</Badge>
</div>
<div className="space-y-2 text-sm">
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Summary
</div>
<div>{checkpointDetail.checkpoint.stateSummary.summary}</div>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Resume Token
</div>
<div className="font-mono text-xs">
{checkpointDetail.checkpoint.resumeToken || '—'}
</div>
</div>
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Checkpoint Artifact
</div>
<div className="font-mono text-xs">
{checkpointDetail.detail.observedRefs.checkpointArtifactId || '—'}
</div>
</div>
</div>
</div>
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-lg border p-3">
<div className="mb-2 text-xs uppercase tracking-wide text-muted-foreground">
Artifact Refs
</div>
<div className="space-y-2 text-xs">
{checkpointDetail.detail.observedRefs.artifactRefs.length > 0
? checkpointDetail.detail.observedRefs.artifactRefs.map(ref => (
<div key={ref} className="font-mono">
{ref}
</div>
))
: '—'}
</div>
</div>
<div className="rounded-lg border p-3">
<div className="mb-2 text-xs uppercase tracking-wide text-muted-foreground">
Memory Refs
</div>
<div className="space-y-2 text-xs">
{checkpointDetail.detail.observedRefs.memoryRefs.length > 0
? checkpointDetail.detail.observedRefs.memoryRefs.map(ref => (
<div key={ref} className="font-mono">
{ref}
</div>
))
: '—'}
</div>
</div>
<div className="rounded-lg border p-3">
<div className="mb-2 text-xs uppercase tracking-wide text-muted-foreground">
Approval Refs
</div>
<div className="space-y-2 text-xs">
{checkpointDetail.detail.observedRefs.approvalRefs.length > 0
? checkpointDetail.detail.observedRefs.approvalRefs.map(ref => (
<div key={ref} className="font-mono">
{ref}
</div>
))
: '—'}
</div>
</div>
</div>
</>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="dispatch">
<Card>
<CardHeader>

View File

@ -3,6 +3,7 @@ import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
const COWORK_SERVICE_URL = process.env.COWORK_SERVICE_URL || 'http://localhost:4009';
function buildHeaders(req: NextRequest, callerId: string): Record<string, string> {
return {
@ -20,17 +21,37 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const section = url.searchParams.get('section') ?? 'sessions';
const productOverride = url.searchParams.get('productId')?.trim() || null;
const qs = new URLSearchParams(url.searchParams);
qs.delete('section');
qs.delete('productId');
const suffix = qs.toString() ? `?${qs.toString()}` : '';
const targetPath =
section === 'runs'
? `/api/agent-runtime/runs${suffix}`
: `/api/agent-runtime/sessions${suffix}`;
const targetProductId =
productOverride || req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai';
const isCowork = targetProductId === 'clawcowork';
const res = await fetch(`${PLATFORM_URL}${targetPath}`, {
let targetBaseUrl = PLATFORM_URL;
let targetPath = `/api/agent-runtime/sessions${suffix}`;
if (section === 'runs') {
targetPath = `/api/agent-runtime/runs${suffix}`;
} else if (isCowork && section === 'checkpoints') {
targetBaseUrl = COWORK_SERVICE_URL;
targetPath = `/api/agent-runtime/checkpoints${suffix}`;
} else if (isCowork && section === 'checkpoint-detail') {
const checkpointId = url.searchParams.get('checkpointId')?.trim();
if (!checkpointId) {
return NextResponse.json({ error: 'checkpointId is required' }, { status: 400 });
}
targetBaseUrl = COWORK_SERVICE_URL;
targetPath = `/api/agent-runtime/checkpoints/${encodeURIComponent(checkpointId)}`;
}
const res = await fetch(`${targetBaseUrl}${targetPath}`, {
method: 'GET',
headers: buildHeaders(req, caller.id),
headers: {
...buildHeaders(req, caller.id),
'x-product-id': targetProductId,
},
});
const data = await res.json().catch(() => null);

View File

@ -41,8 +41,8 @@ Build a shared ByteLyst ecosystem layer so products do not behave like isolated
### What Remains In The Roadmap
- optional product expansion, not baseline blockers:
- richer Cowork checkpoint drill-in if product usage justifies it
- no remaining mandatory roadmap items
- optional future product expansion may continue if real usage justifies it
Canonical backlog:
@ -52,7 +52,8 @@ Canonical backlog:
- the implementation roadmap baseline is materially complete through Phase 5
- the requested documentation hardening pass is complete
- the remaining work is optional product expansion, not missing core baseline functionality
- the currently tracked optional product enhancements are complete
- any next work is net-new product expansion, not unfinished roadmap delivery
### Remaining Work Buckets
@ -106,11 +107,12 @@ Canonical backlog:
- `learning_ai_flowmonk`
Notes:
- safe replay preview and guarded replay execution now exist for deterministic runtime actions
- [ ] evaluate richer Cowork checkpoint drill-in
- [x] evaluate richer Cowork checkpoint drill-in
Owner:
- `oss/learning_ai_claw-cowork`
- `learning_ai_common_plat`
Notes:
- current checkpoint summaries and refs are sufficient for the baseline; richer drill-in is optional product expansion
- `cowork-service` now exposes one-checkpoint drill-in and the hosted admin runtime console now distinguishes direct checkpoint observations from projected summary fields
---

View File

@ -89,6 +89,8 @@ Execution controls add complexity and should only be introduced if the review-on
### 2.2 Cowork richer checkpoint drill-in
> **Status:** implemented
> **Priority:** `medium`
> **Owner repo:** `oss/learning_ai_claw-cowork`
> **Type:** optional product enhancement
@ -129,11 +131,11 @@ Richer drill-in is useful for power-user inspection, but not required to keep th
#### Acceptance criteria
- [ ] checkpoint drill-in shows meaningful incremental state beyond the summary row
- [ ] linked artifact and memory refs can be opened or traced cleanly
- [ ] session/task context remains clear throughout drill-in
- [ ] the UI clearly distinguishes direct observations from projected summaries
- [ ] Rust and service-level tests cover the richer checkpoint shape
- [x] checkpoint drill-in shows meaningful incremental state beyond the summary row
- [x] linked artifact and memory refs can be opened or traced cleanly
- [x] session/task context remains clear throughout drill-in
- [x] the UI clearly distinguishes direct observations from projected summaries
- [x] Rust and service-level tests cover the richer checkpoint shape
#### Risks
@ -141,6 +143,18 @@ Richer drill-in is useful for power-user inspection, but not required to keep th
- drill-in UI can expose implementation detail without user value
- replay/resume affordances may be confused with guaranteed deterministic restoration
#### Implementation notes
- `cowork-service` now exposes a checkpoint-detail route for one persisted Cowork checkpoint
- the hosted admin runtime console now includes a dedicated Cowork checkpoint tab with drill-in detail
- the drill-in explicitly separates persisted Cowork observations from projected runtime summary fields
- checkpoint-linked artifact, memory, and approval refs are surfaced directly in the review panel
#### Commits
- `7240de7` added direct checkpoint artifact IDs to Cowork runtime IPC
- `59ae0e1` added the shared checkpoint artifact contract and projection mapping
---
## 3. Priority Guidance

View File

@ -2,7 +2,7 @@
> **Repo:** `oss/learning_ai_claw-cowork`
> **Ecosystem focus:** audited artifact producer for Phase 3
> **Status:** existing producer surface reused
> **Status:** runtime and checkpoint drill-in adopted
---
@ -45,6 +45,8 @@ Current runtime adoption state:
- approvals and action logs can participate in ActionTrail replay
- persisted checkpoint records now back runtime todo review and runtime checkpoint summaries
- checkpoint summaries now preserve artifact, memory, and approval refs when Cowork provides them
- hosted admin runtime review now supports Cowork-specific checkpoint drill-in
- checkpoint drill-in now separates direct persisted Cowork observations from projected runtime summary fields
- session and task IPC projections now expose canonical event IDs directly
- the shared runtime contract now explicitly documents which Cowork states are direct Rust observations versus `cowork-service` projections

View File

@ -261,6 +261,74 @@ describe('agent runtime routes', () => {
});
});
it('returns richer checkpoint drill-in detail for one persisted checkpoint', async () => {
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',
folder: '/tmp/workspace',
model: 'gpt-5.4',
created_at: '2026-04-04T08:00:00.000Z',
last_updated: '2026-04-04T08:10:00.000Z',
completed: false,
cancelled: false,
completed_tool_calls: 2,
checkpoint_artifact_id: 'artifact://notelett/note-1',
artifact_refs: ['artifact://notelett/note-1'],
memory_refs: ['memory://mindlyst/memory-1'],
approval_refs: ['approval://cowork/ap-1'],
event_id: 'evt_cowork_task_task-1',
correlation_id: 'evt_cowork_task_task-1',
error: null,
},
],
},
});
const res = await app.inject({
method: 'GET',
url: '/api/agent-runtime/checkpoints/ckpt_task-1',
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.payload);
expect(body.checkpoint).toMatchObject({
checkpointId: 'ckpt_task-1',
runId: 'task-1',
sessionId: 'sess-1',
checkpointArtifactId: 'artifact://notelett/note-1',
});
expect(body.detail).toMatchObject({
goal: 'Review the repository and summarize changes',
folder: '/tmp/workspace',
model: 'gpt-5.4',
completedToolCalls: 2,
eventId: 'evt_cowork_task_task-1',
correlationId: 'evt_cowork_task_task-1',
source: {
observationKind: 'persisted-checkpoint',
projectionKind: 'agent-runtime-summary',
},
});
});
it('projects approval audit records into AgentApprovalCheckpoint objects', async () => {
mockFetch
.mockResolvedValueOnce({

View File

@ -16,7 +16,7 @@ import {
type AgentTask,
type AgentTodo,
} from '@bytelyst/events';
import { BadRequestError } from '@bytelyst/errors';
import { BadRequestError, NotFoundError } from '@bytelyst/errors';
import { config } from '../../lib/config.js';
import { getIpcBridge } from '../../lib/ipc-bridge.js';
import { PRODUCT_ID } from '../../lib/product-config.js';
@ -38,6 +38,16 @@ function asIsoString(value: unknown, fallback: string): string {
return fallback;
}
function stringList(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === 'string')
: [];
}
function checkpointTaskIdFromId(checkpointId: string): string {
return checkpointId.startsWith('ckpt_') ? checkpointId.slice(5) : checkpointId;
}
function mapTaskStatus(status: unknown): AgentRun['status'] {
switch (status) {
case 'pending':
@ -79,15 +89,9 @@ function toAgentSession(session: Record<string, unknown>, fallbackNow: string):
typeof session.currentTaskId === 'string' && session.currentTaskId.length > 0
? session.currentTaskId
: null,
memoryRefs: Array.isArray(session.memoryRefs)
? session.memoryRefs.filter((value): value is string => typeof value === 'string')
: [],
artifactRefs: Array.isArray(session.artifactRefs)
? session.artifactRefs.filter((value): value is string => typeof value === 'string')
: [],
approvalRefs: Array.isArray(session.approvalRefs)
? session.approvalRefs.filter((value): value is string => typeof value === 'string')
: [],
memoryRefs: stringList(session.memoryRefs),
artifactRefs: stringList(session.artifactRefs),
approvalRefs: stringList(session.approvalRefs),
dispatchContext: {
originSurface: 'desktop',
originProductId: PRODUCT_ID,
@ -279,9 +283,7 @@ function toAgentCheckpoint(
: 'checkpoint available for resume',
].join('; ');
const artifactRefs = Array.isArray(checkpoint.artifact_refs)
? checkpoint.artifact_refs.filter((value): value is string => typeof value === 'string')
: [];
const artifactRefs = stringList(checkpoint.artifact_refs);
const checkpointArtifactId =
typeof checkpoint.checkpoint_artifact_id === 'string' &&
checkpoint.checkpoint_artifact_id.length > 0
@ -304,12 +306,8 @@ function toAgentCheckpoint(
checkpointArtifactId,
todoIds: [`todo_${taskId}`],
artifactRefs,
memoryRefs: Array.isArray(checkpoint.memory_refs)
? checkpoint.memory_refs.filter((value): value is string => typeof value === 'string')
: [],
approvalRefs: Array.isArray(checkpoint.approval_refs)
? checkpoint.approval_refs.filter((value): value is string => typeof value === 'string')
: [],
memoryRefs: stringList(checkpoint.memory_refs),
approvalRefs: stringList(checkpoint.approval_refs),
dispatchContext: {
originSurface: 'desktop',
originProductId: PRODUCT_ID,
@ -325,6 +323,62 @@ function toAgentCheckpoint(
});
}
function toCheckpointDetail(
task: Record<string, unknown> | null,
checkpoint: Record<string, unknown>,
fallbackNow: string
) {
const projected = toAgentCheckpoint(task, checkpoint, fallbackNow);
return {
checkpoint: projected,
detail: {
goal:
typeof checkpoint.goal === 'string' && checkpoint.goal.trim().length > 0
? checkpoint.goal.trim()
: projected.stateSummary.title,
folder:
typeof checkpoint.folder === 'string' && checkpoint.folder.length > 0
? checkpoint.folder
: null,
model:
typeof checkpoint.model === 'string' && checkpoint.model.length > 0
? checkpoint.model
: null,
createdAt: asIsoString(checkpoint.created_at, projected.createdAt),
updatedAt: asIsoString(
checkpoint.last_updated,
projected.stateSummary.lastActionAt ?? projected.createdAt
),
completedToolCalls:
typeof checkpoint.completed_tool_calls === 'number' ? checkpoint.completed_tool_calls : 0,
completed: checkpoint.completed === true,
cancelled: checkpoint.cancelled === true,
error:
typeof checkpoint.error === 'string' && checkpoint.error.length > 0
? checkpoint.error
: null,
eventId:
typeof checkpoint.event_id === 'string' && checkpoint.event_id.length > 0
? checkpoint.event_id
: null,
correlationId:
typeof checkpoint.correlation_id === 'string' && checkpoint.correlation_id.length > 0
? checkpoint.correlation_id
: null,
source: {
observationKind: 'persisted-checkpoint',
projectionKind: 'agent-runtime-summary',
},
observedRefs: {
checkpointArtifactId: projected.checkpointArtifactId ?? null,
artifactRefs: projected.artifactRefs,
memoryRefs: projected.memoryRefs,
approvalRefs: projected.approvalRefs,
},
},
};
}
function mapApprovalStatus(action: unknown): AgentApprovalCheckpoint['status'] {
switch (action) {
case 'approval_granted':
@ -641,6 +695,34 @@ export async function agentRuntimeRoutes(app: FastifyInstance) {
};
});
app.get('/api/agent-runtime/checkpoints/:checkpointId', async req => {
if (!bridge.isRunning) {
throw new NotFoundError('Checkpoint not found');
}
const checkpointId = checkpointTaskIdFromId(
(req.params as { checkpointId: string }).checkpointId ?? ''
);
if (!checkpointId) {
throw new BadRequestError('checkpointId is required');
}
const [rawTasks, checkpoints] = await Promise.all([fetchTasks(req), fetchCheckpoints(req)]);
const rawCheckpoint = checkpoints.find(
checkpoint => typeof checkpoint.task_id === 'string' && checkpoint.task_id === checkpointId
);
if (!rawCheckpoint) {
throw new NotFoundError('Checkpoint not found');
}
const task =
rawTasks.find(rawTask => typeof rawTask.id === 'string' && rawTask.id === checkpointId) ??
null;
const now = new Date().toISOString();
return toCheckpointDetail(task, rawCheckpoint, now);
});
app.get('/api/agent-runtime/approvals', async req => {
const records = await fetchApprovalRecords(req);
const now = new Date().toISOString();