mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 20:42:10 +00:00
Compare commits
3 Commits
v0.97.1-pr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90f856c11 | ||
|
|
059d76e14a | ||
|
|
4324dce4be |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.97.0"
|
||||
".": "0.98.0"
|
||||
}
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [0.98.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.97.0...v0.98.0) (2026-10-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add batch session metadata lookup ([#1408](https://github.com/agentclientprotocol/claude-agent-acp/issues/1408)) ([0ce42f2](https://github.com/agentclientprotocol/claude-agent-acp/commit/0ce42f2c312d190f78024d8b4b1ae5f00d9e5939))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* publish Monitor async tasks as monitors ([#1452](https://github.com/agentclientprotocol/claude-agent-acp/issues/1452)) ([4324dce](https://github.com/agentclientprotocol/claude-agent-acp/commit/4324dce4be877c55385b3389cebac1c63d08f456))
|
||||
* settle the tool call of a SandboxNetworkAccess permission request ([#1448](https://github.com/agentclientprotocol/claude-agent-acp/issues/1448)) ([8f43ac8](https://github.com/agentclientprotocol/claude-agent-acp/commit/8f43ac808931a3f14719bbed37b9b153c9933139))
|
||||
* **v2:** leave the tool call status out of permission subjects ([#1447](https://github.com/agentclientprotocol/claude-agent-acp/issues/1447)) ([c7c80f7](https://github.com/agentclientprotocol/claude-agent-acp/commit/c7c80f7b26f8d90f7f1c828036db64bc46dddddf))
|
||||
|
||||
## [0.97.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.96.0...v0.97.0) (2026-10-26)
|
||||
|
||||
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.97.0",
|
||||
"version": "0.98.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.97.0",
|
||||
"version": "0.98.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.0.2",
|
||||
@@ -477,9 +477,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz",
|
||||
"integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==",
|
||||
"version": "1.32.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz",
|
||||
"integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.97.0",
|
||||
"version": "0.98.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
|
||||
@@ -15,6 +15,8 @@ type AsyncTask = {
|
||||
taskType: string;
|
||||
description: string;
|
||||
showInTranscript: boolean;
|
||||
/** A Monitor tool call or an SDK `local_monitor` task: published as a monitor the client cannot stop. */
|
||||
monitor: boolean;
|
||||
outputFilePath?: string;
|
||||
toolCallId?: string;
|
||||
/**
|
||||
@@ -142,7 +144,8 @@ export class AsyncTaskRuntime {
|
||||
* the tasks that a tool call in another session started, for example in
|
||||
* a native subagent session. `toolNameOf`: the name of the tool of a tool
|
||||
* call, or `undefined` when the tool call is unknown. A task that a
|
||||
* `Monitor` tool call started is ignored. */
|
||||
* `Monitor` tool call started is published as a monitor: the SDK reports
|
||||
* it as `local_bash`, so the adapter names it a monitor. */
|
||||
private readonly options: {
|
||||
notices?: boolean;
|
||||
routeOf?: (toolCallId: string) => Route | undefined;
|
||||
@@ -155,7 +158,7 @@ export class AsyncTaskRuntime {
|
||||
const taskId = taskIdOf(message);
|
||||
if (!taskId) return;
|
||||
const task = this.task(taskId);
|
||||
if (isSubagentTask(message) || isMonitorTask(message)) {
|
||||
if (isSubagentTask(message)) {
|
||||
this.ignore(task);
|
||||
return;
|
||||
}
|
||||
@@ -186,7 +189,7 @@ export class AsyncTaskRuntime {
|
||||
const taskId = taskIdOf(message);
|
||||
if (!taskId) return;
|
||||
const task = this.task(taskId);
|
||||
if (isSubagentTask(message) || isMonitorTask(message)) {
|
||||
if (isSubagentTask(message)) {
|
||||
this.ignore(task);
|
||||
return;
|
||||
}
|
||||
@@ -355,7 +358,7 @@ export class AsyncTaskRuntime {
|
||||
const task = this.task(taskId);
|
||||
if (task.ignored || isTerminal(task.state)) continue;
|
||||
const levelTaskType = field(item, "taskType", "task_type");
|
||||
if (levelTaskType === "local_agent" || levelTaskType === "local_monitor") {
|
||||
if (levelTaskType === "local_agent") {
|
||||
this.ignore(task);
|
||||
continue;
|
||||
}
|
||||
@@ -408,7 +411,13 @@ export class AsyncTaskRuntime {
|
||||
|
||||
canStop(taskId: string): boolean {
|
||||
const task = this.tasks.get(taskId);
|
||||
return task?.announced === true && !task.ignored && !task.stopping && !isTerminal(task.state);
|
||||
return (
|
||||
task?.announced === true &&
|
||||
!task.monitor &&
|
||||
!task.ignored &&
|
||||
!task.stopping &&
|
||||
!isTerminal(task.state)
|
||||
);
|
||||
}
|
||||
|
||||
claimStop(taskId: string): boolean {
|
||||
@@ -575,6 +584,7 @@ export class AsyncTaskRuntime {
|
||||
taskType: "task",
|
||||
description: "Background task",
|
||||
showInTranscript: true,
|
||||
monitor: false,
|
||||
announced: false,
|
||||
held: false,
|
||||
deferred: [],
|
||||
@@ -595,6 +605,7 @@ export class AsyncTaskRuntime {
|
||||
task.startedObserved = true;
|
||||
const rawTaskType = field(message, "taskType", "task_type");
|
||||
if (rawTaskType !== undefined) task.taskType = friendlyTaskType(rawTaskType);
|
||||
if (rawTaskType === "local_monitor") task.monitor = true;
|
||||
const description = nonBlankString(message.description);
|
||||
if (description) task.description = description;
|
||||
else if (task.description === "Background task")
|
||||
@@ -652,6 +663,7 @@ export class AsyncTaskRuntime {
|
||||
private mergeLevel(task: AsyncTask, level: BackgroundTaskLevel): void {
|
||||
const rawTaskType = field(level, "taskType", "task_type");
|
||||
if (rawTaskType !== undefined) task.taskType = friendlyTaskType(rawTaskType);
|
||||
if (rawTaskType === "local_monitor") task.monitor = true;
|
||||
const description = nonBlankString(level.description);
|
||||
if (description) {
|
||||
task.description = description;
|
||||
@@ -676,25 +688,29 @@ export class AsyncTaskRuntime {
|
||||
* ends first, or when the prompt result ends the model turn
|
||||
* ({@link releaseHeld}). No guess by the command text binds a task.
|
||||
*
|
||||
* A task that a `Monitor` tool call started is ignored: Monitor streams its
|
||||
* output to the model only, so the task has nothing for the client. The
|
||||
* tool_use streams before the tool runs, so the tool name is known when the
|
||||
* spawn has the tool call id.
|
||||
* A task that a `Monitor` tool call started is published as a monitor. The
|
||||
* SDK reports it as `local_bash`, so the adapter names it a monitor instead of
|
||||
* a shell. It has no transcript card because Monitor streams its output to
|
||||
* the model only, and no Stop because stopping it only ends the agent's wait
|
||||
* (the agent's own TaskStop still works). The tool_use streams before the
|
||||
* tool runs, so the tool name is known when the spawn has the tool call id.
|
||||
*/
|
||||
private async announce(
|
||||
task: AsyncTask,
|
||||
options: { withoutToolCall?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (task.announced || task.ignored) return;
|
||||
const toolName = task.toolCallId ? this.options.toolNameOf?.(task.toolCallId) : undefined;
|
||||
if (toolName !== undefined && HIDDEN_TASK_TOOLS.has(toolName)) {
|
||||
this.ignore(task);
|
||||
return;
|
||||
}
|
||||
if (!task.toolCallId && !options.withoutToolCall && !isTerminal(task.state)) {
|
||||
task.held = true;
|
||||
return;
|
||||
}
|
||||
if (task.toolCallId && this.options.toolNameOf?.(task.toolCallId) === "Monitor") {
|
||||
task.monitor = true;
|
||||
}
|
||||
if (task.monitor) {
|
||||
task.taskType = "monitor";
|
||||
task.showInTranscript = false;
|
||||
}
|
||||
if (task.toolCallId) task.route ??= this.options.routeOf?.(task.toolCallId);
|
||||
await this.send(task, {
|
||||
sessionUpdate: "async_task_spawned",
|
||||
@@ -703,7 +719,7 @@ export class AsyncTaskRuntime {
|
||||
taskType: task.taskType,
|
||||
description: task.description,
|
||||
showInTranscript: task.showInTranscript,
|
||||
canStop: true,
|
||||
canStop: !task.monitor,
|
||||
...(task.outputFilePath ? { outputFilePath: task.outputFilePath } : {}),
|
||||
...(task.toolCallId ? { toolCallId: task.toolCallId } : {}),
|
||||
});
|
||||
@@ -1051,13 +1067,6 @@ function isBackgroundTask(isBackgrounded: unknown, taskType: unknown): boolean {
|
||||
return taskType !== "local_bash" && taskType !== "local_agent";
|
||||
}
|
||||
|
||||
/** The tools whose async tasks the client never sees. */
|
||||
const HIDDEN_TASK_TOOLS = new Set(["Monitor"]);
|
||||
|
||||
function isMonitorTask(message: AsyncTaskStarted): boolean {
|
||||
return field(message, "taskType", "task_type") === "local_monitor";
|
||||
}
|
||||
|
||||
function isSubagentTask(message: AsyncTaskStarted): boolean {
|
||||
return (
|
||||
field(message, "subagentType", "subagent_type") !== undefined ||
|
||||
|
||||
@@ -2980,7 +2980,16 @@ describe("task notification replay", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends nothing for a task that a Monitor tool call started", async () => {
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. A replayed transcript has a Monitor tool call that started a background task.
|
||||
* 2. The task notification completes it.
|
||||
* Expected:
|
||||
* - The task spawn is a monitor with no transcript card and no Stop.
|
||||
* - The task then reaches the completed state.
|
||||
* - The Monitor tool call itself stays in the transcript.
|
||||
*/
|
||||
it("replays a task that a Monitor tool call started as a monitor", async () => {
|
||||
const monitorLaunch = record({
|
||||
...bashLaunch[1],
|
||||
message: {
|
||||
@@ -3002,7 +3011,18 @@ describe("task notification replay", () => {
|
||||
notification("completed"),
|
||||
]);
|
||||
|
||||
expect(asyncTaskUpdates(updates)).toEqual([]);
|
||||
const taskUpdates = asyncTaskUpdates(updates);
|
||||
expect(taskUpdates[0]).toMatchObject({
|
||||
sessionUpdate: "async_task_spawned",
|
||||
taskType: "monitor",
|
||||
showInTranscript: false,
|
||||
canStop: false,
|
||||
toolCallId: "toolu_bash",
|
||||
});
|
||||
expect(taskUpdates.at(-1)).toMatchObject({
|
||||
sessionUpdate: "async_task_state_update",
|
||||
state: "completed",
|
||||
});
|
||||
// The Monitor tool call itself stays in the transcript.
|
||||
expect(
|
||||
updates.some(
|
||||
@@ -9731,7 +9751,18 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
expect(backgrounded("child-cat")).toBe(false);
|
||||
});
|
||||
|
||||
it("hides the async task of a Monitor tool call and keeps the tool call", async () => {
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. The model calls Monitor twice; the SDK reports both tasks as `local_bash`.
|
||||
* 2. The first task has its tool call id at start, the second only in a later progress.
|
||||
* 3. The first task completes; the client asks to stop the second.
|
||||
* Expected:
|
||||
* - Both tasks are spawned as monitors with no transcript card and no Stop.
|
||||
* - The first task reaches the completed state.
|
||||
* - The stop request returns `stopped: false`.
|
||||
* - Both Monitor tool calls stay in the transcript.
|
||||
*/
|
||||
it("publishes the async task of a Monitor tool call as a monitor and keeps the tool call", async () => {
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
{
|
||||
@@ -9835,9 +9866,25 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
|
||||
|
||||
expect(updates.filter(({ update }) => update.sessionUpdate.startsWith("async_task_"))).toEqual(
|
||||
[],
|
||||
);
|
||||
const taskUpdates = updates
|
||||
.map(({ update }) => update)
|
||||
.filter((update) => update.sessionUpdate.startsWith("async_task_"));
|
||||
const spawns = taskUpdates.filter((update) => update.sessionUpdate === "async_task_spawned");
|
||||
expect(spawns.map((update: any) => [update.asyncTaskId, update.toolCallId])).toEqual([
|
||||
["mon-a", "monitor-a"],
|
||||
["mon-b", "monitor-b"],
|
||||
]);
|
||||
for (const spawn of spawns) {
|
||||
expect(spawn).toMatchObject({ taskType: "monitor", showInTranscript: false, canStop: false });
|
||||
}
|
||||
expect(
|
||||
taskUpdates.some(
|
||||
(update: any) =>
|
||||
update.sessionUpdate === "async_task_state_update" &&
|
||||
update.asyncTaskId === "mon-a" &&
|
||||
update.state === "completed",
|
||||
),
|
||||
).toBe(true);
|
||||
await expect(
|
||||
agent.stopAsyncTask({ sessionId: "test-session", asyncTaskId: "mon-b" }),
|
||||
).resolves.toEqual({ stopped: false });
|
||||
@@ -10020,6 +10067,15 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
expect(bashUpdate?._meta ?? {}).not.toHaveProperty("jetbrains");
|
||||
});
|
||||
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. A subagent starts Bash tasks and a Monitor task; the root session starts one Bash task.
|
||||
* 2. The client stops a Bash task and the Monitor task.
|
||||
* Expected:
|
||||
* - Subagent tasks, including the monitor, go to the subagent session; the root task stays in the root.
|
||||
* - The monitor is spawned with no transcript card and no Stop.
|
||||
* - Only the Bash task is stopped; the Monitor stop returns `stopped: false`.
|
||||
*/
|
||||
it("sends the background tasks of a subagent to its session and keeps a root task in the root", async () => {
|
||||
const updates: AcpSessionNotification[] = [];
|
||||
const agent = new ClaudeAcpAgent(
|
||||
@@ -10155,7 +10211,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
await expect(
|
||||
agent.stopAsyncTask({ sessionId: "test-session", asyncTaskId: "watch-child" }),
|
||||
).resolves.toEqual({ stopped: true });
|
||||
// The client never sees a Monitor task, so it cannot stop it.
|
||||
// A Monitor task is published with canStop false, so the stop request is refused.
|
||||
await expect(
|
||||
agent.stopAsyncTask({ sessionId: "test-session", asyncTaskId: "monitor-child" }),
|
||||
).resolves.toEqual({ stopped: false });
|
||||
@@ -10172,6 +10228,7 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
expect(lifecycle).toEqual([
|
||||
["agent-1", "async_task_spawned", "shell-child", undefined],
|
||||
["agent-1", "async_task_spawned", "watch-child", undefined],
|
||||
["agent-1", "async_task_spawned", "monitor-child", undefined],
|
||||
["agent-1", "async_task_spawned", "shell-late", undefined],
|
||||
["test-session", "async_task_spawned", "shell-root", undefined],
|
||||
["agent-1", "async_task_state_update", "shell-child", "completed"],
|
||||
@@ -10181,6 +10238,15 @@ describe("subagent permission attribution (issue #851)", () => {
|
||||
["agent-1", "async_task_spawned", "shell-held", undefined],
|
||||
["agent-1", "async_task_state_update", "shell-held", "completed"],
|
||||
]);
|
||||
const monitorSpawn = updates.find(
|
||||
({ update }) =>
|
||||
update.sessionUpdate === "async_task_spawned" && update.asyncTaskId === "monitor-child",
|
||||
)?.update;
|
||||
expect(monitorSpawn).toMatchObject({
|
||||
taskType: "monitor",
|
||||
showInTranscript: false,
|
||||
canStop: false,
|
||||
});
|
||||
// The stop acknowledgement goes to the transcript that holds the task.
|
||||
const stopAcknowledgement = updates.find(
|
||||
({ update }) =>
|
||||
|
||||
@@ -1221,10 +1221,28 @@ describe("AsyncTaskRuntime", () => {
|
||||
return { runtime, published };
|
||||
};
|
||||
|
||||
it("never announces a task that a Monitor tool call started", async () => {
|
||||
const updateKinds = (published: AcpSessionNotification[]) =>
|
||||
published.map(({ update }) => update.sessionUpdate);
|
||||
const MONITOR_SPAWN = {
|
||||
sessionUpdate: "async_task_spawned",
|
||||
taskType: "monitor",
|
||||
showInTranscript: false,
|
||||
canStop: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. The SDK starts a `local_bash` task for a Monitor tool call.
|
||||
* 2. The task reports progress, then its notification completes it.
|
||||
* 3. The client asks to stop the task.
|
||||
* Expected:
|
||||
* - The spawn is a monitor with no transcript card and no Stop.
|
||||
* - Progress and the completed state follow.
|
||||
* - `canStop` and `claimStop` are false.
|
||||
*/
|
||||
it("publishes a task that a Monitor tool call started as a monitor", async () => {
|
||||
const { runtime, published } = monitorRuntime();
|
||||
|
||||
// The SDK reports a Monitor task as a background shell.
|
||||
await runtime.taskStarted({
|
||||
task_id: "monitor",
|
||||
task_type: "local_bash",
|
||||
@@ -1233,22 +1251,38 @@ describe("AsyncTaskRuntime", () => {
|
||||
tool_use_id: "monitor-tool",
|
||||
});
|
||||
await runtime.taskProgress({ task_id: "monitor", summary: "First line" });
|
||||
await runtime.taskUpdated("monitor", { status: "paused" });
|
||||
await runtime.backgroundTasksChanged([{ task_id: "monitor", task_type: "local_bash" }]);
|
||||
expect(runtime.canStop("monitor")).toBe(false);
|
||||
expect(runtime.claimStop("monitor")).toBe(false);
|
||||
await runtime.taskStopped("monitor");
|
||||
const stoppable = runtime.canStop("monitor");
|
||||
const claimed = runtime.claimStop("monitor");
|
||||
await runtime.taskNotification({
|
||||
task_id: "monitor",
|
||||
status: "completed",
|
||||
tool_use_id: "monitor-tool",
|
||||
});
|
||||
await runtime.finishAll("stopped");
|
||||
|
||||
expect(published).toEqual([]);
|
||||
expect(published[0]?.update).toMatchObject({
|
||||
...MONITOR_SPAWN,
|
||||
asyncTaskId: "monitor",
|
||||
toolCallId: "monitor-tool",
|
||||
});
|
||||
expect(updateKinds(published)).toEqual([
|
||||
"async_task_spawned",
|
||||
"async_task_progress",
|
||||
"async_task_state_update",
|
||||
]);
|
||||
expect(published[2]?.update).toMatchObject({ state: "completed" });
|
||||
expect(stoppable).toBe(false);
|
||||
expect(claimed).toBe(false);
|
||||
});
|
||||
|
||||
it("never announces a held task whose tool call id names a Monitor", async () => {
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. A task starts without a tool call id and is held.
|
||||
* 2. A later progress carries the Monitor tool call id.
|
||||
* Expected:
|
||||
* - The task is spawned as a monitor once the id arrives, not before.
|
||||
* - It is not stoppable.
|
||||
*/
|
||||
it("spawns a held task as a monitor when its tool call id names a Monitor", async () => {
|
||||
const { runtime, published } = monitorRuntime();
|
||||
|
||||
await runtime.taskStarted({
|
||||
@@ -1257,18 +1291,26 @@ describe("AsyncTaskRuntime", () => {
|
||||
description: "Watch the logs",
|
||||
});
|
||||
await runtime.taskProgress({ task_id: "monitor", summary: "First line" });
|
||||
const spawnedBeforeId = published.length;
|
||||
await runtime.taskProgress({
|
||||
task_id: "monitor",
|
||||
summary: "Second line",
|
||||
tool_use_id: "monitor-tool",
|
||||
});
|
||||
await runtime.releaseHeld();
|
||||
await runtime.taskNotification({ task_id: "monitor", status: "stopped" });
|
||||
|
||||
expect(published).toEqual([]);
|
||||
expect(spawnedBeforeId).toBe(0);
|
||||
expect(published[0]?.update).toMatchObject({ ...MONITOR_SPAWN, toolCallId: "monitor-tool" });
|
||||
expect(runtime.canStop("monitor")).toBe(false);
|
||||
});
|
||||
|
||||
it("never announces a held task that a Monitor ends", async () => {
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. A task starts without a tool call id and is held.
|
||||
* 2. A notification with the Monitor tool call id completes it.
|
||||
* Expected:
|
||||
* - The task is spawned as a monitor, then reaches the completed state.
|
||||
*/
|
||||
it("spawns a held task that a Monitor ends as a monitor", async () => {
|
||||
const { runtime, published } = monitorRuntime();
|
||||
|
||||
await runtime.taskStarted({ task_id: "monitor", task_type: "local_workflow" });
|
||||
@@ -1278,23 +1320,47 @@ describe("AsyncTaskRuntime", () => {
|
||||
tool_use_id: "monitor-tool",
|
||||
});
|
||||
|
||||
expect(published).toEqual([]);
|
||||
expect(updateKinds(published)).toEqual(["async_task_spawned", "async_task_state_update"]);
|
||||
expect(published[0]?.update).toMatchObject(MONITOR_SPAWN);
|
||||
expect(published[1]?.update).toMatchObject({ state: "completed" });
|
||||
});
|
||||
|
||||
it("ignores a local_monitor task without a tool call", async () => {
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. The SDK starts a `local_monitor` task for a Bash tool call.
|
||||
* 2. The SDK reports another `local_monitor` task only in the task level.
|
||||
* 3. The turn ends (`releaseHeld`), which spawns the level-only task without a tool call.
|
||||
* Expected:
|
||||
* - Both tasks are spawned as monitors and neither is stoppable.
|
||||
*/
|
||||
it("spawns a local_monitor task as a monitor", async () => {
|
||||
const { runtime, published } = monitorRuntime();
|
||||
|
||||
await runtime.backgroundTasksChanged([{ task_id: "level", task_type: "local_monitor" }]);
|
||||
await runtime.taskStarted({ task_id: "level", task_type: "local_monitor" });
|
||||
await runtime.taskStarted({ task_id: "started", task_type: "local_monitor" });
|
||||
await runtime.taskProgress({ task_id: "started", summary: "line" });
|
||||
await runtime.taskStarted({
|
||||
task_id: "started",
|
||||
task_type: "local_monitor",
|
||||
tool_use_id: "bash-tool",
|
||||
});
|
||||
await runtime.backgroundTasksChanged([
|
||||
{ task_id: "started", task_type: "local_monitor" },
|
||||
{ task_id: "level", task_type: "local_monitor" },
|
||||
]);
|
||||
await runtime.releaseHeld();
|
||||
await runtime.taskNotification({ task_id: "started", status: "completed" });
|
||||
|
||||
expect(published).toEqual([]);
|
||||
const spawns = published.map(({ update }) => update);
|
||||
expect(spawns).toHaveLength(2);
|
||||
expect(spawns[0]).toMatchObject({ ...MONITOR_SPAWN, asyncTaskId: "started" });
|
||||
expect(spawns[1]).toMatchObject({ ...MONITOR_SPAWN, asyncTaskId: "level" });
|
||||
expect(runtime.canStop("started")).toBe(false);
|
||||
expect(runtime.canStop("level")).toBe(false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Scenario:
|
||||
* 1. The SDK starts a background `local_bash` task for a Bash tool call.
|
||||
* Expected:
|
||||
* - The spawn is a shell that stays in the transcript and can be stopped.
|
||||
*/
|
||||
it("still announces a task of another tool", async () => {
|
||||
const { runtime, published } = monitorRuntime();
|
||||
|
||||
@@ -1307,6 +1373,11 @@ describe("AsyncTaskRuntime", () => {
|
||||
});
|
||||
|
||||
expect(published.map(({ update }) => update.sessionUpdate)).toEqual(["async_task_spawned"]);
|
||||
expect(published[0]?.update).toMatchObject({
|
||||
taskType: "shell",
|
||||
showInTranscript: true,
|
||||
canStop: true,
|
||||
});
|
||||
expect(runtime.canStop("shell")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user