fix(runtime): add queued agent run state

This commit is contained in:
Saravana Achu Mac 2026-04-04 11:11:45 -07:00
parent 152b294d38
commit ff8c5eb704
8 changed files with 90 additions and 7 deletions

View File

@ -246,17 +246,24 @@ These should be resolved before claiming the ecosystem docs are fully implementa
- `b8242b4` adds `GET /api/agent-runtime/actions` with canonical `AgentActionLog` projection
- FlowMonk local installs now resolve `@bytelyst/*` from the sibling `learning_ai_common_plat` workspace instead of the dead localhost registry
- `1ccafa7` adds FlowMonk direct runtime projections for sessions, tasks, runs, action logs, and dispatch validation
- [x] promote `queued` to a first-class `AgentRun` state and preserve it in shared runtime projections
Commits:
- `pending current commit`
Status note:
- `AgentRun` now supports `queued` directly in `@bytelyst/events`
- platform-service now preserves queued platform runs as `queued`
- cowork-service now projects pending IPC tasks as queued runs
- FlowMonk now projects scheduled entries as queued runs
### 6.1 Remaining Direct Runtime TODOs
- Cowork: add `AgentTodo` direct projection once the product exposes first-class todo entities.
- Cowork: attach canonical event IDs to approval and audit trails so ActionTrail lineage can stop using fallback/null semantics.
- FlowMonk: add direct `AgentApprovalCheckpoint` and `AgentTodo` projections once the product exposes first-class approval/todo primitives.
- Platform-service: refine the `queued -> paused` projection fallback once run-vs-session semantics are finalized.
- Shared docs: clarify run-vs-session lifecycle guidance now that `queued` is first-class.
### 6.2 Explicit Blockers And Questions
- Question: should the shared runtime contract add a first-class `queued` run state rather than continuing the current `queued -> paused` fallback projection?
- Question: should Cowork approval/audit records emit canonical event IDs from Rust so runtime projections and ActionTrail lineage can share the same identifiers?
---

View File

@ -1,7 +1,7 @@
# Phase 5 Execution Plan
> **Flow:** Shared agent runtime contract baseline
> **Status:** Baseline implemented, Cowork product integration implemented, FlowMonk follow-up pending
> **Status:** Baseline implemented, Cowork product integration implemented, FlowMonk product integration implemented
> **Owner:** `learning_ai_common_plat`
> **Purpose:** Turn the runtime contract draft into concrete schemas for sessions, tasks, todos, runs, approvals, dispatch, and action logs.
@ -98,14 +98,15 @@ Observed baseline:
- `01201f8` cowork-service runtime task projection
- `b8242b4` cowork-service runtime action-log projection
- `1ccafa7` FlowMonk local shared-package resolution + runtime projection routes
- `a3ae6fe` FlowMonk queued-run projection preservation
- `pending current commit` shared runtime queued-run state
## 7. Remaining Gaps
- Cowork now emits shared runtime projections from cowork-service, but Rust-side canonical event IDs are still missing on approval/audit records and `AgentTodo` still has no first-class product source.
- FlowMonk now emits direct runtime projections for planning sessions, tasks, runs, and action logs, but it still has no first-class approval checkpoint or todo primitive.
- run-vs-session semantics for queued work still need a stricter mapping than the current projection fallback.
- run-vs-session semantics for queued work now preserve `queued` directly in the shared runtime contract, but broader session/run lifecycle guidance still needs to be documented.
## 8. Explicit Blockers And Questions
- Question: should queued work remain represented as `paused`, or should the shared runtime contract gain a first-class `queued` run state?
- Question: should Cowork approval and audit records start emitting canonical event IDs from Rust so ActionTrail and runtime lineage can share the same identifiers?

View File

@ -117,4 +117,19 @@ describe('agent runtime contract baseline', () => {
expect(todo.status).toBe('in-progress');
expect(actionLog.eventName).toBe('agent.run.started');
});
it('accepts queued runs as a first-class runtime state', () => {
const run = AgentRunSchema.parse({
runId: 'run_queued_1',
sessionId: 'sess_queued_1',
productId: 'clawcowork',
status: 'queued',
startedAt: '2026-04-04T10:00:00.000Z',
completedAt: null,
checkpointArtifactId: null,
correlationId: 'corr_queued_1',
});
expect(run.status).toBe('queued');
});
});

View File

@ -63,6 +63,7 @@ export const AgentTodoSchema = z.object({
});
export const AgentRunStatusSchema = z.enum([
'queued',
'running',
'paused',
'waiting-approval',

View File

@ -100,6 +100,31 @@ describe('agent runtime routes', () => {
});
});
it('projects pending IPC tasks into queued AgentRun objects', async () => {
call.mockResolvedValue({
result: {
tasks: [
{
id: 'task-queued',
status: 'pending',
createdAt: '2026-04-04T08:00:00.000Z',
updatedAt: '2026-04-04T08:10:00.000Z',
sessionId: 'sess-queued',
},
],
},
});
const res = await app.inject({ method: 'GET', url: '/api/agent-runtime/runs' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.payload);
expect(body.runs[0]).toMatchObject({
runId: 'task-queued',
sessionId: 'sess-queued',
status: 'queued',
});
});
it('projects IPC tasks into shared AgentTask objects', async () => {
call.mockResolvedValue({
result: {

View File

@ -30,6 +30,8 @@ function asIsoString(value: unknown, fallback: string): string {
function mapTaskStatus(status: unknown): AgentRun['status'] {
switch (status) {
case 'pending':
return 'queued';
case 'running':
return 'running';
case 'completed':
@ -38,7 +40,6 @@ function mapTaskStatus(status: unknown): AgentRun['status'] {
return 'failed';
case 'cancelled':
return 'cancelled';
case 'pending':
default:
return 'paused';
}

View File

@ -101,6 +101,38 @@ describe('agentRuntimeRoutes', () => {
});
});
it('GET /agent-runtime/runs preserves queued runs as queued', async () => {
runRepoMock.listRuns.mockResolvedValue([
{
id: 'run_queued',
productId: 'lysnrai',
kind: 'agent',
name: 'queued-run',
source: 'dispatch',
status: 'queued',
createdAt: '2026-04-04T18:00:00.000Z',
updatedAt: '2026-04-04T18:10:00.000Z',
metadata: {
sessionId: 'sess_queued',
},
},
]);
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
const res = await app.inject({
method: 'GET',
url: '/api/agent-runtime/runs?limit=10',
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.runs[0]).toMatchObject({
runId: 'run_queued',
sessionId: 'sess_queued',
status: 'queued',
});
});
it('POST /agent-runtime/dispatch/validate validates the shared dispatch contract', async () => {
const app = await buildApp({ sub: 'user_1', productId: 'lysnrai' });

View File

@ -38,6 +38,8 @@ function mapSessionStatus(session: {
function mapRunStatus(status: string): AgentRun['status'] {
switch (status) {
case 'queued':
return 'queued';
case 'running':
return 'running';
case 'succeeded':
@ -46,7 +48,6 @@ function mapRunStatus(status: string): AgentRun['status'] {
return 'failed';
case 'cancelled':
return 'cancelled';
case 'queued':
default:
return 'paused';
}