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,
|
||||
Wrench,
|
||||
MonitorSmartphone,
|
||||
Bot,
|
||||
Globe,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
@ -94,6 +95,7 @@ const navItems = [
|
||||
{ href: '/marketplace', label: 'Marketplace', icon: Store },
|
||||
{ href: '/delivery', label: 'Delivery Log', icon: Mail },
|
||||
{ href: '/jobs', label: 'Scheduled Jobs', icon: Timer },
|
||||
{ href: '/agent-runtime', label: 'Agent Runtime', icon: Bot },
|
||||
{ href: '/event-subscriptions', label: 'Event Subs', icon: Radio },
|
||||
{ href: '/ip-rules', label: 'IP Rules', icon: ShieldBan },
|
||||
{ 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`
|
||||
- `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
|
||||
- [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/runs` projects platform runs into the shared `AgentRun` shape
|
||||
- `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
|
||||
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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Phase 4 Execution Plan
|
||||
|
||||
> **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`
|
||||
> **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
|
||||
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/routes.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 `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
|
||||
- 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] verify that Phase 1 to Phase 3 events render in one unified stream
|
||||
- [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
|
||||
- `e377351` platform-service timeline ingest + query baseline
|
||||
- `6f3a563` admin-web hosted timeline dashboard
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Phase 5 Execution Plan
|
||||
|
||||
> **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`
|
||||
> **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
|
||||
4. action-log contract
|
||||
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.
|
||||
|
||||
@ -28,6 +29,9 @@ This phase establishes the contract layer needed before cross-product runtime ad
|
||||
- `packages/events/src/index.ts`
|
||||
- `services/platform-service/src/modules/agent-runtime/routes.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 && 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/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:
|
||||
|
||||
@ -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/runs` as a shared runtime-run projection
|
||||
- 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
|
||||
- `fe36296` platform-service runtime projection + dispatch validation
|
||||
- `COMMIT_PENDING` admin-web hosted runtime console
|
||||
|
||||
## 7. Remaining Gaps
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user