diff --git a/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md b/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md index 05933dec..f973d228 100644 --- a/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md +++ b/docs/ecosystem/ECOSYSTEM_IMPLEMENTATION_TRACKER.md @@ -243,6 +243,7 @@ These should be resolved before claiming the ecosystem docs are fully implementa - Cowork product-native runtime projections are now implemented in cowork-service - `023826e` adds `GET /api/agent-runtime/sessions`, `GET /api/agent-runtime/runs`, `GET /api/agent-runtime/approvals`, and `POST /api/agent-runtime/dispatch/validate` - `01201f8` adds `GET /api/agent-runtime/tasks` with canonical `AgentTask` projection + - `COMMIT_PENDING` adds `GET /api/agent-runtime/actions` with canonical `AgentActionLog` projection - 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 ### 6.1 Remaining Direct Runtime TODOs diff --git a/docs/ecosystem/PHASE5_AGENT_RUNTIME_CONTRACT_EXECUTION_PLAN.md b/docs/ecosystem/PHASE5_AGENT_RUNTIME_CONTRACT_EXECUTION_PLAN.md index f8f3a737..472e3111 100644 --- a/docs/ecosystem/PHASE5_AGENT_RUNTIME_CONTRACT_EXECUTION_PLAN.md +++ b/docs/ecosystem/PHASE5_AGENT_RUNTIME_CONTRACT_EXECUTION_PLAN.md @@ -71,8 +71,9 @@ Observed baseline: - `GET /api/agent-runtime/tasks` - `GET /api/agent-runtime/runs` - `GET /api/agent-runtime/approvals` + - `GET /api/agent-runtime/actions` - `POST /api/agent-runtime/dispatch/validate` -- Cowork projections now emit canonical `AgentSession`, `AgentTask`, `AgentRun`, and `AgentApprovalCheckpoint` objects from the product backend instead of only relying on platform projections +- Cowork projections now emit canonical `AgentSession`, `AgentTask`, `AgentRun`, `AgentApprovalCheckpoint`, and `AgentActionLog` objects from the product backend instead of only relying on platform projections --- diff --git a/services/cowork-service/src/modules/agent-runtime/routes.test.ts b/services/cowork-service/src/modules/agent-runtime/routes.test.ts index 767e37ee..8c42de5a 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.test.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.test.ts @@ -165,6 +165,40 @@ describe('agent runtime routes', () => { }); }); + it('projects audit records into AgentActionLog objects', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ + records: [ + { + id: 'audit-2', + action: 'task_completed', + category: 'execution', + timestamp: '2026-04-04T09:05:00.000Z', + details: { + taskId: 'task-1', + sessionId: 'sess-1', + correlationId: 'corr-1', + result: 'success', + }, + }, + ], + }), + }); + + const res = await app.inject({ method: 'GET', url: '/api/agent-runtime/actions' }); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.payload); + expect(body.actions[0]).toMatchObject({ + actionLogId: 'audit-2', + sessionId: 'sess-1', + runId: 'task-1', + eventName: 'task_completed', + actorType: 'agent', + correlationId: 'corr-1', + }); + }); + it('validates Cowork-targeted dispatch payloads', async () => { const res = await app.inject({ method: 'POST', diff --git a/services/cowork-service/src/modules/agent-runtime/routes.ts b/services/cowork-service/src/modules/agent-runtime/routes.ts index 8749474c..2b1e32b8 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.ts @@ -1,10 +1,12 @@ import type { FastifyInstance, FastifyRequest } from 'fastify'; import { + AgentActionLogSchema, AgentApprovalCheckpointSchema, AgentDispatchRequestSchema, AgentRunSchema, AgentSessionSchema, AgentTaskSchema, + type AgentActionLog, type AgentApprovalCheckpoint, type AgentRun, type AgentSession, @@ -205,6 +207,58 @@ function toApprovalCheckpoint( }); } +function mapActorType(record: Record): AgentActionLog['actorType'] { + if (typeof record.action === 'string' && record.action.startsWith('approval_')) { + return 'user'; + } + + const category = typeof record.category === 'string' ? record.category : ''; + if (category === 'system' || category === 'platform') { + return 'system'; + } + + return 'agent'; +} + +function toActionLog(record: Record, fallbackNow: string): AgentActionLog { + const details = + record.details && typeof record.details === 'object' + ? (record.details as Record) + : {}; + const sessionId = + typeof details.sessionId === 'string' && details.sessionId.length > 0 + ? details.sessionId + : typeof details.taskId === 'string' && details.taskId.length > 0 + ? details.taskId + : 'unknown-session'; + const runId = + typeof details.runId === 'string' && details.runId.length > 0 + ? details.runId + : typeof details.taskId === 'string' && details.taskId.length > 0 + ? details.taskId + : 'unknown-run'; + + return AgentActionLogSchema.parse({ + actionLogId: + typeof record.id === 'string' && record.id.length > 0 ? record.id : `action_${fallbackNow}`, + sessionId, + runId, + eventName: + typeof record.action === 'string' && record.action.length > 0 + ? record.action + : 'unknown_action', + occurredAt: asIsoString(record.timestamp ?? record.createdAt, fallbackNow), + actorType: mapActorType(record), + correlationId: + typeof details.correlationId === 'string' + ? details.correlationId + : typeof details.taskId === 'string' + ? details.taskId + : null, + payload: details, + }); +} + async function fetchApprovalRecords(req: FastifyRequest): Promise[]> { const params = new URLSearchParams({ days: '30', @@ -234,6 +288,27 @@ async function fetchApprovalRecords(req: FastifyRequest): Promise[]> { + const params = new URLSearchParams({ + days: '30', + limit: '100', + }); + + const res = await fetch(`${config.PLATFORM_SERVICE_URL}/audit?${params.toString()}`, { + headers: { + 'x-product-id': PRODUCT_ID, + 'x-request-id': req.id, + }, + }); + + if (!res.ok) { + throw new BadRequestError(`Platform returned ${res.status} while fetching action logs`); + } + + const body = (await res.json()) as { records?: Record[] }; + return Array.isArray(body.records) ? body.records : []; +} + export async function agentRuntimeRoutes(app: FastifyInstance) { const bridge = getIpcBridge(); @@ -303,6 +378,17 @@ export async function agentRuntimeRoutes(app: FastifyInstance) { }; }); + app.get('/api/agent-runtime/actions', async req => { + const records = await fetchAuditRecords(req); + const now = new Date().toISOString(); + const actions = records.map(record => toActionLog(record, now)); + + return { + actions, + count: actions.length, + }; + }); + app.post('/api/agent-runtime/dispatch/validate', async req => { const parsed = AgentDispatchRequestSchema.safeParse(req.body); if (!parsed.success) {