* 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>
7.6 KiB
title, description, nav
| title | description | nav |
|---|---|---|
| SDK 事件 | 订阅 Agent 状态、时间线、工作区和 Provider 的更新,无需维护第二套状态模型。 | 事件 |
SDK 事件
使用一个由你持有的订阅来获取快照并跟踪它的变化。在支持的 daemon 上,建立连接、普通读取和本地目录监听器都不会启动观察。每次观察都会获得一个由服务器签发的新 ID,即使它的过滤条件与另一次观察相同。
跟踪一个 Agent 的状态
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: {} }) 返回快照、subscriptionId 和 subscription。省略 subscribe 时只返回快照。不要自己提供订阅 ID。订阅会先投递快照再投递更新,重连后会获得新的 ID 和快照。释放订阅不会影响其他观察和底层的 Agent。
client.agents.subscribe() 和 Agent 句柄上的 subscribe() 会为该 API 实例持有的观察添加本地监听器。它们不会请求数据。当多个带过滤条件的视图需要各自的更新时,请使用返回的订阅上的回调。
跟踪 Agent 的后台工作
后台工作是 Agent 的工具调用返回后仍在运行的工作,例如使用 run_in_background 启动的 Claude shell 或 Monitor 监控。子 Agent 和 workflow 显示在子 Agent 中。
const agent = client.agents.ref(agentId);
const unsubscribe = agent.backgroundWork.subscribe((items) => render(items));
// When this view closes:
unsubscribe();
订阅建立后,subscribe() 会向监听器投递当前列表;每次重连后和每次变化时,也会投递完整列表。用 list() 执行单次读取。每项包含 id、kind("shell"、"monitor" 或 "other",未知值按 "other" 处理)、可为 null 的 description、可选的 command,以及 Paseo 首次观察到该项的时间 startedAt。列表属于当前 Provider 会话,不会持久化。运行时丢失后,旧项会被清除。列表不可用时会报告错误,而不是返回空列表。
传入 subscribe(listener, { onError }) 可接收读取当前列表失败的错误;此时不会调用监听器,下次重连时会重新读取。未提供 onError 时会记录错误。
主机未声明 features.agentBackgroundWork 时,list() 会 reject,subscribe() 会抛出错误;请让用户更新主机。对具有 canStop 的项目调用 backgroundWork.stop(item),等待提供方确认。停止操作需要 features.agentBackgroundWorkStop。
跟踪时间线事件
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。它会等待初始的实时订阅建立(在只支持广播的主机上则是本地监听器挂载完成);初始历史是一次单独的读取。调用 unsubscribe() 释放需求,或在清理时 await unsubscribe.release()。
重连后,同一个句柄会在后续实时更新之前收到 { agentId, subscriptionId, event: { type: "subscription_restored" } }。这是 SDK 在成员关系确认后(在只支持广播的主机上是本地挂载后)发出的本地通知。订阅会获得一个新的 ID。不会自动获取历史,错过的事件也不会重放。
请为你的使用方选择恢复方式:继续接收实时事件、请求最近一页,或者用你保存的位置调用 timeline.refetch({ direction: "after", cursor: { epoch, seq } })。读取进行期间,实时投递会继续;请按 epoch 和序号缓冲这些事件,或与返回的页面进行对账。需要时,根据 hasNewer 和 endCursor 继续读取后面的页面。实时的 replacement 会使之前的 epoch 失效。显式的历史读取失败时,只会让那次读取 reject,实时订阅仍然有效。
订阅建立失败时会投递 { agentId, event: { type: "error", error } } 并释放该观察。准备好重试时,请建立新的订阅。释放订阅会停止它的回调,并阻止重连后的恢复。
助手消息可能分段到达。需要完整消息时请拼接它们的文本;如果只需要最终回复,请使用 run() 并读取 lastMessage。
一轮的完成由 turn_completed、turn_failed 或 turn_canceled 表示。不要根据 agent_update 转为 idle 来推断一轮已经完成。
获取时间线历史
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);
}
使用结果中的 startCursor、endCursor、hasOlder 和 hasNewer 翻页,不要自己计算偏移量。
跟踪工作区更新
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();
跟踪 Provider 目录的变化
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 和项目的 subscribe() 调用会请求各自的更新,并在取消订阅时释放该需求。对于初始的项目缓存,请在 await client.projects.list() 期间缓冲更新,拿到快照后再应用它们。
当应用不再需要连接时,调用 client.close()。
旧版 daemon
同样的方法会使用现有的连接和旧的投递行为。旧版的目录订阅共享一个服务器槽位:之后带过滤条件的观察会替换该槽位的过滤条件。句柄有本地 ID 和各自的回调,但独立的服务器端过滤需要更新后的 daemon。释放句柄会解除它的回调;旧版 daemon 可能会继续发送广播。