mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
* feat(server): add an in-memory store for agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(claude): report background tasks as background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(server): track agent background work in the agent manager Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(protocol): add agent background work messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(server): serve and push agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(client): expose agent background work on agent handles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sdk): document agent background work Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(claude): cover background shells in background work end to end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): resync background work subscriptions on every snapshot subscribe() now lists the agent's background work each time the event observation is established, so the listener starts with the current list and catches up after a reconnect. A list answered after a newer update is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): answer background work lists without loading dormant agents Background work lives only in memory, so a stored agent that is not loaded has none. The list handler now answers [] from storage instead of resuming the agent, and still rejects unknown, internal, and archived agents. A new test pins that old apps never receive agent.background_work.update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): read Claude background tasks defensively The user's claude binary can differ from the SDK typings, so a task's description is read as unknown and an empty or missing one becomes null. Drop the monitor kind: Claude Monitor watches report as local_bash, so kind is "shell" or "other" today. The store now copies only the wire fields of each input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(server): prove internal background work was emitted before checking globals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(sdk): describe background work resync and feature gating Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(claude): cover background work reported by a retired query The daemon stops reading a Claude query it no longer owns, so work that a retired process still had queued cannot reach the background work list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): let background work subscribers handle a failed list read When the list read that follows each subscription snapshot failed, the error was only logged and the caller got neither the list nor a failure. subscribe() now takes an optional onError that receives it; without one the error is still logged. The read is not retried, since the next reconnect reads the list again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agents): show background work as active Keep background activity visible after a foreground turn ends while preserving independent send and interrupt behavior. * fix(agents): protect background work subscriptions Keep internal work out of public updates and restore observation after a subscription failure. * feat(agents): show Codex and OpenCode background work Keep provider-owned shell activity visible after a turn ends while leaving foreground turn controls independent. * feat(agents): stop individual background work Wait for provider confirmation so stopping one background item leaves sibling work and foreground turn controls independent. * refactor(agents): name background stop parameters * test(website): isolate browser tests from live metadata * test(agents): clarify background stop journeys * fix(sdk): report background work subscription failures Report setup failures so subscribers can replace an observation that the host released. * fix(app): restore background work after reconnect Wait for the host handshake before selecting the retained client so an offline pane starts observing when the connection returns. * fix(agents): retain background shells across rewind Preserve known live shell ownership when conversation history is rewound, and revoke it when the provider runtime exits. * fix(website): launch test seeder across platforms Invoke Wrangler through Node so local website test startup does not depend on an executable npm command shim. * feat(agents): clarify background task details and tracks Keep launch descriptions and commands distinct so background work remains identifiable, and allow composer tracks to scroll when space is limited. * fix(agents): preserve active background work during archive Keep background-running children out of bulk cleanup and warn before an individual archive. Preserve first taps on composer tracks while the keyboard is open. * fix(codex): retain background terminals across rewind Keep live terminal inventories attached to their originating threads so a fork does not hide or misroute their stop actions. * fix(codex): scope background terminal identities to threads Prevent commands on separate threads with the same provider process ID from sharing a stop target. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
164 lines
7.7 KiB
Markdown
164 lines
7.7 KiB
Markdown
---
|
|
title: SDK events
|
|
description: Subscribe to agent status, timeline, workspace, and provider updates without maintaining a second state model.
|
|
nav: Events
|
|
order: 56
|
|
category: 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
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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 === "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](#fetch-timeline-history). 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 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
|
|
|
|
```ts
|
|
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.
|
|
|
|
## Follow workspace updates
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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.
|