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 a432bd3f..98639145 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.test.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.test.ts @@ -153,6 +153,33 @@ describe('agent runtime routes', () => { }); }); + it('projects IPC tasks into shared AgentTodo objects', async () => { + call.mockResolvedValue({ + result: { + tasks: [ + { + id: 'task-1', + goal: 'Review the repository and summarize changes', + status: 'running', + createdAt: '2026-04-04T08:00:00.000Z', + updatedAt: '2026-04-04T08:10:00.000Z', + sessionId: 'sess-1', + }, + ], + }, + }); + + const res = await app.inject({ method: 'GET', url: '/api/agent-runtime/todos' }); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.payload); + expect(body.todos[0]).toMatchObject({ + todoId: 'todo_task-1', + sessionId: 'sess-1', + status: 'in-progress', + text: 'Review the repository and summarize changes', + }); + }); + it('projects approval audit records into AgentApprovalCheckpoint objects', async () => { mockFetch .mockResolvedValueOnce({ diff --git a/services/cowork-service/src/modules/agent-runtime/routes.ts b/services/cowork-service/src/modules/agent-runtime/routes.ts index 78b76cff..20af7abe 100644 --- a/services/cowork-service/src/modules/agent-runtime/routes.ts +++ b/services/cowork-service/src/modules/agent-runtime/routes.ts @@ -6,11 +6,13 @@ import { AgentRunSchema, AgentSessionSchema, AgentTaskSchema, + AgentTodoSchema, type AgentActionLog, type AgentApprovalCheckpoint, type AgentRun, type AgentSession, type AgentTask, + type AgentTodo, } from '@bytelyst/events'; import { BadRequestError } from '@bytelyst/errors'; import { config } from '../../lib/config.js'; @@ -146,6 +148,41 @@ function toAgentTask(task: Record, fallbackNow: string): AgentT }); } +function mapTodoStatus(status: unknown): AgentTodo['status'] { + switch (status) { + case 'running': + return 'in-progress'; + case 'completed': + return 'done'; + case 'cancelled': + return 'dropped'; + case 'failed': + return 'open'; + case 'pending': + default: + return 'open'; + } +} + +function toAgentTodo(task: Record, fallbackNow: string): AgentTodo { + const todoId = + typeof task.id === 'string' && task.id.length > 0 ? `todo_${task.id}` : `todo_${fallbackNow}`; + const createdAt = asIsoString(task.createdAt ?? task.startedAt, fallbackNow); + const updatedAt = asIsoString(task.updatedAt ?? task.completedAt ?? createdAt, createdAt); + const text = + typeof task.goal === 'string' && task.goal.trim().length > 0 ? task.goal.trim() : 'Cowork task'; + + return AgentTodoSchema.parse({ + todoId, + sessionId: + typeof task.sessionId === 'string' && task.sessionId.length > 0 ? task.sessionId : todoId, + text, + status: mapTodoStatus(task.status), + createdAt, + updatedAt, + }); +} + function mapApprovalStatus(action: unknown): AgentApprovalCheckpoint['status'] { switch (action) { case 'approval_granted': @@ -381,6 +418,24 @@ export async function agentRuntimeRoutes(app: FastifyInstance) { }; }); + app.get('/api/agent-runtime/todos', async req => { + if (!bridge.isRunning) { + return { todos: [], count: 0 }; + } + + const resp = await bridge.call('list_tasks', { auth: buildAuth(req) }); + if (resp.error) throw new BadRequestError(resp.error.message); + + const raw = (resp.result as { tasks?: Record[] } | undefined)?.tasks ?? []; + const now = new Date().toISOString(); + const todos = raw.map(task => toAgentTodo(task, now)); + + return { + todos, + count: todos.length, + }; + }); + app.get('/api/agent-runtime/approvals', async req => { const records = await fetchApprovalRecords(req); const now = new Date().toISOString();