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
|
- 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`
|
- `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
|
- `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
|
- 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
|
### 6.1 Remaining Direct Runtime TODOs
|
||||||
|
|||||||
@ -71,8 +71,9 @@ Observed baseline:
|
|||||||
- `GET /api/agent-runtime/tasks`
|
- `GET /api/agent-runtime/tasks`
|
||||||
- `GET /api/agent-runtime/runs`
|
- `GET /api/agent-runtime/runs`
|
||||||
- `GET /api/agent-runtime/approvals`
|
- `GET /api/agent-runtime/approvals`
|
||||||
|
- `GET /api/agent-runtime/actions`
|
||||||
- `POST /api/agent-runtime/dispatch/validate`
|
- `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 () => {
|
it('validates Cowork-targeted dispatch payloads', async () => {
|
||||||
const res = await app.inject({
|
const res = await app.inject({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
||||||
import {
|
import {
|
||||||
|
AgentActionLogSchema,
|
||||||
AgentApprovalCheckpointSchema,
|
AgentApprovalCheckpointSchema,
|
||||||
AgentDispatchRequestSchema,
|
AgentDispatchRequestSchema,
|
||||||
AgentRunSchema,
|
AgentRunSchema,
|
||||||
AgentSessionSchema,
|
AgentSessionSchema,
|
||||||
AgentTaskSchema,
|
AgentTaskSchema,
|
||||||
|
type AgentActionLog,
|
||||||
type AgentApprovalCheckpoint,
|
type AgentApprovalCheckpoint,
|
||||||
type AgentRun,
|
type AgentRun,
|
||||||
type AgentSession,
|
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>[]> {
|
async function fetchApprovalRecords(req: FastifyRequest): Promise<Record<string, unknown>[]> {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
days: '30',
|
days: '30',
|
||||||
@ -234,6 +288,27 @@ async function fetchApprovalRecords(req: FastifyRequest): Promise<Record<string,
|
|||||||
return records;
|
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) {
|
export async function agentRuntimeRoutes(app: FastifyInstance) {
|
||||||
const bridge = getIpcBridge();
|
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 => {
|
app.post('/api/agent-runtime/dispatch/validate', async req => {
|
||||||
const parsed = AgentDispatchRequestSchema.safeParse(req.body);
|
const parsed = AgentDispatchRequestSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user