Adapts main's changes to the single asynchronous timeline owner: - The subscribe freshness check (#9175) awaits the loaded timeline head and compares the client's cursor against the epoch that client sees, so legacy clients compare against their derived epoch after a rewind. - History reads (#8737) pass purpose "history" when an agent with no stored timeline must be loaded; stored history is still read without the provider. - Background work clears on reload, close and parent delete next to the async provider-child cancellation. - Claude steered-message rewinds (#8372) report an at-target cut. Codex keeps main's saved thread id alongside the reported cut. - Plugin rewinds (#8368) return an unreported outcome; a failed replay after the native revert leaves the rewind for the reload to settle rather than a retry. Manager-level rewinds target the timeline row's message id. - Main's tests read the timeline asynchronously, and wait for the provider to receive a prompt instead of assuming it arrives in the dispatch tick.
8.4 KiB
title, description, nav, order, category
| title | description | nav | order | category |
|---|---|---|---|---|
| SDK events | Subscribe to agent status, timeline, workspace, and provider updates without maintaining a second state model. | Events | 56 | TypeScript SDK |
SDK events
Use an owned subscription to fetch a snapshot and follow its changes. On capable daemons, connecting, plain reads, and local directory listeners do not start observation. Each observation gets a new server-issued ID, even when its filter matches another observation.
Follow one agent's status
const directory = await client.agents.list({
filter: { includeArchived: false },
subscribe: {},
});
directory.subscription.subscribe({
snapshot({ entries }) {
const entry = entries.find(({ agent }) => agent.id === agentId);
console.log(entry?.agent.status);
},
update(message) {
if (message.type !== "agent_update") return;
const update = message.payload;
if (update.kind === "upsert" && update.agent.id === agentId) {
console.log(update.agent.status);
} else if (update.kind === "remove" && update.agentId === agentId) {
console.log("Agent removed from this directory");
}
},
});
// When this view closes:
await directory.subscription.release();
list({ subscribe: {} }) returns the snapshot, subscriptionId, and subscription. Omitting subscribe returns only a snapshot. Do not supply a subscription ID. The subscription delivers its snapshot before updates and receives a new ID and snapshot after reconnect. Releasing it leaves other observations and the underlying agents intact.
client.agents.subscribe() and agent-handle subscribe() add local listeners to observations owned by that API instance. They do not request data. Use the returned subscription's callbacks when multiple filtered views need separate updates.
Follow an agent's background work
Background work is what an agent left running after its tool call returned: a Claude shell started with run_in_background, a Monitor watch, or another task that is not a subagent. Subagents and workflows stay in the agent's subagents.
const agent = client.agents.ref(agentId);
const unsubscribe = agent.backgroundWork.subscribe((items) => render(items));
// When this view closes:
unsubscribe();
subscribe() calls the listener with the current list once the subscription is established, again after each reconnect, then with the whole list on every change. Use list() for a one-shot read. Each item has id, kind ("shell", "monitor", or "other"; treat unknown values as "other"), description (or null), optional command, and startedAt, the time Paseo first observed it. Inventory is scoped to the current provider session and is not persisted. When its runtime is lost, its old items are discarded. An unavailable inventory is reported as an error rather than an empty list.
Pass subscribe(listener, { onError }) to receive setup or list errors. After a setup failure, create a new subscription; after a list failure, the list is read again on the next reconnect. Without onError, the error is logged.
list() rejects and subscribe() throws when the host does not advertise features.agentBackgroundWork; ask the user to update the host. For an item with canStop, call backgroundWork.stop(item) to wait for provider confirmation. Stop requires features.agentBackgroundWorkStop.
Follow timeline events
let entries = (await agent.timeline.refetch()).entries;
const unsubscribe = agent.timeline.subscribe((update) => {
const { event } = update;
if (event.type === "subscription_restored") {
// Live delivery has resumed. Choose whether to fetch missed history.
console.log("Reconnected; events may have been missed");
return;
}
if (event.type === "error") {
console.error("Timeline observation stopped:", event.error);
return;
}
if (event.type === "replacement") {
// Previously fetched history belongs to an old epoch. Fetch the page your UI needs.
void agent.timeline.refetch().then((page) => console.log(page.entries));
return;
}
if (event.type === "rewind") {
// Entries that started in [retireFromSeq, seq) left the conversation. Later ones stay.
entries = entries.filter(
(entry) => entry.seqStart < event.retireFromSeq || entry.seqStart >= event.seq,
);
return;
}
if (event.type === "timeline" && event.item.type === "assistant_message") {
process.stdout.write(event.item.text);
}
if (event.type === "turn_completed") {
console.log("\nTurn completed");
}
});
Await unsubscribe.ready before starting work whose events you need to observe. It waits for the initial live subscription (local listener attachment on broadcast-only hosts); initial history is a separate read. Call unsubscribe() to release demand, or await unsubscribe.release() for teardown.
After reconnect, the same handle receives { agentId, subscriptionId, event: { type: "subscription_restored" } } before subsequent live updates. This is a local SDK notification after membership acknowledgement (local attachment on broadcast-only hosts). The subscription gets a fresh ID. No history is fetched automatically, and missed events are not replayed.
Choose recovery for your consumer: continue live, request a recent page, or call timeline.refetch({ direction: "after", cursor: { epoch, seq } }) using your saved position. Live delivery continues while your read is pending; buffer or reconcile those events with the returned page by epoch and sequence. Follow hasNewer and endCursor to read further pages when needed. A live replacement invalidates the previous epoch. A live rewind keeps the epoch and removes the entries whose seqStart is at least retireFromSeq and below seq; entries from seq on belong to the rewound conversation. A failed explicit history read rejects that read and leaves the live subscription active.
Subscription establishment failures deliver { agentId, event: { type: "error", error } } and release the observation. Establish a new subscription when ready to retry. Releasing a subscription stops its callbacks and prevents restoration after reconnect.
Assistant messages can arrive in pieces. Concatenate their text when you need a complete message, or use run() and read lastMessage when you only need the final reply.
Turn completion comes from turn_completed, turn_failed, or turn_canceled. Do not infer turn completion from an agent_update transition to idle.
Fetch timeline history
const page = await agent.timeline.refetch({
direction: "before",
limit: 100,
projection: "projected",
});
for (const entry of page.entries) {
console.log(entry.seqStart, entry.seqEnd, entry.item.type);
}
Use startCursor, endCursor, hasOlder, and hasNewer from the result to page without inventing offsets. A page can carry retirements, the rewinds whose seq falls inside it. When you append the page to entries you already hold, first drop held entries whose seqStart is at least that retirement's retireFromSeq and below its seq. Entries you hold from seq on stay.
Follow workspace updates
const directory = await client.workspaces.list({ subscribe: {} });
directory.subscription.subscribe({
snapshot({ entries }) {
console.log(entries);
},
update(message) {
if (message.type === "workspace_update") console.log(message.payload);
},
});
// When this view closes:
await directory.subscription.release();
Follow provider catalog changes
const unsubscribe = client.providers.subscribe((update) => {
const ready = update.entries.filter((entry) => entry.status === "ready");
console.log(
"Ready providers:",
ready.map((entry) => entry.provider),
);
});
console.log(await client.providers.snapshot());
// When this view closes:
unsubscribe();
Provider and project subscribe() calls request their own updates and release that demand on unsubscribe. For an initial project cache, buffer updates while awaiting client.projects.list(), then apply them after the snapshot.
Call client.close() when the application no longer needs the connection.
Older daemons
The same methods use the existing connection and legacy delivery behavior. Old directory subscriptions share a server slot: a later filtered observation replaces that slot's filter. Handles have local IDs and separate callbacks, but independent server filters require an updated daemon. Releasing a handle detaches its callbacks; old daemons may continue sending broadcasts.