Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ events.
| `acp_protocol` | `SessionUpdate \| unknown` | Known ACP protocol event (agent or client method) |
| `claude_protocol` | `SDKMessage` | Known Claude protocol event |
| `codex_protocol` | Typed app-server frame | Known Codex app-server event (narrow with `event.eventType` or the `isCodex*` guards) |
| `system` | `SystemEvent` | Broker system event (`turn.started`, `turn.completed`, `turn.failed`, `broker.error`) |
| `system` | `SystemEvent` | Broker system event (`turn.started`, `turn.resumed`, `turn.completed`, `turn.failed`, `broker.error`, `background.changed`) |
| `unknown` | `null` | Anything else — inspect `axonEvent` for details |

Every timeline event has `{ kind, data, axonEvent }` where `axonEvent` is the
Expand All @@ -279,7 +279,7 @@ conn.onTimelineEvent((event) => {
// event.data is SessionUpdate | unknown
break;
case "system":
// event.data is SystemEvent ({ type: "turn.started" | "turn.completed" | "turn.failed", turnId, ... })
// event.data is SystemEvent ({ type: "turn.started" | "turn.resumed" | "turn.completed" | "turn.failed", turnId, ... } | { type: "background.changed", active })
break;
case "unknown":
// event.data is null — check event.axonEvent for raw data
Expand Down
27 changes: 22 additions & 5 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ conn.onTimelineEvent((event) => {
// Typed ACP payload — narrow further with event.eventType
break;
case "system":
// event.data: { type: "turn.started" | "turn.completed" | "turn.failed", turnId, ... }
// event.data: { type: "turn.started" | "turn.resumed" | "turn.completed" | "turn.failed", turnId, ... } | { type: "background.changed", active }
break;
case "unknown":
break;
Expand Down Expand Up @@ -233,7 +233,7 @@ conn.onTimelineEvent((event) => {
}
break;
case "system":
// event.data: { type: "turn.started" | "turn.completed" | "turn.failed", turnId, ... }
// event.data: { type: "turn.started" | "turn.resumed" | "turn.completed" | "turn.failed", turnId, ... } | { type: "background.changed", active }
break;
case "unknown":
break;
Expand Down Expand Up @@ -703,7 +703,7 @@ conn.onTimelineEvent((event: ACPTimelineEvent) => {
// Use isFromAgent(event) / isFromUser(event) to check direction (or event.axonEvent.origin directly)
break;
case "system":
// event.data is SystemEvent: { type: "turn.started", turnId } | { type: "turn.completed", turnId, stopReason? } | { type: "turn.failed", turnId, error, stopReason? } | { type: "broker.error", message }
// event.data is SystemEvent: { type: "turn.started" | "turn.resumed", turnId } | { type: "turn.completed", turnId, stopReason? } | { type: "turn.failed", turnId, error, stopReason? } | { type: "broker.error", message } | { type: "background.changed", active }
break;
case "unknown":
// event.data is null — use axonEvent to identify and parse the event yourself
Expand Down Expand Up @@ -752,7 +752,7 @@ conn.onTimelineEvent((event: CodexTimelineEvent) => {
// the typed notification frame, "response" -> a correlated JSON-RPC response)
break;
case "system":
// event.data is SystemEvent (turn.started / turn.completed / turn.failed / broker.error)
// event.data is SystemEvent (turn.started / turn.resumed / turn.completed / turn.failed / broker.error / background.changed)
break;
case "unknown":
// event.data is null — use axonEvent to identify and parse the event yourself
Expand Down Expand Up @@ -919,11 +919,28 @@ Typed representation of recognized broker system events:
```typescript
type SystemEvent =
| { type: "turn.started"; turnId: string }
| { type: "turn.resumed"; turnId: string }
| { type: "turn.completed"; turnId: string; stopReason?: string }
| { type: "turn.failed"; turnId: string; error: string; stopReason?: string }
| { type: "broker.error"; message: string };
| { type: "broker.error"; message: string }
| { type: "background.changed"; active: BackgroundTask[] };

interface BackgroundTask {
id: string;
kind: "command" | "agent" | "workflow" | (string & {}); // open: the broker may add kinds
}
```

`turn.resumed` is emitted when the agent resumes the last turn on its own,
typically because a background task it started has finished. It carries the
same `turnId` as that turn, and the resumed turn ends with another
`turn.completed`.

`background.changed` is emitted while no turn is running, whenever the set of
background tasks holding the devbox awake differs from what was last reported.
`active` is the full set still running; an empty list means the agent's
background work is done. Task churn inside a turn is not reported.

`turn.failed` is emitted when the broker terminates an in-flight turn (for
example, on a model error). The Axon stream layer also rejects any pending
ACP JSON-RPC request with a JSON-RPC error (`code: -32000`, `message: error`),
Expand Down
9 changes: 5 additions & 4 deletions sdk/src/acp/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,8 @@ export class ACPAxonConnection {
*
* Every Axon event on the channel is classified into one of:
* - `acp_protocol` — a known ACP protocol event (agent or client method)
* - `system` — a broker system event (`turn.started`, `turn.completed`,
* `turn.failed`, `broker.error`)
* - `system` — a broker system event (`turn.started`, `turn.resumed`,
* `turn.completed`, `turn.failed`, `broker.error`, `background.changed`)
* - `unknown` — anything else
*
* For a pull-based alternative, see {@link receiveTimelineEvents}.
Expand Down Expand Up @@ -606,8 +606,9 @@ export class ACPAxonConnection {
* Classifies a raw Axon event into an {@link ACPTimelineEvent}.
*
* Classification rules:
* 1. `SYSTEM_EVENT` with `turn.started` / `turn.completed` / `turn.failed` /
* `broker.error` -> `system`
* 1. `SYSTEM_EVENT` with a recognized type (`turn.started`, `turn.resumed`,
* `turn.completed`, `turn.failed`, `broker.error`, `background.changed`,
* ...) -> `system`
* 2. Known ACP protocol `event_type` (agent or client method) -> `acp_protocol`
* 3. Everything else -> `unknown`
*
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/acp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export { tryParseSystemEvent, tryParseTimelinePayload } from "../shared/timeline
export type {
AxonEventListener,
AxonEventView,
BackgroundChangedEvent,
BackgroundTask,
BackgroundTaskKind,
BaseConnectionOptions,
CustomTimelineEvent,
SystemEvent,
Expand Down Expand Up @@ -126,19 +129,22 @@ export {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
ElicitationCompleteTimelineEvent,
ElicitationTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "./timeline-event-guards.js";
export {
createCustomEventGuard,
isACPProtocolEvent,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isDevboxLifecycleEvent,
isElicitationCompleteEvent,
Expand All @@ -151,6 +157,7 @@ export {
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "./timeline-event-guards.js";
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/acp/timeline-event-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,25 @@ import type {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "../shared/timeline-event-guards.js";
export {
createCustomEventGuard,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isDevboxLifecycleEvent,
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "../shared/timeline-event-guards.js";
Expand Down
5 changes: 3 additions & 2 deletions sdk/src/claude/classify-claude-axon-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ export function isClaudeProtocolEventType(eventType: string): boolean {
* Classifies a raw Axon event into a {@link ClaudeTimelineEvent}.
*
* Classification rules:
* 1. `SYSTEM_EVENT` with `turn.started` / `turn.completed` / `turn.failed` /
* `broker.error` -> `system`
* 1. `SYSTEM_EVENT` with a recognized type (`turn.started`, `turn.resumed`,
* `turn.completed`, `turn.failed`, `broker.error`, `background.changed`,
* ...) -> `system`
* 2. Known Claude protocol `event_type` -> `claude_protocol` with `eventType` discriminator
* 3. Everything else -> `unknown`
*
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/claude/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,8 @@ export class ClaudeAxonConnection {
*
* Every Axon event on the channel is classified into one of:
* - `claude_protocol` — a known Claude protocol event (user or agent message)
* - `system` — a broker system event (`turn.started`, `turn.completed`,
* `turn.failed`, `broker.error`)
* - `system` — a broker system event (`turn.started`, `turn.resumed`,
* `turn.completed`, `turn.failed`, `broker.error`, `background.changed`)
* - `unknown` — anything else
*
* For a pull-based alternative, see {@link receiveTimelineEvents}.
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/claude/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export { tryParseSystemEvent, tryParseTimelinePayload } from "../shared/timeline
export type {
AxonEventListener,
AxonEventView,
BackgroundChangedEvent,
BackgroundTask,
BackgroundTaskKind,
BaseConnectionOptions,
CustomTimelineEvent,
SystemEvent,
Expand All @@ -100,16 +103,19 @@ export {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "./timeline-event-guards.js";
export {
createCustomEventGuard,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isClaudeAssistantEvent,
isClaudeAssistantTextEvent,
Expand All @@ -123,6 +129,7 @@ export {
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "./timeline-event-guards.js";
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/claude/timeline-event-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,25 @@ import type {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "../shared/timeline-event-guards.js";
export {
createCustomEventGuard,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isDevboxLifecycleEvent,
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "../shared/timeline-event-guards.js";
Expand Down
4 changes: 4 additions & 0 deletions sdk/src/codex/timeline-event-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@ import type {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "../shared/timeline-event-guards.js";
export {
createCustomEventGuard,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isDevboxLifecycleEvent,
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "../shared/timeline-event-guards.js";
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,25 @@ export {
export type {
AgentErrorTimelineEvent,
AgentLogTimelineEvent,
BackgroundChangedTimelineEvent,
BrokerErrorTimelineEvent,
DevboxLifecycleTimelineEvent,
TurnCompletedTimelineEvent,
TurnFailedTimelineEvent,
TurnResumedTimelineEvent,
TurnStartedTimelineEvent,
} from "./timeline-event-guards.js";
export {
createCustomEventGuard,
isAgentErrorEvent,
isAgentLogEvent,
isBackgroundChangedEvent,
isBrokerErrorEvent,
isDevboxLifecycleEvent,
isSystemTimelineEvent,
isTurnCompletedEvent,
isTurnFailedEvent,
isTurnResumedEvent,
isTurnStartedEvent,
isUnknownTimelineEvent,
} from "./timeline-event-guards.js";
Expand All @@ -99,6 +103,9 @@ export type {
AgentLogType,
AxonEventListener,
AxonEventView,
BackgroundChangedEvent,
BackgroundTask,
BackgroundTaskKind,
BaseConnectionOptions,
BaseTimelineEvent,
CustomTimelineEvent,
Expand Down
41 changes: 41 additions & 0 deletions sdk/src/shared/timeline-event-guards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { makeFullAxonEvent as makeAxonEvent } from "../__test-utils__/mock-axon.js";
import { isBackgroundChangedEvent, isTurnResumedEvent } from "./timeline-event-guards.js";
import type { BaseTimelineEvent, SystemEvent } from "./types.js";

function systemEvent(data: SystemEvent): BaseTimelineEvent {
return {
kind: "system",
data,
axonEvent: makeAxonEvent({ event_type: data.type, origin: "SYSTEM_EVENT" }),
} as BaseTimelineEvent;
}

describe("isTurnResumedEvent", () => {
it("narrows turn.resumed and rejects other system events", () => {
expect(isTurnResumedEvent(systemEvent({ type: "turn.resumed", turnId: "t-1" }))).toBe(true);
expect(isTurnResumedEvent(systemEvent({ type: "turn.started", turnId: "t-1" }))).toBe(false);
});
});

describe("isBackgroundChangedEvent", () => {
it("narrows background.changed and rejects other system events", () => {
expect(
isBackgroundChangedEvent(
systemEvent({ type: "background.changed", active: [{ id: "a", kind: "agent" }] }),
),
).toBe(true);
expect(isBackgroundChangedEvent(systemEvent({ type: "turn.resumed", turnId: "t-1" }))).toBe(
false,
);
});

it("rejects non-system events", () => {
const ev = {
kind: "unknown",
data: { type: "background.changed" },
axonEvent: makeAxonEvent({ event_type: "background.changed" }),
} as BaseTimelineEvent;
expect(isBackgroundChangedEvent(ev)).toBe(false);
});
});
Loading
Loading