feat(admin-web): add agent runtime console
This commit is contained in:
parent
6f3a563edf
commit
71ef2ac6f6
107
dashboards/admin-web/src/__tests__/agent-runtime.test.ts
Normal file
107
dashboards/admin-web/src/__tests__/agent-runtime.test.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const mockGetCurrentUser = vi.fn();
|
||||||
|
vi.mock('@/lib/auth-server', () => ({
|
||||||
|
getCurrentUser: (...args: unknown[]) => mockGetCurrentUser(...args),
|
||||||
|
getCurrentUserFromRequest: (...args: unknown[]) => mockGetCurrentUser(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockLogError = vi.fn();
|
||||||
|
vi.mock('@/lib/logger', () => ({
|
||||||
|
logError: (...args: unknown[]) => mockLogError(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { GET, POST } from '@/app/api/agent-runtime/route';
|
||||||
|
|
||||||
|
async function callGet(qs = '') {
|
||||||
|
const { NextRequest } = await import('next/server');
|
||||||
|
return GET(
|
||||||
|
new NextRequest(
|
||||||
|
new Request(`http://localhost:3001/api/agent-runtime${qs}`, {
|
||||||
|
headers: { Authorization: 'Bearer test', 'x-product-id': 'lysnrai' },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callPost(payload: unknown) {
|
||||||
|
const { NextRequest } = await import('next/server');
|
||||||
|
return POST(
|
||||||
|
new NextRequest(
|
||||||
|
new Request('http://localhost:3001/api/agent-runtime', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer test', 'x-product-id': 'lysnrai' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('agent runtime proxy route', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 401 for unauthenticated callers', async () => {
|
||||||
|
mockGetCurrentUser.mockResolvedValue(null);
|
||||||
|
const res = await callGet();
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('proxies GET session requests to platform-service', async () => {
|
||||||
|
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ sessions: [{ sessionId: 'sess_1' }], count: 1 }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const res = await callGet('?section=sessions&userId=user_1');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
'http://localhost:4003/api/agent-runtime/sessions?userId=user_1',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'x-user-id': 'usr_admin',
|
||||||
|
'x-product-id': 'lysnrai',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('proxies POST dispatch validation requests to platform-service', async () => {
|
||||||
|
mockGetCurrentUser.mockResolvedValue({ id: 'usr_admin', role: 'admin' });
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ valid: true }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const res = await callPost({
|
||||||
|
dispatchId: 'dispatch_1',
|
||||||
|
targetProductId: 'lysnrai',
|
||||||
|
targetExecutor: 'generic-agent',
|
||||||
|
userId: 'user_1',
|
||||||
|
title: 'Test',
|
||||||
|
intent: 'Validate payload',
|
||||||
|
artifactRefs: [],
|
||||||
|
memoryRefs: [],
|
||||||
|
dispatchContext: {
|
||||||
|
originSurface: 'web',
|
||||||
|
originProductId: 'lysnrai',
|
||||||
|
dispatchMode: 'interactive',
|
||||||
|
initiatedAt: '2026-04-04T08:00:00.000Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
'http://localhost:4003/api/agent-runtime/dispatch/validate',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
332
dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx
Normal file
332
dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { BadgeCheck, PlayCircle, RefreshCw, Send, TimerReset } from 'lucide-react';
|
||||||
|
import { createProxyFetch } from '@/lib/proxy-fetch';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
||||||
|
type AgentSession = {
|
||||||
|
sessionId: string;
|
||||||
|
productId: string;
|
||||||
|
userId: string;
|
||||||
|
status: string;
|
||||||
|
startedAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
resumable: boolean;
|
||||||
|
dispatchContext?: {
|
||||||
|
originSurface: string;
|
||||||
|
dispatchMode: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AgentRun = {
|
||||||
|
runId: string;
|
||||||
|
sessionId: string;
|
||||||
|
productId: string;
|
||||||
|
status: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt?: string | null;
|
||||||
|
correlationId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiFetch = createProxyFetch('/api/agent-runtime');
|
||||||
|
|
||||||
|
function formatDate(iso: string | null | undefined) {
|
||||||
|
if (!iso) return '—';
|
||||||
|
return new Date(iso).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgentRuntimePage() {
|
||||||
|
const [sessions, setSessions] = useState<AgentSession[]>([]);
|
||||||
|
const [runs, setRuns] = useState<AgentRun[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [userId, setUserId] = useState('');
|
||||||
|
const [limit, setLimit] = useState('20');
|
||||||
|
const [dispatchPayload, setDispatchPayload] = useState(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
dispatchId: 'dispatch_example',
|
||||||
|
targetProductId: 'lysnrai',
|
||||||
|
targetExecutor: 'generic-agent',
|
||||||
|
userId: 'user_1',
|
||||||
|
title: 'Summarize the latest transcript',
|
||||||
|
intent: 'Prepare a note and memory candidate.',
|
||||||
|
artifactRefs: ['art_transcript_1'],
|
||||||
|
memoryRefs: [],
|
||||||
|
dispatchContext: {
|
||||||
|
originSurface: 'web',
|
||||||
|
originProductId: 'lysnrai',
|
||||||
|
dispatchMode: 'interactive',
|
||||||
|
initiatedAt: '2026-04-04T08:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const [dispatchResult, setDispatchResult] = useState<string>('');
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const sessionParams = new URLSearchParams({ section: 'sessions' });
|
||||||
|
if (userId.trim()) sessionParams.set('userId', userId.trim());
|
||||||
|
|
||||||
|
const runParams = new URLSearchParams({ section: 'runs', limit });
|
||||||
|
|
||||||
|
const [sessionData, runData] = await Promise.all([
|
||||||
|
apiFetch(`?${sessionParams.toString()}`),
|
||||||
|
apiFetch(`?${runParams.toString()}`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSessions(Array.isArray(sessionData?.sessions) ? sessionData.sessions : []);
|
||||||
|
setRuns(Array.isArray(runData?.runs) ? runData.runs : []);
|
||||||
|
setLoading(false);
|
||||||
|
}, [limit, userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
async function validateDispatch() {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(dispatchPayload);
|
||||||
|
const result = await apiFetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(parsed),
|
||||||
|
});
|
||||||
|
setDispatchResult(JSON.stringify(result, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
setDispatchResult(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
error: error instanceof Error ? error.message : 'Invalid JSON payload',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">Agent Runtime</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Inspect shared runtime projections and validate dispatch payloads
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={() => void loadData()}>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Sessions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{sessions.length}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Runs</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{runs.length}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">Active Runs</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{runs.filter(run => run.status === 'running').length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Resumable Sessions
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{sessions.filter(session => session.resumable).length}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter sessions by user ID"
|
||||||
|
value={userId}
|
||||||
|
onChange={event => setUserId(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Run limit"
|
||||||
|
value={limit}
|
||||||
|
onChange={event => setLimit(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="sessions" className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="sessions">Sessions</TabsTrigger>
|
||||||
|
<TabsTrigger value="runs">Runs</TabsTrigger>
|
||||||
|
<TabsTrigger value="dispatch">Dispatch Validation</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="sessions">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="space-y-3 p-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-sm text-muted-foreground">Loading sessions...</div>
|
||||||
|
) : sessions.length === 0 ? (
|
||||||
|
<div className="text-sm text-muted-foreground">No runtime sessions found.</div>
|
||||||
|
) : (
|
||||||
|
sessions.map(session => (
|
||||||
|
<div key={session.sessionId} className="rounded-lg border p-3">
|
||||||
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||||
|
<div className="font-medium">{session.sessionId}</div>
|
||||||
|
<Badge variant="outline">{session.status}</Badge>
|
||||||
|
<Badge variant="outline">{session.productId}</Badge>
|
||||||
|
{session.resumable && (
|
||||||
|
<Badge className="bg-emerald-50 text-emerald-700 border-0">resumable</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 text-sm md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
User
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs">{session.userId}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Started
|
||||||
|
</div>
|
||||||
|
<div>{formatDate(session.startedAt)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Updated
|
||||||
|
</div>
|
||||||
|
<div>{formatDate(session.updatedAt)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Dispatch
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
{session.dispatchContext
|
||||||
|
? `${session.dispatchContext.originSurface} · ${session.dispatchContext.dispatchMode}`
|
||||||
|
: '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="runs">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="space-y-3 p-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-sm text-muted-foreground">Loading runs...</div>
|
||||||
|
) : runs.length === 0 ? (
|
||||||
|
<div className="text-sm text-muted-foreground">No runtime runs found.</div>
|
||||||
|
) : (
|
||||||
|
runs.map(run => (
|
||||||
|
<div key={run.runId} className="rounded-lg border p-3">
|
||||||
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||||
|
<div className="font-medium">{run.runId}</div>
|
||||||
|
<Badge variant="outline">{run.status}</Badge>
|
||||||
|
<Badge variant="outline">{run.productId}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 text-sm md:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Session
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs">{run.sessionId}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Started
|
||||||
|
</div>
|
||||||
|
<div>{formatDate(run.startedAt)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Completed
|
||||||
|
</div>
|
||||||
|
<div>{formatDate(run.completedAt)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Correlation
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs">{run.correlationId || '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="dispatch">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
Dispatch Validation
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Textarea
|
||||||
|
value={dispatchPayload}
|
||||||
|
onChange={event => setDispatchPayload(event.target.value)}
|
||||||
|
className="min-h-[240px] font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<Button onClick={() => void validateDispatch()}>
|
||||||
|
<BadgeCheck className="mr-2 h-4 w-4" />
|
||||||
|
Validate Payload
|
||||||
|
</Button>
|
||||||
|
<div className="rounded-lg border bg-muted/20 p-3">
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||||
|
<TimerReset className="h-4 w-4" />
|
||||||
|
Validation Result
|
||||||
|
</div>
|
||||||
|
<pre className="overflow-x-auto text-xs">
|
||||||
|
{dispatchResult || 'Run validation to inspect the result.'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
dashboards/admin-web/src/app/api/agent-runtime/route.ts
Normal file
61
dashboards/admin-web/src/app/api/agent-runtime/route.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { getCurrentUserFromRequest } from '@/lib/auth-server';
|
||||||
|
import { logError } from '@/lib/logger';
|
||||||
|
|
||||||
|
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
|
||||||
|
|
||||||
|
function buildHeaders(req: NextRequest, callerId: string): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
|
||||||
|
'x-user-id': callerId,
|
||||||
|
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const caller = await getCurrentUserFromRequest(req);
|
||||||
|
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const section = url.searchParams.get('section') ?? 'sessions';
|
||||||
|
const qs = new URLSearchParams(url.searchParams);
|
||||||
|
qs.delete('section');
|
||||||
|
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
||||||
|
const targetPath =
|
||||||
|
section === 'runs'
|
||||||
|
? `/api/agent-runtime/runs${suffix}`
|
||||||
|
: `/api/agent-runtime/sessions${suffix}`;
|
||||||
|
|
||||||
|
const res = await fetch(`${PLATFORM_URL}${targetPath}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: buildHeaders(req, caller.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
|
||||||
|
} catch (error) {
|
||||||
|
logError('Agent runtime proxy GET error', error);
|
||||||
|
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const caller = await getCurrentUserFromRequest(req);
|
||||||
|
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
|
const res = await fetch(`${PLATFORM_URL}/api/agent-runtime/dispatch/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: buildHeaders(req, caller.id),
|
||||||
|
body: await req.text(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
|
||||||
|
} catch (error) {
|
||||||
|
logError('Agent runtime proxy POST error', error);
|
||||||
|
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,6 +54,7 @@ import {
|
|||||||
ShieldBan,
|
ShieldBan,
|
||||||
Wrench,
|
Wrench,
|
||||||
MonitorSmartphone,
|
MonitorSmartphone,
|
||||||
|
Bot,
|
||||||
Globe,
|
Globe,
|
||||||
Download,
|
Download,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@ -94,6 +95,7 @@ const navItems = [
|
|||||||
{ href: '/marketplace', label: 'Marketplace', icon: Store },
|
{ href: '/marketplace', label: 'Marketplace', icon: Store },
|
||||||
{ href: '/delivery', label: 'Delivery Log', icon: Mail },
|
{ href: '/delivery', label: 'Delivery Log', icon: Mail },
|
||||||
{ href: '/jobs', label: 'Scheduled Jobs', icon: Timer },
|
{ href: '/jobs', label: 'Scheduled Jobs', icon: Timer },
|
||||||
|
{ href: '/agent-runtime', label: 'Agent Runtime', icon: Bot },
|
||||||
{ href: '/event-subscriptions', label: 'Event Subs', icon: Radio },
|
{ href: '/event-subscriptions', label: 'Event Subs', icon: Radio },
|
||||||
{ href: '/ip-rules', label: 'IP Rules', icon: ShieldBan },
|
{ href: '/ip-rules', label: 'IP Rules', icon: ShieldBan },
|
||||||
{ href: '/maintenance', label: 'Maintenance', icon: Wrench },
|
{ href: '/maintenance', label: 'Maintenance', icon: Wrench },
|
||||||
|
|||||||
@ -205,6 +205,12 @@ These should be resolved before claiming the ecosystem docs are fully implementa
|
|||||||
- platform-service now persists timeline items in `timeline_items`
|
- platform-service now persists timeline items in `timeline_items`
|
||||||
- `POST /api/timeline/ingest` accepts canonical ecosystem events and stores unified timeline items
|
- `POST /api/timeline/ingest` accepts canonical ecosystem events and stores unified timeline items
|
||||||
- `GET /api/timeline` returns the unified timeline stream with non-admin JWT callers scoped to their own data
|
- `GET /api/timeline` returns the unified timeline stream with non-admin JWT callers scoped to their own data
|
||||||
|
- [x] add first hosted internal timeline UI in `admin-web`
|
||||||
|
Commits:
|
||||||
|
- `6f3a563`
|
||||||
|
Status note:
|
||||||
|
- admin-web now exposes `/timeline` over the shared platform timeline API
|
||||||
|
- the first hosted internal timeline UI supports user, artifact, correlation, and event-name filtering
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -226,6 +232,12 @@ These should be resolved before claiming the ecosystem docs are fully implementa
|
|||||||
- `GET /api/agent-runtime/sessions` projects platform sessions into the shared `AgentSession` shape
|
- `GET /api/agent-runtime/sessions` projects platform sessions into the shared `AgentSession` shape
|
||||||
- `GET /api/agent-runtime/runs` projects platform runs into the shared `AgentRun` shape
|
- `GET /api/agent-runtime/runs` projects platform runs into the shared `AgentRun` shape
|
||||||
- `POST /api/agent-runtime/dispatch/validate` validates `AgentDispatchRequest` payloads against the canonical schema
|
- `POST /api/agent-runtime/dispatch/validate` validates `AgentDispatchRequest` payloads against the canonical schema
|
||||||
|
- [x] add first hosted internal runtime console in `admin-web`
|
||||||
|
Commits:
|
||||||
|
- `COMMIT_PENDING`
|
||||||
|
Status note:
|
||||||
|
- admin-web now exposes `/agent-runtime` over the shared platform runtime API
|
||||||
|
- the first hosted internal runtime UI supports projected session review, projected run review, and dispatch payload validation
|
||||||
- [ ] wire first product implementations to emit the shared runtime objects directly from Cowork and FlowMonk
|
- [ ] wire first product implementations to emit the shared runtime objects directly from Cowork and FlowMonk
|
||||||
Status note:
|
Status note:
|
||||||
- FlowMonk runtime-emitter implementation work was started, but this clone cannot currently verify it because the repo depends on a local npm registry at `http://localhost:3300` and backend dependencies are not installed
|
- FlowMonk runtime-emitter implementation work was started, but this clone cannot currently verify it because the repo depends on a local npm registry at `http://localhost:3300` and backend dependencies are not installed
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Phase 4 Execution Plan
|
# Phase 4 Execution Plan
|
||||||
|
|
||||||
> **Flow:** Shared personal timeline over the first three ecosystem flows
|
> **Flow:** Shared personal timeline over the first three ecosystem flows
|
||||||
> **Status:** Service baseline implemented
|
> **Status:** Service and first hosted UI implemented
|
||||||
> **Owner:** `learning_ai_common_plat`
|
> **Owner:** `learning_ai_common_plat`
|
||||||
> **Purpose:** Turn the personal timeline PRD into a reusable contract and baseline aggregator so the first three ecosystem flows can be rendered as one coherent activity stream.
|
> **Purpose:** Turn the personal timeline PRD into a reusable contract and baseline aggregator so the first three ecosystem flows can be rendered as one coherent activity stream.
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ Phase 4 defines and implements:
|
|||||||
3. a pure aggregator that converts canonical ecosystem events into timeline items
|
3. a pure aggregator that converts canonical ecosystem events into timeline items
|
||||||
4. the first hosted platform-service API for timeline ingest and timeline query
|
4. the first hosted platform-service API for timeline ingest and timeline query
|
||||||
|
|
||||||
This phase still does not add a hosted UI. It now establishes both the shared data contract and the first persisted service surface.
|
This phase now establishes the shared data contract, the first persisted service surface, and the first hosted internal timeline UI.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -29,6 +29,9 @@ This phase still does not add a hosted UI. It now establishes both the shared da
|
|||||||
- `services/platform-service/src/modules/timeline/repository.ts`
|
- `services/platform-service/src/modules/timeline/repository.ts`
|
||||||
- `services/platform-service/src/modules/timeline/routes.ts`
|
- `services/platform-service/src/modules/timeline/routes.ts`
|
||||||
- `services/platform-service/src/modules/timeline/routes.test.ts`
|
- `services/platform-service/src/modules/timeline/routes.test.ts`
|
||||||
|
- `dashboards/admin-web/src/app/api/timeline/route.ts`
|
||||||
|
- `dashboards/admin-web/src/app/(dashboard)/timeline/page.tsx`
|
||||||
|
- `dashboards/admin-web/src/__tests__/timeline.test.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -69,6 +72,8 @@ Observed baseline:
|
|||||||
- platform-service now exposes `POST /api/timeline/ingest` for canonical ecosystem-event ingestion
|
- platform-service now exposes `POST /api/timeline/ingest` for canonical ecosystem-event ingestion
|
||||||
- platform-service now exposes `GET /api/timeline` for unified timeline query
|
- platform-service now exposes `GET /api/timeline` for unified timeline query
|
||||||
- non-admin JWT callers are scoped to their own timeline while admin and API-key callers can query broader product timelines
|
- non-admin JWT callers are scoped to their own timeline while admin and API-key callers can query broader product timelines
|
||||||
|
- admin-web now exposes the first hosted internal timeline UI at `/timeline`
|
||||||
|
- the hosted UI supports filtering by user, correlation, artifact, event name, and page size
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -79,7 +84,7 @@ Observed baseline:
|
|||||||
- [x] implement pure timeline aggregation over canonical events
|
- [x] implement pure timeline aggregation over canonical events
|
||||||
- [x] verify that Phase 1 to Phase 3 events render in one unified stream
|
- [x] verify that Phase 1 to Phase 3 events render in one unified stream
|
||||||
- [x] add first hosted timeline ingest/query API in platform-service
|
- [x] add first hosted timeline ingest/query API in platform-service
|
||||||
- [ ] choose first hosted timeline UI
|
- [x] choose first hosted timeline UI
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -87,3 +92,4 @@ Observed baseline:
|
|||||||
|
|
||||||
- `3f2482b` timeline contract and aggregator baseline
|
- `3f2482b` timeline contract and aggregator baseline
|
||||||
- `e377351` platform-service timeline ingest + query baseline
|
- `e377351` platform-service timeline ingest + query baseline
|
||||||
|
- `6f3a563` admin-web hosted timeline dashboard
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Phase 5 Execution Plan
|
# Phase 5 Execution Plan
|
||||||
|
|
||||||
> **Flow:** Shared agent runtime contract baseline
|
> **Flow:** Shared agent runtime contract baseline
|
||||||
> **Status:** Baseline implemented, platform integration in progress
|
> **Status:** Baseline implemented, platform integration and first hosted UI in progress
|
||||||
> **Owner:** `learning_ai_common_plat`
|
> **Owner:** `learning_ai_common_plat`
|
||||||
> **Purpose:** Turn the runtime contract draft into concrete schemas for sessions, tasks, todos, runs, approvals, dispatch, and action logs.
|
> **Purpose:** Turn the runtime contract draft into concrete schemas for sessions, tasks, todos, runs, approvals, dispatch, and action logs.
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ Phase 5 defines and implements:
|
|||||||
3. approval checkpoint contract
|
3. approval checkpoint contract
|
||||||
4. action-log contract
|
4. action-log contract
|
||||||
5. the first product-facing runtime projection and dispatch-validation API
|
5. the first product-facing runtime projection and dispatch-validation API
|
||||||
|
6. the first hosted internal runtime console over that API
|
||||||
|
|
||||||
This phase establishes the contract layer needed before cross-product runtime adoption in Cowork, FlowMonk, and future agent surfaces.
|
This phase establishes the contract layer needed before cross-product runtime adoption in Cowork, FlowMonk, and future agent surfaces.
|
||||||
|
|
||||||
@ -28,6 +29,9 @@ This phase establishes the contract layer needed before cross-product runtime ad
|
|||||||
- `packages/events/src/index.ts`
|
- `packages/events/src/index.ts`
|
||||||
- `services/platform-service/src/modules/agent-runtime/routes.ts`
|
- `services/platform-service/src/modules/agent-runtime/routes.ts`
|
||||||
- `services/platform-service/src/modules/agent-runtime/routes.test.ts`
|
- `services/platform-service/src/modules/agent-runtime/routes.test.ts`
|
||||||
|
- `dashboards/admin-web/src/app/api/agent-runtime/route.ts`
|
||||||
|
- `dashboards/admin-web/src/app/(dashboard)/agent-runtime/page.tsx`
|
||||||
|
- `dashboards/admin-web/src/__tests__/agent-runtime.test.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -49,6 +53,8 @@ This phase establishes the contract layer needed before cross-product runtime ad
|
|||||||
- `cd learning_ai_common_plat/packages/events && pnpm exec vitest run src/agent-runtime.test.ts`
|
- `cd learning_ai_common_plat/packages/events && pnpm exec vitest run src/agent-runtime.test.ts`
|
||||||
- `cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service exec vitest run src/modules/agent-runtime/routes.test.ts src/server.test.ts`
|
- `cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service exec vitest run src/modules/agent-runtime/routes.test.ts src/server.test.ts`
|
||||||
- `cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service exec tsc --noEmit`
|
- `cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service exec tsc --noEmit`
|
||||||
|
- `cd learning_ai_common_plat/dashboards/admin-web && npm run test -- src/__tests__/agent-runtime.test.ts`
|
||||||
|
- `cd learning_ai_common_plat/dashboards/admin-web && npm run typecheck`
|
||||||
|
|
||||||
Observed baseline:
|
Observed baseline:
|
||||||
|
|
||||||
@ -58,6 +64,8 @@ Observed baseline:
|
|||||||
- platform-service now exposes `GET /api/agent-runtime/sessions` as a shared runtime-session projection
|
- platform-service now exposes `GET /api/agent-runtime/sessions` as a shared runtime-session projection
|
||||||
- platform-service now exposes `GET /api/agent-runtime/runs` as a shared runtime-run projection
|
- platform-service now exposes `GET /api/agent-runtime/runs` as a shared runtime-run projection
|
||||||
- platform-service now exposes `POST /api/agent-runtime/dispatch/validate` against the canonical dispatch schema
|
- platform-service now exposes `POST /api/agent-runtime/dispatch/validate` against the canonical dispatch schema
|
||||||
|
- admin-web now exposes the first hosted internal runtime console at `/agent-runtime`
|
||||||
|
- the hosted UI supports projected session review, projected run review, and dispatch payload validation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -77,6 +85,7 @@ Observed baseline:
|
|||||||
|
|
||||||
- `3f2482b` runtime contract schema baseline
|
- `3f2482b` runtime contract schema baseline
|
||||||
- `fe36296` platform-service runtime projection + dispatch validation
|
- `fe36296` platform-service runtime projection + dispatch validation
|
||||||
|
- `COMMIT_PENDING` admin-web hosted runtime console
|
||||||
|
|
||||||
## 7. Remaining Gaps
|
## 7. Remaining Gaps
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user