diff --git a/dashboards/admin-web/.env.example b/dashboards/admin-web/.env.example index 487f8155..2aff50db 100644 --- a/dashboards/admin-web/.env.example +++ b/dashboards/admin-web/.env.example @@ -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 ── diff --git a/dashboards/admin-web/.env.local.example b/dashboards/admin-web/.env.local.example index ff267e88..df853652 100644 --- a/dashboards/admin-web/.env.local.example +++ b/dashboards/admin-web/.env.local.example @@ -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= # Azure Blob Storage @@ -41,4 +42,3 @@ AZURE_BLOB_ACCOUNT_KEY= # Perplexity AI (admin docs chatbot / RAG) PERPLEXITY_API_KEY=pplx-... - diff --git a/dashboards/admin-web/src/__tests__/agent-runtime.test.ts b/dashboards/admin-web/src/__tests__/agent-runtime.test.ts index 8c1494b6..a55a8ee5 100644 --- a/dashboards/admin-web/src/__tests__/agent-runtime.test.ts +++ b/dashboards/admin-web/src/__tests__/agent-runtime.test.ts @@ -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({ diff --git a/dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx b/dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx index 61c6f1eb..518f4c9d 100644 --- a/dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx @@ -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([]); const [runs, setRuns] = useState([]); + const [checkpoints, setCheckpoints] = useState([]); + const [selectedCheckpointId, setSelectedCheckpointId] = useState(null); + const [checkpointDetail, setCheckpointDetail] = useState(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() { + + + + Cowork Checkpoints + + + +
{checkpoints.length}
+
+
@@ -190,6 +286,7 @@ export default function AgentRuntimePage() { Sessions Runs + Cowork Checkpoints Dispatch Validation @@ -296,6 +393,226 @@ export default function AgentRuntimePage() { + +
+ + + Checkpoint Summaries + + + {loading ? ( +
Loading checkpoints...
+ ) : checkpoints.length === 0 ? ( +
+ No Cowork checkpoints are currently available. +
+ ) : ( + checkpoints.map(checkpoint => ( + + )) + )} +
+
+ + + + Checkpoint Drill-In + + + {checkpointLoading ? ( +
Loading checkpoint detail...
+ ) : !checkpointDetail ? ( +
+ Select a checkpoint to inspect direct Cowork observations and projected runtime + summary fields. +
+ ) : ( + <> +
+
+ + direct observation + + + {checkpointDetail.detail.source.observationKind} + +
+
+
+
+ Goal +
+
{checkpointDetail.detail.goal}
+
+
+
+ Model +
+
{checkpointDetail.detail.model || '—'}
+
+
+
+ Folder +
+
+ {checkpointDetail.detail.folder || '—'} +
+
+
+
+ Tool Calls +
+
{checkpointDetail.detail.completedToolCalls}
+
+
+
+ Event ID +
+
+ {checkpointDetail.detail.eventId || '—'} +
+
+
+
+ Correlation +
+
+ {checkpointDetail.detail.correlationId || '—'} +
+
+
+
+ +
+
+ + projected summary + + + {checkpointDetail.detail.source.projectionKind} + +
+
+
+
+ Summary +
+
{checkpointDetail.checkpoint.stateSummary.summary}
+
+
+
+
+ Resume Token +
+
+ {checkpointDetail.checkpoint.resumeToken || '—'} +
+
+
+
+ Checkpoint Artifact +
+
+ {checkpointDetail.detail.observedRefs.checkpointArtifactId || '—'} +
+
+
+
+
+ +
+
+
+ Artifact Refs +
+
+ {checkpointDetail.detail.observedRefs.artifactRefs.length > 0 + ? checkpointDetail.detail.observedRefs.artifactRefs.map(ref => ( +
+ {ref} +
+ )) + : '—'} +
+
+
+
+ Memory Refs +
+
+ {checkpointDetail.detail.observedRefs.memoryRefs.length > 0 + ? checkpointDetail.detail.observedRefs.memoryRefs.map(ref => ( +
+ {ref} +
+ )) + : '—'} +
+
+
+
+ Approval Refs +
+
+ {checkpointDetail.detail.observedRefs.approvalRefs.length > 0 + ? checkpointDetail.detail.observedRefs.approvalRefs.map(ref => ( +
+ {ref} +
+ )) + : '—'} +
+
+
+ + )} +
+
+
+
+ diff --git a/dashboards/admin-web/src/app/api/agent-runtime/route.ts b/dashboards/admin-web/src/app/api/agent-runtime/route.ts index f1561e7b..de06ace1 100644 --- a/dashboards/admin-web/src/app/api/agent-runtime/route.ts +++ b/dashboards/admin-web/src/app/api/agent-runtime/route.ts @@ -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 { 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); diff --git a/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md b/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md index 4207af50..2196550e 100644 --- a/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md +++ b/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md @@ -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 --- diff --git a/docs/ecosystem/OPTIONAL_PRODUCT_ENHANCEMENT_BACKLOG.md b/docs/ecosystem/OPTIONAL_PRODUCT_ENHANCEMENT_BACKLOG.md index adea8a73..8508b7e4 100644 --- a/docs/ecosystem/OPTIONAL_PRODUCT_ENHANCEMENT_BACKLOG.md +++ b/docs/ecosystem/OPTIONAL_PRODUCT_ENHANCEMENT_BACKLOG.md @@ -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 diff --git a/docs/ecosystem/adoption/learning_ai_claw-cowork.md b/docs/ecosystem/adoption/learning_ai_claw-cowork.md index f364d4c2..f5ab46eb 100644 --- a/docs/ecosystem/adoption/learning_ai_claw-cowork.md +++ b/docs/ecosystem/adoption/learning_ai_claw-cowork.md @@ -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 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 6810506a..65182cbd 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.test.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.test.ts @@ -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({ diff --git a/services/cowork-service/src/modules/agent-runtime/routes.ts b/services/cowork-service/src/modules/agent-runtime/routes.ts index 3a935f15..064bdd8b 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.ts @@ -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, 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 | null, + checkpoint: Record, + 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();