feat(runtime): add Cowork checkpoint drill-in
This commit is contained in:
parent
9cc4bbe906
commit
82a44c249f
@ -21,6 +21,7 @@ NEXT_PUBLIC_GOOGLE_CLIENT_ID=
|
|||||||
# ── Microservice URLs (consolidated platform-service) ──
|
# ── Microservice URLs (consolidated platform-service) ──
|
||||||
PLATFORM_SERVICE_URL=http://localhost:4003
|
PLATFORM_SERVICE_URL=http://localhost:4003
|
||||||
ACTIONTRAIL_SERVICE_URL=http://localhost:4018
|
ACTIONTRAIL_SERVICE_URL=http://localhost:4018
|
||||||
|
COWORK_SERVICE_URL=http://localhost:4009
|
||||||
BILLING_INTERNAL_KEY=
|
BILLING_INTERNAL_KEY=
|
||||||
|
|
||||||
# ── Stripe ──
|
# ── Stripe ──
|
||||||
|
|||||||
@ -32,6 +32,7 @@ API_BASE_URL=http://localhost:8000
|
|||||||
|
|
||||||
# Microservice URLs (consolidated platform-service)
|
# Microservice URLs (consolidated platform-service)
|
||||||
PLATFORM_SERVICE_URL=http://localhost:4003
|
PLATFORM_SERVICE_URL=http://localhost:4003
|
||||||
|
COWORK_SERVICE_URL=http://localhost:4009
|
||||||
BILLING_INTERNAL_KEY=<billing-internal-key>
|
BILLING_INTERNAL_KEY=<billing-internal-key>
|
||||||
|
|
||||||
# Azure Blob Storage
|
# Azure Blob Storage
|
||||||
@ -41,4 +42,3 @@ AZURE_BLOB_ACCOUNT_KEY=<blob-account-key>
|
|||||||
|
|
||||||
# Perplexity AI (admin docs chatbot / RAG)
|
# Perplexity AI (admin docs chatbot / RAG)
|
||||||
PERPLEXITY_API_KEY=pplx-...
|
PERPLEXITY_API_KEY=pplx-...
|
||||||
|
|
||||||
|
|||||||
@ -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 () => {
|
it('proxies POST dispatch validation requests to platform-service', async () => {
|
||||||
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
|
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
|
||||||
const fetchMock = vi.fn().mockResolvedValue({
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
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 { createProxyFetch } from '@/lib/proxy-fetch';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -34,6 +34,54 @@ type AgentRun = {
|
|||||||
correlationId?: string | null;
|
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');
|
const apiFetch = createProxyFetch('/api/agent-runtime');
|
||||||
|
|
||||||
function formatDate(iso: string | null | undefined) {
|
function formatDate(iso: string | null | undefined) {
|
||||||
@ -49,6 +97,10 @@ function formatDate(iso: string | null | undefined) {
|
|||||||
export default function AgentRuntimePage() {
|
export default function AgentRuntimePage() {
|
||||||
const [sessions, setSessions] = useState<AgentSession[]>([]);
|
const [sessions, setSessions] = useState<AgentSession[]>([]);
|
||||||
const [runs, setRuns] = useState<AgentRun[]>([]);
|
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 [loading, setLoading] = useState(true);
|
||||||
const [userId, setUserId] = useState('');
|
const [userId, setUserId] = useState('');
|
||||||
const [limit, setLimit] = useState('20');
|
const [limit, setLimit] = useState('20');
|
||||||
@ -83,20 +135,54 @@ export default function AgentRuntimePage() {
|
|||||||
|
|
||||||
const runParams = new URLSearchParams({ section: 'runs', limit });
|
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(`?${sessionParams.toString()}`),
|
||||||
apiFetch(`?${runParams.toString()}`),
|
apiFetch(`?${runParams.toString()}`),
|
||||||
|
apiFetch(`?${checkpointParams.toString()}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setSessions(Array.isArray(sessionData?.sessions) ? sessionData.sessions : []);
|
setSessions(Array.isArray(sessionData?.sessions) ? sessionData.sessions : []);
|
||||||
setRuns(Array.isArray(runData?.runs) ? runData.runs : []);
|
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);
|
setLoading(false);
|
||||||
}, [limit, userId]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
void loadData();
|
void loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadCheckpointDetail(selectedCheckpointId);
|
||||||
|
}, [loadCheckpointDetail, selectedCheckpointId]);
|
||||||
|
|
||||||
async function validateDispatch() {
|
async function validateDispatch() {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(dispatchPayload);
|
const parsed = JSON.parse(dispatchPayload);
|
||||||
@ -171,6 +257,16 @@ export default function AgentRuntimePage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
@ -190,6 +286,7 @@ export default function AgentRuntimePage() {
|
|||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="sessions">Sessions</TabsTrigger>
|
<TabsTrigger value="sessions">Sessions</TabsTrigger>
|
||||||
<TabsTrigger value="runs">Runs</TabsTrigger>
|
<TabsTrigger value="runs">Runs</TabsTrigger>
|
||||||
|
<TabsTrigger value="checkpoints">Cowork Checkpoints</TabsTrigger>
|
||||||
<TabsTrigger value="dispatch">Dispatch Validation</TabsTrigger>
|
<TabsTrigger value="dispatch">Dispatch Validation</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@ -296,6 +393,226 @@ export default function AgentRuntimePage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="dispatch">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getCurrentUserFromRequest } from '@/lib/auth-server';
|
|||||||
import { logError } from '@/lib/logger';
|
import { logError } from '@/lib/logger';
|
||||||
|
|
||||||
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
|
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> {
|
function buildHeaders(req: NextRequest, callerId: string): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
@ -20,17 +21,37 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const section = url.searchParams.get('section') ?? 'sessions';
|
const section = url.searchParams.get('section') ?? 'sessions';
|
||||||
|
const productOverride = url.searchParams.get('productId')?.trim() || null;
|
||||||
const qs = new URLSearchParams(url.searchParams);
|
const qs = new URLSearchParams(url.searchParams);
|
||||||
qs.delete('section');
|
qs.delete('section');
|
||||||
|
qs.delete('productId');
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
||||||
const targetPath =
|
const targetProductId =
|
||||||
section === 'runs'
|
productOverride || req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai';
|
||||||
? `/api/agent-runtime/runs${suffix}`
|
const isCowork = targetProductId === 'clawcowork';
|
||||||
: `/api/agent-runtime/sessions${suffix}`;
|
|
||||||
|
|
||||||
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',
|
method: 'GET',
|
||||||
headers: buildHeaders(req, caller.id),
|
headers: {
|
||||||
|
...buildHeaders(req, caller.id),
|
||||||
|
'x-product-id': targetProductId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json().catch(() => null);
|
const data = await res.json().catch(() => null);
|
||||||
|
|||||||
@ -41,8 +41,8 @@ Build a shared ByteLyst ecosystem layer so products do not behave like isolated
|
|||||||
|
|
||||||
### What Remains In The Roadmap
|
### What Remains In The Roadmap
|
||||||
|
|
||||||
- optional product expansion, not baseline blockers:
|
- no remaining mandatory roadmap items
|
||||||
- richer Cowork checkpoint drill-in if product usage justifies it
|
- optional future product expansion may continue if real usage justifies it
|
||||||
|
|
||||||
Canonical backlog:
|
Canonical backlog:
|
||||||
|
|
||||||
@ -52,7 +52,8 @@ Canonical backlog:
|
|||||||
|
|
||||||
- the implementation roadmap baseline is materially complete through Phase 5
|
- the implementation roadmap baseline is materially complete through Phase 5
|
||||||
- the requested documentation hardening pass is complete
|
- 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
|
### Remaining Work Buckets
|
||||||
|
|
||||||
@ -106,11 +107,12 @@ Canonical backlog:
|
|||||||
- `learning_ai_flowmonk`
|
- `learning_ai_flowmonk`
|
||||||
Notes:
|
Notes:
|
||||||
- safe replay preview and guarded replay execution now exist for deterministic runtime actions
|
- 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:
|
Owner:
|
||||||
- `oss/learning_ai_claw-cowork`
|
- `oss/learning_ai_claw-cowork`
|
||||||
|
- `learning_ai_common_plat`
|
||||||
Notes:
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -89,6 +89,8 @@ Execution controls add complexity and should only be introduced if the review-on
|
|||||||
|
|
||||||
### 2.2 Cowork richer checkpoint drill-in
|
### 2.2 Cowork richer checkpoint drill-in
|
||||||
|
|
||||||
|
> **Status:** implemented
|
||||||
|
|
||||||
> **Priority:** `medium`
|
> **Priority:** `medium`
|
||||||
> **Owner repo:** `oss/learning_ai_claw-cowork`
|
> **Owner repo:** `oss/learning_ai_claw-cowork`
|
||||||
> **Type:** optional product enhancement
|
> **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
|
#### Acceptance criteria
|
||||||
|
|
||||||
- [ ] checkpoint drill-in shows meaningful incremental state beyond the summary row
|
- [x] checkpoint drill-in shows meaningful incremental state beyond the summary row
|
||||||
- [ ] linked artifact and memory refs can be opened or traced cleanly
|
- [x] linked artifact and memory refs can be opened or traced cleanly
|
||||||
- [ ] session/task context remains clear throughout drill-in
|
- [x] session/task context remains clear throughout drill-in
|
||||||
- [ ] the UI clearly distinguishes direct observations from projected summaries
|
- [x] the UI clearly distinguishes direct observations from projected summaries
|
||||||
- [ ] Rust and service-level tests cover the richer checkpoint shape
|
- [x] Rust and service-level tests cover the richer checkpoint shape
|
||||||
|
|
||||||
#### Risks
|
#### 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
|
- drill-in UI can expose implementation detail without user value
|
||||||
- replay/resume affordances may be confused with guaranteed deterministic restoration
|
- 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
|
## 3. Priority Guidance
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Repo:** `oss/learning_ai_claw-cowork`
|
> **Repo:** `oss/learning_ai_claw-cowork`
|
||||||
> **Ecosystem focus:** audited artifact producer for Phase 3
|
> **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
|
- approvals and action logs can participate in ActionTrail replay
|
||||||
- persisted checkpoint records now back runtime todo review and runtime checkpoint summaries
|
- 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
|
- 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
|
- 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
|
- the shared runtime contract now explicitly documents which Cowork states are direct Rust observations versus `cowork-service` projections
|
||||||
|
|
||||||
|
|||||||
@ -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 () => {
|
it('projects approval audit records into AgentApprovalCheckpoint objects', async () => {
|
||||||
mockFetch
|
mockFetch
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
type AgentTask,
|
type AgentTask,
|
||||||
type AgentTodo,
|
type AgentTodo,
|
||||||
} from '@bytelyst/events';
|
} from '@bytelyst/events';
|
||||||
import { BadRequestError } from '@bytelyst/errors';
|
import { BadRequestError, NotFoundError } from '@bytelyst/errors';
|
||||||
import { config } from '../../lib/config.js';
|
import { config } from '../../lib/config.js';
|
||||||
import { getIpcBridge } from '../../lib/ipc-bridge.js';
|
import { getIpcBridge } from '../../lib/ipc-bridge.js';
|
||||||
import { PRODUCT_ID } from '../../lib/product-config.js';
|
import { PRODUCT_ID } from '../../lib/product-config.js';
|
||||||
@ -38,6 +38,16 @@ function asIsoString(value: unknown, fallback: string): string {
|
|||||||
return fallback;
|
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'] {
|
function mapTaskStatus(status: unknown): AgentRun['status'] {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
@ -79,15 +89,9 @@ function toAgentSession(session: Record<string, unknown>, fallbackNow: string):
|
|||||||
typeof session.currentTaskId === 'string' && session.currentTaskId.length > 0
|
typeof session.currentTaskId === 'string' && session.currentTaskId.length > 0
|
||||||
? session.currentTaskId
|
? session.currentTaskId
|
||||||
: null,
|
: null,
|
||||||
memoryRefs: Array.isArray(session.memoryRefs)
|
memoryRefs: stringList(session.memoryRefs),
|
||||||
? session.memoryRefs.filter((value): value is string => typeof value === 'string')
|
artifactRefs: stringList(session.artifactRefs),
|
||||||
: [],
|
approvalRefs: stringList(session.approvalRefs),
|
||||||
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')
|
|
||||||
: [],
|
|
||||||
dispatchContext: {
|
dispatchContext: {
|
||||||
originSurface: 'desktop',
|
originSurface: 'desktop',
|
||||||
originProductId: PRODUCT_ID,
|
originProductId: PRODUCT_ID,
|
||||||
@ -279,9 +283,7 @@ function toAgentCheckpoint(
|
|||||||
: 'checkpoint available for resume',
|
: 'checkpoint available for resume',
|
||||||
].join('; ');
|
].join('; ');
|
||||||
|
|
||||||
const artifactRefs = Array.isArray(checkpoint.artifact_refs)
|
const artifactRefs = stringList(checkpoint.artifact_refs);
|
||||||
? checkpoint.artifact_refs.filter((value): value is string => typeof value === 'string')
|
|
||||||
: [];
|
|
||||||
const checkpointArtifactId =
|
const checkpointArtifactId =
|
||||||
typeof checkpoint.checkpoint_artifact_id === 'string' &&
|
typeof checkpoint.checkpoint_artifact_id === 'string' &&
|
||||||
checkpoint.checkpoint_artifact_id.length > 0
|
checkpoint.checkpoint_artifact_id.length > 0
|
||||||
@ -304,12 +306,8 @@ function toAgentCheckpoint(
|
|||||||
checkpointArtifactId,
|
checkpointArtifactId,
|
||||||
todoIds: [`todo_${taskId}`],
|
todoIds: [`todo_${taskId}`],
|
||||||
artifactRefs,
|
artifactRefs,
|
||||||
memoryRefs: Array.isArray(checkpoint.memory_refs)
|
memoryRefs: stringList(checkpoint.memory_refs),
|
||||||
? checkpoint.memory_refs.filter((value): value is string => typeof value === 'string')
|
approvalRefs: stringList(checkpoint.approval_refs),
|
||||||
: [],
|
|
||||||
approvalRefs: Array.isArray(checkpoint.approval_refs)
|
|
||||||
? checkpoint.approval_refs.filter((value): value is string => typeof value === 'string')
|
|
||||||
: [],
|
|
||||||
dispatchContext: {
|
dispatchContext: {
|
||||||
originSurface: 'desktop',
|
originSurface: 'desktop',
|
||||||
originProductId: PRODUCT_ID,
|
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'] {
|
function mapApprovalStatus(action: unknown): AgentApprovalCheckpoint['status'] {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'approval_granted':
|
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 => {
|
app.get('/api/agent-runtime/approvals', async req => {
|
||||||
const records = await fetchApprovalRecords(req);
|
const records = await fetchApprovalRecords(req);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user