feat(cowork-service): add runtime action log projection
This commit is contained in:
parent
2f936bb3de
commit
b8242b4601
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<string, unknown>): 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<string, unknown>, fallbackNow: string): AgentActionLog {
|
||||
const details =
|
||||
record.details && typeof record.details === 'object'
|
||||
? (record.details as Record<string, unknown>)
|
||||
: {};
|
||||
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<Record<string, unknown>[]> {
|
||||
const params = new URLSearchParams({
|
||||
days: '30',
|
||||
@ -234,6 +288,27 @@ async function fetchApprovalRecords(req: FastifyRequest): Promise<Record<string,
|
||||
return records;
|
||||
}
|
||||
|
||||
async function fetchAuditRecords(req: FastifyRequest): Promise<Record<string, unknown>[]> {
|
||||
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<string, unknown>[] };
|
||||
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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user