@agentclientprotocol/claude-agent-acp 0.81.3-preview.1 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/acp-agent.d.ts +31 -37
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +546 -534
- package/dist/acp-subagents.d.ts +6 -0
- package/dist/acp-subagents.d.ts.map +1 -1
- package/dist/air-extension.d.ts +31 -1
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +35 -1
- package/dist/async-tasks.d.ts +39 -2
- package/dist/async-tasks.d.ts.map +1 -1
- package/dist/async-tasks.js +147 -44
- package/dist/context-compaction-meta.d.ts +6 -5
- package/dist/context-compaction-meta.d.ts.map +1 -1
- package/dist/context-compaction-meta.js +10 -10
- package/dist/context-compaction.d.ts +3 -0
- package/dist/context-compaction.d.ts.map +1 -1
- package/dist/context-compaction.js +23 -57
- package/dist/diff.d.ts +73 -5
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +435 -64
- package/dist/elicitation.d.ts +1 -1
- package/dist/elicitation.d.ts.map +1 -1
- package/dist/elicitation.js +12 -14
- package/dist/native-subagents.d.ts +58 -0
- package/dist/native-subagents.d.ts.map +1 -1
- package/dist/native-subagents.js +186 -34
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +6 -0
- package/dist/permissions/presentation.d.ts +13 -2
- package/dist/permissions/presentation.d.ts.map +1 -1
- package/dist/permissions/presentation.js +49 -44
- package/dist/resumed-session.d.ts +11 -0
- package/dist/resumed-session.d.ts.map +1 -1
- package/dist/resumed-session.js +106 -4
- package/dist/session-mode.d.ts +2 -0
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +12 -5
- package/dist/settings.js +2 -2
- package/dist/subagent-history.d.ts +20 -0
- package/dist/subagent-history.d.ts.map +1 -0
- package/dist/subagent-history.js +46 -0
- package/dist/tool-calls/background.d.ts +20 -0
- package/dist/tool-calls/background.d.ts.map +1 -0
- package/dist/tool-calls/background.js +41 -0
- package/dist/tool-calls/changed-meta-filter.d.ts +27 -0
- package/dist/tool-calls/changed-meta-filter.d.ts.map +1 -0
- package/dist/tool-calls/changed-meta-filter.js +102 -0
- package/dist/tool-calls/client-capabilities.d.ts +51 -0
- package/dist/tool-calls/client-capabilities.d.ts.map +1 -0
- package/dist/tool-calls/client-capabilities.js +43 -0
- package/dist/tool-calls/content.d.ts +59 -0
- package/dist/tool-calls/content.d.ts.map +1 -0
- package/dist/tool-calls/content.js +194 -0
- package/dist/tool-calls/facts.d.ts +116 -0
- package/dist/tool-calls/facts.d.ts.map +1 -0
- package/dist/tool-calls/facts.js +1 -0
- package/dist/tool-calls/field-tracker.d.ts +58 -0
- package/dist/tool-calls/field-tracker.d.ts.map +1 -0
- package/dist/tool-calls/field-tracker.js +167 -0
- package/dist/tool-calls/renderer.d.ts +216 -0
- package/dist/tool-calls/renderer.d.ts.map +1 -0
- package/dist/tool-calls/renderer.js +487 -0
- package/dist/tool-calls/reporters/agent.d.ts +7 -0
- package/dist/tool-calls/reporters/agent.d.ts.map +1 -0
- package/dist/tool-calls/reporters/agent.js +190 -0
- package/dist/tool-calls/reporters/bash.d.ts +9 -0
- package/dist/tool-calls/reporters/bash.d.ts.map +1 -0
- package/dist/tool-calls/reporters/bash.js +83 -0
- package/dist/tool-calls/reporters/compaction.d.ts +24 -0
- package/dist/tool-calls/reporters/compaction.d.ts.map +1 -0
- package/dist/tool-calls/reporters/compaction.js +66 -0
- package/dist/tool-calls/reporters/file-edit.d.ts +46 -0
- package/dist/tool-calls/reporters/file-edit.d.ts.map +1 -0
- package/dist/tool-calls/reporters/file-edit.js +180 -0
- package/dist/tool-calls/reporters/index.d.ts +4 -0
- package/dist/tool-calls/reporters/index.d.ts.map +1 -0
- package/dist/tool-calls/reporters/index.js +40 -0
- package/dist/tool-calls/reporters/interaction.d.ts +71 -0
- package/dist/tool-calls/reporters/interaction.d.ts.map +1 -0
- package/dist/tool-calls/reporters/interaction.js +222 -0
- package/dist/tool-calls/reporters/read.d.ts +7 -0
- package/dist/tool-calls/reporters/read.d.ts.map +1 -0
- package/dist/tool-calls/reporters/read.js +64 -0
- package/dist/tool-calls/reporters/search.d.ts +10 -0
- package/dist/tool-calls/reporters/search.d.ts.map +1 -0
- package/dist/tool-calls/reporters/search.js +50 -0
- package/dist/tool-calls/reporters/web.d.ts +11 -0
- package/dist/tool-calls/reporters/web.d.ts.map +1 -0
- package/dist/tool-calls/reporters/web.js +42 -0
- package/dist/tools.d.ts +32 -37
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +48 -970
- package/package.json +4 -1
package/dist/acp-agent.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { agent as acpAgent, CreateElicitationResponse, methods, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
-
import { deleteSession, getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { deleteSession, getSessionMessages, getSubagentMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { GOAL_ACTIONS, GOAL_CONTROL_METHOD, GOAL_EXTENSION_VERSION, goalUpdateFromPrompt, parseGoalRequest, toGoalSnapshot, } from "./goal-extension.js";
|
|
4
4
|
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
|
|
5
5
|
import { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, asSdkSessionNotification, clientSupportsSubagents, } from "./acp-subagents.js";
|
|
6
|
-
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, } from "./native-subagents.js";
|
|
7
|
-
import { AIR_ASYNC_TASKS_CAPABILITY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
6
|
+
import { isNativeSubagentControlTool, isNativeSubagentControlUpdate, NativeSubagentRuntime, resumedNativeSubagentId, sendMessageResumePrompt, } from "./native-subagents.js";
|
|
7
|
+
import { AIR_ASYNC_TASKS_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY, AIR_GOAL_KEY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, clientSupportsAirCapability, withAirMeta, } from "./air-extension.js";
|
|
8
8
|
import { AsyncTaskRuntime, backgroundBashTaskFromToolResult, clientSupportsAsyncTasks, } from "./async-tasks.js";
|
|
9
9
|
import { AUTH_STATUS_PROBE_TIMEOUT_MS, AUTH_STATUS_UPDATE_METHOD, authStatusCapability, fromAccountInfo, fromCliStatus, gatewayAuthStatus, mergeAuthStatus, notLoggedInAuthStatus, sameAuthStatus, } from "./auth-status.js";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { existsSync } from "node:fs";
|
|
13
12
|
import * as fs from "node:fs/promises";
|
|
14
|
-
import * as os from "node:os";
|
|
15
13
|
import * as path from "node:path";
|
|
16
14
|
import { promisify } from "node:util";
|
|
17
15
|
import packageJson from "../package.json" with { type: "json" };
|
|
18
16
|
import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, extractRefusalFallbackPrompt, mcpElicitationToCreateRequest, REFUSAL_FALLBACK_DIALOG_KIND, refusalFallbackResultFromResponse, refusalFallbackToCreateRequest, } from "./elicitation.js";
|
|
19
17
|
import { forkSession } from "./fork-session.js";
|
|
20
|
-
import {
|
|
18
|
+
import { subagentHistory } from "./subagent-history.js";
|
|
19
|
+
import { readResumedModel, readResumedSession, } from "./resumed-session.js";
|
|
21
20
|
import { SessionTiming } from "./session-timing.js";
|
|
22
21
|
import { ALLOW_BYPASS, resolvePermissionMode } from "./permissions/modes.js";
|
|
23
22
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
@@ -30,10 +29,15 @@ import { billsClaudeSubscription, claudeLoginRequiredError, claudeSubscriptionNo
|
|
|
30
29
|
import { AGENT_FILE_CHANGE_REPORT_CAPABILITY, agentFileChangeReportMeta, createNativeFileChangeReporter, supportsAgentFileChangeReport, } from "./file-change-audit.js";
|
|
31
30
|
import { ContextCompactionLifecycle, clientSupportsCompactionUpdates, contextCompactionMetadataFromBoundary, isCompactSummaryMessage, } from "./context-compaction.js";
|
|
32
31
|
import { clientSupportsNotices, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
33
|
-
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback,
|
|
34
|
-
import {
|
|
32
|
+
import { applyTaskCreate, applyTaskList, applyTaskUpdate, clearHookCallbacks, completeHookCallback, createPostToolUseHook, hasHookCallback, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, changedTaskPlanEntries, forgetPublishedTaskPlan, unregisterHookCallback, } from "./tools.js";
|
|
33
|
+
import { previewPatchContent } from "./diff.js";
|
|
34
|
+
import { backgroundedBashToolCall } from "./tool-calls/background.js";
|
|
35
|
+
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
|
|
36
|
+
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
|
|
37
|
+
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
|
|
38
|
+
import { AcpToolCallRenderer } from "./tool-calls/renderer.js";
|
|
35
39
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
36
|
-
import { acceptedPlanToolResult, ExitPlanCoordinator,
|
|
40
|
+
import { acceptedPlanToolResult, ExitPlanCoordinator, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
37
41
|
import { parseToolResultMeta } from "./tool-result-meta.js";
|
|
38
42
|
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
|
|
39
43
|
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
|
|
@@ -41,7 +45,6 @@ import { applyAvailableModelsAllowlist, buildModelConfigOption, getAvailableMode
|
|
|
41
45
|
import { buildEffortConfigOption, EFFORT_CONFIG_ID, mergeEffortSettings, settingsEffortForModel, toSdkEffortLevel, } from "./session-effort.js";
|
|
42
46
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
|
43
47
|
export { applyAvailableModelsAllowlist, matchResumedModel, MODEL_CONFIG_ID, resolveModelPreference, } from "./session-model.js";
|
|
44
|
-
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
45
48
|
const execFileAsync = promisify(execFile);
|
|
46
49
|
/** Claude CLI emits this synthetic result when an interrupted cycle ends on
|
|
47
50
|
* queued user input before producing any assistant content. It is a hand-off
|
|
@@ -74,6 +77,27 @@ const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
|
74
77
|
* pre-empt a slow-but-healthy interrupt. */
|
|
75
78
|
const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
|
|
76
79
|
const STRUCTURED_USAGE_TIMEOUT_MS = 5_000;
|
|
80
|
+
/** The number of settled subagents whose parent tool call a session keeps for
|
|
81
|
+
* a later resume (see `resumableSubagents`). */
|
|
82
|
+
const MAX_RESUMABLE_SUBAGENTS = 256;
|
|
83
|
+
/** Removes a settled task from `liveBackgroundTasks`. A subagent keeps its
|
|
84
|
+
* parent tool call in `resumableSubagents` for a later resume. */
|
|
85
|
+
function settleLiveBackgroundTask(session, taskId) {
|
|
86
|
+
const record = session.liveBackgroundTasks.get(taskId);
|
|
87
|
+
if (!record)
|
|
88
|
+
return;
|
|
89
|
+
session.liveBackgroundTasks.delete(taskId);
|
|
90
|
+
if (!record.isSubagent)
|
|
91
|
+
return;
|
|
92
|
+
const resumable = (session.resumableSubagents ??= new Map());
|
|
93
|
+
resumable.delete(taskId);
|
|
94
|
+
resumable.set(taskId, { parentToolUseId: record.parentToolUseId });
|
|
95
|
+
if (resumable.size > MAX_RESUMABLE_SUBAGENTS) {
|
|
96
|
+
const oldest = resumable.keys().next().value;
|
|
97
|
+
if (oldest !== undefined)
|
|
98
|
+
resumable.delete(oldest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
77
101
|
/** Best-effort structured presentation for a local `/usage` turn. The command
|
|
78
102
|
* itself always runs through Claude Code; null tells the consumer to forward
|
|
79
103
|
* its original output unchanged. The timeout prevents an unstable control
|
|
@@ -412,15 +436,95 @@ const SUPPORTED_PROTOCOLS = ["anthropic", "bedrock", "vertex"];
|
|
|
412
436
|
const PROVIDER_ID = "main";
|
|
413
437
|
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
414
438
|
const DEFAULT_VERTEX_BASE_URL = "https://aiplatform.googleapis.com";
|
|
439
|
+
/**
|
|
440
|
+
* The blocks of a consolidated assistant message without the text that
|
|
441
|
+
* already streamed as deltas.
|
|
442
|
+
*
|
|
443
|
+
* Each assembled text/thinking block is diffed against the streamed blocks in
|
|
444
|
+
* document order: nothing is left if it streamed in full (the common case),
|
|
445
|
+
* the whole block if it never streamed (a non-streaming gateway), and just the
|
|
446
|
+
* tail if the stream was cut short mid-block. Matching on content rather than
|
|
447
|
+
* the message id keeps the dedupe robust for gateways without a stable id.
|
|
448
|
+
* Tool-use and other blocks pass through untouched.
|
|
449
|
+
*/
|
|
450
|
+
function unstreamedRemainder(blocks, streamedBlocks) {
|
|
451
|
+
const kept = [];
|
|
452
|
+
let streamPos = 0;
|
|
453
|
+
for (const item of blocks) {
|
|
454
|
+
if (item.type !== "text" && item.type !== "thinking") {
|
|
455
|
+
kept.push(item);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const block = item;
|
|
459
|
+
const full = (item.type === "text" ? block.text : block.thinking) ?? "";
|
|
460
|
+
// Empty assembled blocks carry nothing: drop them.
|
|
461
|
+
if (full.length === 0)
|
|
462
|
+
continue;
|
|
463
|
+
// A streamed block of the same type whose text is a prefix of this one
|
|
464
|
+
// was already delivered, at least partly. A non-empty streamed text is
|
|
465
|
+
// required so an empty or aborted streamed block does not swallow it.
|
|
466
|
+
const streamed = streamedBlocks[streamPos];
|
|
467
|
+
if (streamed &&
|
|
468
|
+
streamed.type === item.type &&
|
|
469
|
+
streamed.text.length > 0 &&
|
|
470
|
+
full.startsWith(streamed.text)) {
|
|
471
|
+
streamPos++;
|
|
472
|
+
const remainder = full.slice(streamed.text.length);
|
|
473
|
+
if (remainder.length === 0)
|
|
474
|
+
continue;
|
|
475
|
+
kept.push({ ...item, [item.type === "text" ? "text" : "thinking"]: remainder });
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
kept.push(item);
|
|
479
|
+
}
|
|
480
|
+
return kept;
|
|
481
|
+
}
|
|
482
|
+
/** Attributes a report to the Agent/Task tool call of the subagent that made it. */
|
|
483
|
+
function stampParentToolUseId(update, parentToolUseId) {
|
|
484
|
+
update._meta = {
|
|
485
|
+
...update._meta,
|
|
486
|
+
claudeCode: {
|
|
487
|
+
...(update._meta?.claudeCode ?? {}),
|
|
488
|
+
parentToolUseId,
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
415
492
|
const SUBAGENT_TRANSCRIPT_CAPABILITY = "subagent-transcript";
|
|
416
493
|
function supportsSubagentTranscript(capabilities) {
|
|
417
494
|
return capabilities?._meta?.[SUBAGENT_TRANSCRIPT_CAPABILITY] === true;
|
|
418
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* The number of sessions in one page of session/list. The SDK scans the whole
|
|
498
|
+
* project directory for each page, so a page holds all sessions of a usual
|
|
499
|
+
* project.
|
|
500
|
+
*/
|
|
501
|
+
const SESSION_LIST_PAGE_SIZE = 1000;
|
|
502
|
+
/** The offset that a session/list cursor names. */
|
|
503
|
+
function sessionListOffset(cursor) {
|
|
504
|
+
if (cursor === null || cursor === undefined)
|
|
505
|
+
return 0;
|
|
506
|
+
const match = /^offset:(\d+)$/.exec(cursor);
|
|
507
|
+
if (!match)
|
|
508
|
+
throw RequestError.invalidParams(undefined, `Unknown session/list cursor: ${cursor}`);
|
|
509
|
+
return Number(match[1]);
|
|
510
|
+
}
|
|
419
511
|
function parentToolUseIdOf(message) {
|
|
420
512
|
if (!("parent_tool_use_id" in message))
|
|
421
513
|
return null;
|
|
422
514
|
return typeof message.parent_tool_use_id === "string" ? message.parent_tool_use_id : null;
|
|
423
515
|
}
|
|
516
|
+
/** The ids of the Agent and Task tool uses in the content of a message. */
|
|
517
|
+
function subagentLaunchIds(content) {
|
|
518
|
+
if (!Array.isArray(content))
|
|
519
|
+
return [];
|
|
520
|
+
return content.flatMap((block) => typeof block === "object" &&
|
|
521
|
+
block !== null &&
|
|
522
|
+
block.type === "tool_use" &&
|
|
523
|
+
typeof block.id === "string" &&
|
|
524
|
+
isNativeSubagentControlTool(block.name)
|
|
525
|
+
? [block.id]
|
|
526
|
+
: []);
|
|
527
|
+
}
|
|
424
528
|
function replaySubagentTerminalState(block) {
|
|
425
529
|
if (block.is_error !== true)
|
|
426
530
|
return "completed";
|
|
@@ -457,11 +561,19 @@ function stripSubagentTextAndThinking(content) {
|
|
|
457
561
|
* that sit at the top level of the input object — everything before such a
|
|
458
562
|
* comma is a set of complete fields. Returns true once the input object's
|
|
459
563
|
* closing brace arrives.
|
|
564
|
+
*
|
|
565
|
+
* The lexer reads the fragment itself, never `partialJson` by index. V8 keeps a
|
|
566
|
+
* string built with `+=` as a chain of parts, and the first index access
|
|
567
|
+
* copies the whole chain into one flat string. An index scan of `partialJson`
|
|
568
|
+
* would copy the whole input on each delta, so a large Write would cost
|
|
569
|
+
* quadratic time. A test in `acp-agent.test.ts` checks this.
|
|
460
570
|
*/
|
|
461
|
-
function scanStreamedToolInput(state) {
|
|
571
|
+
function scanStreamedToolInput(state, fragment) {
|
|
572
|
+
const offset = state.partialJson.length;
|
|
573
|
+
state.partialJson += fragment;
|
|
462
574
|
let complete = false;
|
|
463
|
-
for (let index =
|
|
464
|
-
const character =
|
|
575
|
+
for (let index = 0; index < fragment.length; index++) {
|
|
576
|
+
const character = fragment[index];
|
|
465
577
|
if (state.inString) {
|
|
466
578
|
if (state.escaped) {
|
|
467
579
|
state.escaped = false;
|
|
@@ -493,10 +605,9 @@ function scanStreamedToolInput(state) {
|
|
|
493
605
|
state.arrayDepth--;
|
|
494
606
|
}
|
|
495
607
|
else if (character === "," && state.objectDepth === 1 && state.arrayDepth === 0) {
|
|
496
|
-
state.lastTopLevelComma = index;
|
|
608
|
+
state.lastTopLevelComma = offset + index;
|
|
497
609
|
}
|
|
498
610
|
}
|
|
499
|
-
state.scannedTo = state.partialJson.length;
|
|
500
611
|
return complete;
|
|
501
612
|
}
|
|
502
613
|
/** Parse the complete top-level fields before a top-level comma by closing the
|
|
@@ -758,6 +869,47 @@ class ClientConnection {
|
|
|
758
869
|
return this.ctx.notify(method, params);
|
|
759
870
|
}
|
|
760
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* The client that the agent talks to. For an AIR client, every session update
|
|
874
|
+
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
|
|
875
|
+
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
|
|
876
|
+
* merges only the top-level tool call fields, not the `_meta` keys. So another
|
|
877
|
+
* client gets the full `_meta` on each update.
|
|
878
|
+
*/
|
|
879
|
+
class ChangedMetaClient {
|
|
880
|
+
inner;
|
|
881
|
+
airClient;
|
|
882
|
+
filter = new ChangedMetaFilter();
|
|
883
|
+
constructor(inner, airClient) {
|
|
884
|
+
this.inner = inner;
|
|
885
|
+
this.airClient = airClient;
|
|
886
|
+
}
|
|
887
|
+
async sessionUpdate(params) {
|
|
888
|
+
if (!this.airClient())
|
|
889
|
+
return this.inner.sessionUpdate(params);
|
|
890
|
+
const update = this.filter.apply(params.update);
|
|
891
|
+
if (update)
|
|
892
|
+
await this.inner.sessionUpdate({ ...params, update });
|
|
893
|
+
}
|
|
894
|
+
requestPermission(params, signal) {
|
|
895
|
+
return this.inner.requestPermission(params, signal);
|
|
896
|
+
}
|
|
897
|
+
readTextFile(params) {
|
|
898
|
+
return this.inner.readTextFile(params);
|
|
899
|
+
}
|
|
900
|
+
writeTextFile(params) {
|
|
901
|
+
return this.inner.writeTextFile(params);
|
|
902
|
+
}
|
|
903
|
+
createElicitation(params, signal) {
|
|
904
|
+
return this.inner.createElicitation(params, signal);
|
|
905
|
+
}
|
|
906
|
+
completeElicitation(params) {
|
|
907
|
+
return this.inner.completeElicitation(params);
|
|
908
|
+
}
|
|
909
|
+
extNotification(method, params) {
|
|
910
|
+
return this.inner.extNotification(method, params);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
761
913
|
function raceWithAbort(operation, signal) {
|
|
762
914
|
return new Promise((resolve, reject) => {
|
|
763
915
|
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
@@ -896,6 +1048,8 @@ export class ClaudeAcpAgent {
|
|
|
896
1048
|
sessions;
|
|
897
1049
|
client;
|
|
898
1050
|
clientCapabilities;
|
|
1051
|
+
/** The tool call report choices, read once from {@link clientCapabilities} in `initialize`. */
|
|
1052
|
+
toolCallCapabilities = new ToolCallClientCapabilities();
|
|
899
1053
|
logger;
|
|
900
1054
|
sessionModes;
|
|
901
1055
|
gatewayAuthRequest;
|
|
@@ -929,7 +1083,7 @@ export class ClaudeAcpAgent {
|
|
|
929
1083
|
forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
930
1084
|
constructor(client, logger) {
|
|
931
1085
|
this.sessions = {};
|
|
932
|
-
this.client = client;
|
|
1086
|
+
this.client = new ChangedMetaClient(client, () => this.toolCallCapabilities.air.client);
|
|
933
1087
|
this.logger = logger ?? console;
|
|
934
1088
|
this.exitPlan = new ExitPlanCoordinator({
|
|
935
1089
|
currentSession: (id) => this.sessions[id],
|
|
@@ -951,6 +1105,7 @@ export class ClaudeAcpAgent {
|
|
|
951
1105
|
this.closeQueryStream(session);
|
|
952
1106
|
session.abortController.abort();
|
|
953
1107
|
session.eagerToolCallSessions?.clear();
|
|
1108
|
+
session.toolCallFields?.clear();
|
|
954
1109
|
clearHookCallbacks(id);
|
|
955
1110
|
session.nativeSubagentRuntime?.clear();
|
|
956
1111
|
session.asyncTaskRuntime?.clear();
|
|
@@ -972,6 +1127,7 @@ export class ClaudeAcpAgent {
|
|
|
972
1127
|
});
|
|
973
1128
|
this.sessionModes = new SessionModeManager({
|
|
974
1129
|
getSession: (sessionId) => this.sessions[sessionId],
|
|
1130
|
+
airClient: () => this.toolCallCapabilities.air.client,
|
|
975
1131
|
sessionEndedMessage: SESSION_ENDED_MESSAGE,
|
|
976
1132
|
updateConfigOption: (sessionId, configId, value) => this.updateConfigOption(sessionId, configId, value),
|
|
977
1133
|
sessionUpdate: (params) => this.client.sessionUpdate(params),
|
|
@@ -982,6 +1138,7 @@ export class ClaudeAcpAgent {
|
|
|
982
1138
|
}
|
|
983
1139
|
async initialize(request) {
|
|
984
1140
|
this.clientCapabilities = request.clientCapabilities;
|
|
1141
|
+
this.toolCallCapabilities = ToolCallClientCapabilities.from(request.clientCapabilities);
|
|
985
1142
|
// Learn the auth identity in the background: `initialize` never waits on
|
|
986
1143
|
// the CLI probe, and no snapshot rides in its response. When the probe
|
|
987
1144
|
// lands it calls `setAuthStatus`, which pushes `_auth/status_update` — the
|
|
@@ -1135,16 +1292,19 @@ export class ClaudeAcpAgent {
|
|
|
1135
1292
|
// Top-level `_meta` (sibling of `agentCapabilities`), per the existing ACP
|
|
1136
1293
|
// steering extension contract: advertises the `_session/steering` request
|
|
1137
1294
|
// so clients know they may inject a follow-up into a running turn.
|
|
1295
|
+
// Only AIR gets the AIR capabilities and the goal capability, under
|
|
1296
|
+
// `jetbrains.air`.
|
|
1138
1297
|
_meta: {
|
|
1139
|
-
...
|
|
1298
|
+
...(this.toolCallCapabilities.air.client
|
|
1299
|
+
? withAirMeta(airSessionFailureCapabilityMeta(AGENT_FILE_CHANGE_REPORT_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY, AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, AIR_DIFF_PATCH_CAPABILITY, AIR_PLAN_FILE_CAPABILITY), AIR_GOAL_KEY, {
|
|
1300
|
+
version: GOAL_EXTENSION_VERSION,
|
|
1301
|
+
controlMethod: GOAL_CONTROL_METHOD,
|
|
1302
|
+
actions: [...GOAL_ACTIONS],
|
|
1303
|
+
})
|
|
1304
|
+
: {}),
|
|
1140
1305
|
steering: {
|
|
1141
1306
|
supported: true,
|
|
1142
1307
|
},
|
|
1143
|
-
goal: {
|
|
1144
|
-
version: GOAL_EXTENSION_VERSION,
|
|
1145
|
-
controlMethod: GOAL_CONTROL_METHOD,
|
|
1146
|
-
actions: [...GOAL_ACTIONS],
|
|
1147
|
-
},
|
|
1148
1308
|
},
|
|
1149
1309
|
};
|
|
1150
1310
|
}
|
|
@@ -1198,10 +1358,21 @@ export class ClaudeAcpAgent {
|
|
|
1198
1358
|
}, 0);
|
|
1199
1359
|
return result;
|
|
1200
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* One page of the sessions, newest first. The cursor is the offset of the
|
|
1363
|
+
* page. The SDK then orders the transcripts by their modification time and
|
|
1364
|
+
* reads the start and the end only of the transcripts of the page.
|
|
1365
|
+
*/
|
|
1201
1366
|
async listSessions(params) {
|
|
1202
|
-
const
|
|
1367
|
+
const offset = sessionListOffset(params.cursor);
|
|
1368
|
+
// One more session than the page tells whether a next page exists.
|
|
1369
|
+
const sdkSessions = await listSessions({
|
|
1370
|
+
dir: params.cwd ?? undefined,
|
|
1371
|
+
limit: SESSION_LIST_PAGE_SIZE + 1,
|
|
1372
|
+
offset,
|
|
1373
|
+
});
|
|
1203
1374
|
const sessions = [];
|
|
1204
|
-
for (const session of
|
|
1375
|
+
for (const session of sdkSessions.slice(0, SESSION_LIST_PAGE_SIZE)) {
|
|
1205
1376
|
if (!session.cwd)
|
|
1206
1377
|
continue;
|
|
1207
1378
|
sessions.push({
|
|
@@ -1211,9 +1382,9 @@ export class ClaudeAcpAgent {
|
|
|
1211
1382
|
updatedAt: new Date(session.lastModified).toISOString(),
|
|
1212
1383
|
});
|
|
1213
1384
|
}
|
|
1214
|
-
return
|
|
1215
|
-
sessions,
|
|
1216
|
-
|
|
1385
|
+
return sdkSessions.length > SESSION_LIST_PAGE_SIZE
|
|
1386
|
+
? { sessions, nextCursor: `offset:${offset + SESSION_LIST_PAGE_SIZE}` }
|
|
1387
|
+
: { sessions };
|
|
1217
1388
|
}
|
|
1218
1389
|
/**
|
|
1219
1390
|
* `authenticate` — the legacy gateway methods store a provider override that
|
|
@@ -1811,21 +1982,24 @@ export class ClaudeAcpAgent {
|
|
|
1811
1982
|
if (session) {
|
|
1812
1983
|
session.lastPublishedGoal = goal;
|
|
1813
1984
|
}
|
|
1985
|
+
// The goal is an AIR extension: only AIR gets it.
|
|
1986
|
+
if (!this.toolCallCapabilities.air.client)
|
|
1987
|
+
return;
|
|
1814
1988
|
await this.client.sessionUpdate({
|
|
1815
1989
|
sessionId,
|
|
1816
1990
|
update: {
|
|
1817
1991
|
sessionUpdate: "session_info_update",
|
|
1818
|
-
_meta:
|
|
1992
|
+
_meta: withAirMeta(undefined, AIR_GOAL_KEY, goal),
|
|
1819
1993
|
},
|
|
1820
1994
|
});
|
|
1821
1995
|
}
|
|
1822
1996
|
async publishTaskPlan(sessionId, taskState) {
|
|
1997
|
+
const entries = changedTaskPlanEntries(taskState, this.toolCallCapabilities.air.client);
|
|
1998
|
+
if (!entries)
|
|
1999
|
+
return;
|
|
1823
2000
|
await this.client.sessionUpdate({
|
|
1824
2001
|
sessionId,
|
|
1825
|
-
update: {
|
|
1826
|
-
sessionUpdate: "plan",
|
|
1827
|
-
entries: taskStateToPlanEntries(taskState),
|
|
1828
|
-
},
|
|
2002
|
+
update: { sessionUpdate: "plan", entries },
|
|
1829
2003
|
});
|
|
1830
2004
|
}
|
|
1831
2005
|
async publishGoalFromPrompt(sessionId, prompt, commandUuid) {
|
|
@@ -2025,7 +2199,28 @@ export class ClaudeAcpAgent {
|
|
|
2025
2199
|
// on content rather than the Anthropic message id makes dedupe robust to
|
|
2026
2200
|
// gateways that don't carry a stable/matching id across the stream and the
|
|
2027
2201
|
// consolidated message. Reset after each consolidated message consumes it.
|
|
2028
|
-
|
|
2202
|
+
//
|
|
2203
|
+
// Keyed by the parent tool use of the stream ("" for the top level), so a
|
|
2204
|
+
// subagent message gets the same remainder diff as a top-level message.
|
|
2205
|
+
// The entry of a subagent goes when the subagent finishes.
|
|
2206
|
+
const streamedBlocksByParent = new Map();
|
|
2207
|
+
const streamedBlocksOf = (parentToolUseId) => {
|
|
2208
|
+
const key = parentToolUseId ?? "";
|
|
2209
|
+
let blocks = streamedBlocksByParent.get(key);
|
|
2210
|
+
if (!blocks)
|
|
2211
|
+
streamedBlocksByParent.set(key, (blocks = []));
|
|
2212
|
+
return blocks;
|
|
2213
|
+
};
|
|
2214
|
+
// A client gets the consolidated subagent text when it negotiated the
|
|
2215
|
+
// transcript extension or the `forwardSubagentText` session option. AIR
|
|
2216
|
+
// also gets it with native subagent sessions, and AIR gets no streamed
|
|
2217
|
+
// subagent text without it: nested text then stays internal to the Agent
|
|
2218
|
+
// tool call. Every other client gets the streamed subagent text, like
|
|
2219
|
+
// upstream.
|
|
2220
|
+
const airClient = this.toolCallCapabilities.air.client;
|
|
2221
|
+
const forwardsSubagentText = () => session.forwardSubagentText ||
|
|
2222
|
+
supportsSubagentTranscript(this.clientCapabilities) ||
|
|
2223
|
+
(airClient && clientSupportsSubagents(this.clientCapabilities));
|
|
2029
2224
|
// Tool-use blocks start streaming before their JSON input. Keep the
|
|
2030
2225
|
// partial input per parent message and block index so completed top-level
|
|
2031
2226
|
// fields can refine the pending tool call while it streams. Entries are
|
|
@@ -2054,9 +2249,15 @@ export class ClaudeAcpAgent {
|
|
|
2054
2249
|
* `tool_use_id` so hosts can collapse them; a notice has no lifecycle to
|
|
2055
2250
|
* update, so only the first becomes one. */
|
|
2056
2251
|
const noticedToolUses = new Set();
|
|
2057
|
-
const asyncTasks = (session.asyncTaskRuntime ??= new AsyncTaskRuntime(clientSupportsAsyncTasks(this.clientCapabilities), params.sessionId, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), {
|
|
2252
|
+
const asyncTasks = (session.asyncTaskRuntime ??= new AsyncTaskRuntime(clientSupportsAsyncTasks(this.clientCapabilities), params.sessionId, async (notification) => this.client.sessionUpdate(asSdkSessionNotification(notification)), {
|
|
2253
|
+
notices: supportsNotices,
|
|
2254
|
+
// A task that a subagent tool call started belongs to the child
|
|
2255
|
+
// session of that tool call, like the tool call itself.
|
|
2256
|
+
routeOf: (toolCallId) => session.nativeSubagentRuntime?.routeOfToolCall(toolCallId, session.eagerToolCallSessions?.get(toolCallId)),
|
|
2257
|
+
}));
|
|
2058
2258
|
const compaction = new ContextCompactionLifecycle((notification) => sendUpdate(notification), {
|
|
2059
2259
|
sessionId: params.sessionId,
|
|
2260
|
+
airClient: this.toolCallCapabilities.air.client,
|
|
2060
2261
|
presentation: clientSupportsCompactionUpdates(this.clientCapabilities)
|
|
2061
2262
|
? "compaction_update"
|
|
2062
2263
|
: "tool_call",
|
|
@@ -2180,12 +2381,12 @@ export class ClaudeAcpAgent {
|
|
|
2180
2381
|
lastAssistantWasUsageLimit = false;
|
|
2181
2382
|
lastAssistantFailureTitle = undefined;
|
|
2182
2383
|
lastRefusalExplanation = null;
|
|
2183
|
-
// Do NOT reset currentStreamMessageId or
|
|
2384
|
+
// Do NOT reset currentStreamMessageId or the streamed blocks here. Turn
|
|
2184
2385
|
// activation can fire mid-message (the replayed user echo with
|
|
2185
2386
|
// --replay-user-messages lands between a message's blocks); clearing the
|
|
2186
2387
|
// streamed-content record on activation would drop the blocks that
|
|
2187
2388
|
// streamed before the echo, so the consolidated assistant message would
|
|
2188
|
-
// re-emit them as duplicates.
|
|
2389
|
+
// re-emit them as duplicates. The streamed blocks are bounded instead by being
|
|
2189
2390
|
// cleared when each consolidated message consumes it. #785 stopped
|
|
2190
2391
|
// resetting the streamed-content tracking here but left this line.
|
|
2191
2392
|
stopReason = "end_turn";
|
|
@@ -2233,7 +2434,7 @@ export class ClaudeAcpAgent {
|
|
|
2233
2434
|
continue;
|
|
2234
2435
|
}
|
|
2235
2436
|
if (record.endedPerLevel === "sweep-armed") {
|
|
2236
|
-
session
|
|
2437
|
+
settleLiveBackgroundTask(session, taskId);
|
|
2237
2438
|
}
|
|
2238
2439
|
else {
|
|
2239
2440
|
record.endedPerLevel = "sweep-armed";
|
|
@@ -2423,6 +2624,28 @@ export class ClaudeAcpAgent {
|
|
|
2423
2624
|
turn.usageOriginalOutput = originalOutput;
|
|
2424
2625
|
return markdown;
|
|
2425
2626
|
};
|
|
2627
|
+
/** Registers a live background task (see `liveBackgroundTasks`). A
|
|
2628
|
+
* subagent is also recorded on the active turn, so that turn holds until
|
|
2629
|
+
* the subagent settles (see `Turn.spawnedTaskIds`). */
|
|
2630
|
+
const registerLiveTask = (taskId, parentToolUseId, isSubagent) => {
|
|
2631
|
+
session.liveBackgroundTasks.set(taskId, { parentToolUseId, isSubagent });
|
|
2632
|
+
session.resumableSubagents?.delete(taskId);
|
|
2633
|
+
if (isSubagent && session.activeTurn && !session.activeTurn.settled) {
|
|
2634
|
+
(session.activeTurn.spawnedTaskIds ??= new Set()).add(taskId);
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
const settleLiveTask = (taskId) => settleLiveBackgroundTask(session, taskId);
|
|
2638
|
+
/** Registers a settled subagent again when the SDK resumes it without a
|
|
2639
|
+
* new `task_started`. The active turn is the one that resumed it, so it
|
|
2640
|
+
* holds until the subagent settles again. A repeated resume signal finds
|
|
2641
|
+
* the subagent live and changes nothing. */
|
|
2642
|
+
const resumeLiveTask = (taskId) => {
|
|
2643
|
+
if (session.liveBackgroundTasks.has(taskId))
|
|
2644
|
+
return;
|
|
2645
|
+
const settled = session.resumableSubagents?.get(taskId);
|
|
2646
|
+
if (settled)
|
|
2647
|
+
registerLiveTask(taskId, settled.parentToolUseId, true);
|
|
2648
|
+
};
|
|
2426
2649
|
/** Whether any background subagent this turn spawned is still live —
|
|
2427
2650
|
* while true, the turn's settlement stays deferred so the subagent's
|
|
2428
2651
|
* output and permission requests land inside it (see
|
|
@@ -2881,7 +3104,7 @@ export class ClaudeAcpAgent {
|
|
|
2881
3104
|
// A fresh `system`/init (e.g. after reinitialize) can carry an
|
|
2882
3105
|
// updated Fast mode state; reconcile it with what we seeded at
|
|
2883
3106
|
// session creation.
|
|
2884
|
-
await this.syncFastModeState(
|
|
3107
|
+
await this.syncFastModeState(params.sessionId, session, message.fast_mode_state, message.fast_mode_disabled_reason);
|
|
2885
3108
|
// Terminal-bound slash commands (absent when none, and on
|
|
2886
3109
|
// older CLIs). The session/new advertisement runs before any
|
|
2887
3110
|
// init frame can be observed, so the first latch (or a
|
|
@@ -2891,7 +3114,7 @@ export class ClaudeAcpAgent {
|
|
|
2891
3114
|
JSON.stringify(session.terminalSlashCommands)) {
|
|
2892
3115
|
session.terminalSlashCommands = message.terminal_slash_commands;
|
|
2893
3116
|
try {
|
|
2894
|
-
await this.sendAvailableCommandsUpdate(
|
|
3117
|
+
await this.sendAvailableCommandsUpdate(params.sessionId);
|
|
2895
3118
|
}
|
|
2896
3119
|
catch (error) {
|
|
2897
3120
|
// Advisory reconcile only — the client keeps its current
|
|
@@ -2937,7 +3160,7 @@ export class ClaudeAcpAgent {
|
|
|
2937
3160
|
lastAssistantTotalUsage = usedTokens;
|
|
2938
3161
|
session.contextUsedTokens = usedTokens;
|
|
2939
3162
|
await sendUpdate({
|
|
2940
|
-
sessionId:
|
|
3163
|
+
sessionId: params.sessionId,
|
|
2941
3164
|
update: attachUsageModel({
|
|
2942
3165
|
sessionUpdate: "usage_update",
|
|
2943
3166
|
used: lastAssistantTotalUsage,
|
|
@@ -2957,7 +3180,7 @@ export class ClaudeAcpAgent {
|
|
|
2957
3180
|
if (usageMarkdown === null)
|
|
2958
3181
|
break;
|
|
2959
3182
|
await sendUpdate({
|
|
2960
|
-
sessionId:
|
|
3183
|
+
sessionId: params.sessionId,
|
|
2961
3184
|
update: {
|
|
2962
3185
|
sessionUpdate: "agent_message_chunk",
|
|
2963
3186
|
content: { type: "text", text: usageMarkdown ?? message.content },
|
|
@@ -3106,39 +3329,9 @@ export class ClaudeAcpAgent {
|
|
|
3106
3329
|
break;
|
|
3107
3330
|
}
|
|
3108
3331
|
case "memory_recall": {
|
|
3109
|
-
const isSynthesis = message.mode === "synthesize";
|
|
3110
|
-
const locations = isSynthesis
|
|
3111
|
-
? []
|
|
3112
|
-
: message.memories.map((m) => ({ path: m.path }));
|
|
3113
|
-
const content = isSynthesis
|
|
3114
|
-
? message.memories
|
|
3115
|
-
.filter((m) => typeof m.content === "string")
|
|
3116
|
-
.map((m) => ({
|
|
3117
|
-
type: "content",
|
|
3118
|
-
content: { type: "text", text: m.content },
|
|
3119
|
-
}))
|
|
3120
|
-
: [];
|
|
3121
|
-
const count = message.memories.length;
|
|
3122
|
-
const title = isSynthesis
|
|
3123
|
-
? "Recalled synthesized memory"
|
|
3124
|
-
: `Recalled ${count} ${count === 1 ? "memory" : "memories"}`;
|
|
3125
3332
|
await sendUpdate({
|
|
3126
|
-
sessionId:
|
|
3127
|
-
update:
|
|
3128
|
-
sessionUpdate: "tool_call",
|
|
3129
|
-
toolCallId: message.uuid,
|
|
3130
|
-
title,
|
|
3131
|
-
kind: "read",
|
|
3132
|
-
status: "completed",
|
|
3133
|
-
...(locations.length > 0 && { locations }),
|
|
3134
|
-
...(content.length > 0 && { content }),
|
|
3135
|
-
_meta: {
|
|
3136
|
-
claudeCode: {
|
|
3137
|
-
toolName: "memory_recall",
|
|
3138
|
-
toolResponse: { mode: message.mode },
|
|
3139
|
-
},
|
|
3140
|
-
},
|
|
3141
|
-
},
|
|
3333
|
+
sessionId: params.sessionId,
|
|
3334
|
+
update: new AcpToolCallRenderer(this.toolCallCapabilities).memoryRecall(message),
|
|
3142
3335
|
});
|
|
3143
3336
|
break;
|
|
3144
3337
|
}
|
|
@@ -3150,7 +3343,7 @@ export class ClaudeAcpAgent {
|
|
|
3150
3343
|
// it's authoritative, and re-querying supportedCommands()
|
|
3151
3344
|
// would just return the same list with an extra round-trip.
|
|
3152
3345
|
await sendUpdate({
|
|
3153
|
-
sessionId:
|
|
3346
|
+
sessionId: params.sessionId,
|
|
3154
3347
|
update: {
|
|
3155
3348
|
sessionUpdate: "available_commands_update",
|
|
3156
3349
|
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
|
|
@@ -3205,32 +3398,18 @@ export class ClaudeAcpAgent {
|
|
|
3205
3398
|
// child tool call was not announced there.
|
|
3206
3399
|
break;
|
|
3207
3400
|
}
|
|
3208
|
-
const
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
content: [
|
|
3216
|
-
{
|
|
3217
|
-
type: "content",
|
|
3218
|
-
content: { type: "text", text: `Permission denied: ${reason}` },
|
|
3219
|
-
},
|
|
3220
|
-
],
|
|
3221
|
-
_meta: {
|
|
3222
|
-
claudeCode: {
|
|
3223
|
-
toolName: message.tool_name,
|
|
3224
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
3225
|
-
toolResponse: {
|
|
3226
|
-
decisionReasonType: message.decision_reason_type,
|
|
3227
|
-
decisionReason: message.decision_reason,
|
|
3228
|
-
message: message.message,
|
|
3229
|
-
},
|
|
3230
|
-
},
|
|
3231
|
-
},
|
|
3232
|
-
},
|
|
3401
|
+
const denied = new AcpToolCallRenderer(this.toolCallCapabilities).permissionDenied({
|
|
3402
|
+
toolCallId: message.tool_use_id,
|
|
3403
|
+
toolName: message.tool_name,
|
|
3404
|
+
parentToolUseId,
|
|
3405
|
+
decisionReasonType: message.decision_reason_type,
|
|
3406
|
+
decisionReason: message.decision_reason,
|
|
3407
|
+
message: message.message,
|
|
3233
3408
|
});
|
|
3409
|
+
// A denial is final, so it replaces a pinned approval patch.
|
|
3410
|
+
if (toolCallFieldsOf(session).apply(denied, { replacePinnedContent: true })) {
|
|
3411
|
+
await sendUpdate({ sessionId: params.sessionId, update: denied });
|
|
3412
|
+
}
|
|
3234
3413
|
break;
|
|
3235
3414
|
}
|
|
3236
3415
|
case "informational": {
|
|
@@ -3265,7 +3444,7 @@ export class ClaudeAcpAgent {
|
|
|
3265
3444
|
? message.content
|
|
3266
3445
|
: `**${sentenceCase(message.level)}:** ${message.content}`;
|
|
3267
3446
|
await sendUpdate({
|
|
3268
|
-
sessionId:
|
|
3447
|
+
sessionId: params.sessionId,
|
|
3269
3448
|
update: noticeOrTranscriptUpdate({
|
|
3270
3449
|
severity,
|
|
3271
3450
|
...splitNoticeText(message.content, severity === "warning"
|
|
@@ -3291,6 +3470,7 @@ export class ClaudeAcpAgent {
|
|
|
3291
3470
|
summary: message.summary,
|
|
3292
3471
|
last_tool_name: message.last_tool_name,
|
|
3293
3472
|
usage: message.usage,
|
|
3473
|
+
tool_use_id: message.tool_use_id,
|
|
3294
3474
|
});
|
|
3295
3475
|
break;
|
|
3296
3476
|
case "task_started":
|
|
@@ -3312,10 +3492,7 @@ export class ClaudeAcpAgent {
|
|
|
3312
3492
|
// turn: a turn only ever waits on its own subagents, and a
|
|
3313
3493
|
// spawn during a held-open drain window (an agent chain)
|
|
3314
3494
|
// extends that turn's hold.
|
|
3315
|
-
|
|
3316
|
-
parentToolUseId: message.tool_use_id,
|
|
3317
|
-
isSubagent: !!message.subagent_type,
|
|
3318
|
-
});
|
|
3495
|
+
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type);
|
|
3319
3496
|
await subagents.taskStarted({
|
|
3320
3497
|
taskId: message.task_id,
|
|
3321
3498
|
toolUseId: message.tool_use_id,
|
|
@@ -3333,9 +3510,6 @@ export class ClaudeAcpAgent {
|
|
|
3333
3510
|
skip_transcript: message.skip_transcript,
|
|
3334
3511
|
tool_use_id: message.tool_use_id,
|
|
3335
3512
|
});
|
|
3336
|
-
if (message.subagent_type && session.activeTurn && !session.activeTurn.settled) {
|
|
3337
|
-
(session.activeTurn.spawnedTaskIds ??= new Set()).add(message.task_id);
|
|
3338
|
-
}
|
|
3339
3513
|
break;
|
|
3340
3514
|
case "task_notification":
|
|
3341
3515
|
// The task settled — no further tool calls can originate
|
|
@@ -3346,10 +3520,14 @@ export class ClaudeAcpAgent {
|
|
|
3346
3520
|
status: message.status,
|
|
3347
3521
|
summary: message.summary,
|
|
3348
3522
|
output_file: message.output_file,
|
|
3523
|
+
tool_use_id: message.tool_use_id,
|
|
3349
3524
|
});
|
|
3350
|
-
if (message.tool_use_id)
|
|
3525
|
+
if (message.tool_use_id) {
|
|
3351
3526
|
subagents.discardPending(message.tool_use_id);
|
|
3352
|
-
|
|
3527
|
+
// The subagent streams no more text under this parent.
|
|
3528
|
+
streamedBlocksByParent.delete(message.tool_use_id);
|
|
3529
|
+
}
|
|
3530
|
+
settleLiveTask(message.task_id);
|
|
3353
3531
|
break;
|
|
3354
3532
|
case "task_updated":
|
|
3355
3533
|
await asyncTasks.taskUpdated(message.task_id, message.patch);
|
|
@@ -3362,7 +3540,17 @@ export class ClaudeAcpAgent {
|
|
|
3362
3540
|
message.patch.status === "failed" ||
|
|
3363
3541
|
message.patch.status === "killed") {
|
|
3364
3542
|
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
3365
|
-
session.liveBackgroundTasks.
|
|
3543
|
+
const parentToolUseId = session.liveBackgroundTasks.get(message.task_id)?.parentToolUseId;
|
|
3544
|
+
if (parentToolUseId)
|
|
3545
|
+
streamedBlocksByParent.delete(parentToolUseId);
|
|
3546
|
+
settleLiveTask(message.task_id);
|
|
3547
|
+
}
|
|
3548
|
+
else if (message.patch.status === "running" ||
|
|
3549
|
+
message.patch.status === "pending") {
|
|
3550
|
+
// The SDK can resume a finished subagent under the same
|
|
3551
|
+
// agent id without a new task_started.
|
|
3552
|
+
resumeLiveTask(message.task_id);
|
|
3553
|
+
await subagents.taskResumed(message.task_id, sendUpdate, sendMessageResumePrompt(session.toolUseCache, message.task_id));
|
|
3366
3554
|
}
|
|
3367
3555
|
break;
|
|
3368
3556
|
case "worker_shutting_down":
|
|
@@ -3480,7 +3668,7 @@ export class ClaudeAcpAgent {
|
|
|
3480
3668
|
...(explanation ? { description: explanation } : {}),
|
|
3481
3669
|
};
|
|
3482
3670
|
await sendUpdate({
|
|
3483
|
-
sessionId:
|
|
3671
|
+
sessionId: params.sessionId,
|
|
3484
3672
|
update: noticeOrTranscriptUpdate({ severity: "warning", ...notice }, supportsNotices, `**Model fallback:** ${fallbackNotice}`),
|
|
3485
3673
|
});
|
|
3486
3674
|
}
|
|
@@ -3562,6 +3750,9 @@ export class ClaudeAcpAgent {
|
|
|
3562
3750
|
}
|
|
3563
3751
|
break;
|
|
3564
3752
|
case "result": {
|
|
3753
|
+
// The result ends the model turn. A background task that still
|
|
3754
|
+
// waits for its tool call id gets its spawn now, without the id.
|
|
3755
|
+
await asyncTasks.releaseHeld();
|
|
3565
3756
|
// A result from an autonomous cycle — a task-notification
|
|
3566
3757
|
// followup, or a peer/coordinator/observer message the model
|
|
3567
3758
|
// handled on its own (see AUTONOMOUS_RESULT_ORIGINS) — is not
|
|
@@ -4130,19 +4321,16 @@ export class ClaudeAcpAgent {
|
|
|
4130
4321
|
// the top-level record. Fires once, before any of this message's
|
|
4131
4322
|
// blocks, so it doesn't disturb the mid-message turn-activation
|
|
4132
4323
|
// path the way resetting on turn activation would.
|
|
4133
|
-
|
|
4134
|
-
streamedBlocks.length = 0;
|
|
4135
|
-
}
|
|
4324
|
+
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4136
4325
|
}
|
|
4137
4326
|
// Accumulate the text/thinking actually streamed live, so the
|
|
4138
4327
|
// `assistant` case below can diff its assembled blocks against what
|
|
4139
4328
|
// already reached the client as chunks and forward only the
|
|
4140
|
-
// remainder.
|
|
4141
|
-
//
|
|
4329
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4330
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
4142
4331
|
// Contiguous deltas of the same block (same index and type) extend
|
|
4143
4332
|
// the current entry; anything else opens a new one.
|
|
4144
|
-
if (message.
|
|
4145
|
-
message.event.type === "content_block_delta") {
|
|
4333
|
+
if (message.event.type === "content_block_delta") {
|
|
4146
4334
|
const delta = message.event.delta;
|
|
4147
4335
|
const chunk = delta.type === "text_delta"
|
|
4148
4336
|
? { type: "text", text: delta.text }
|
|
@@ -4156,6 +4344,7 @@ export class ClaudeAcpAgent {
|
|
|
4156
4344
|
// re-emitting the next block as a duplicate.
|
|
4157
4345
|
if (chunk?.text) {
|
|
4158
4346
|
const index = message.event.index;
|
|
4347
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4159
4348
|
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4160
4349
|
if (last && last.index === index && last.type === chunk.type) {
|
|
4161
4350
|
last.text += chunk.text;
|
|
@@ -4222,9 +4411,19 @@ export class ClaudeAcpAgent {
|
|
|
4222
4411
|
cwd: session.cwd,
|
|
4223
4412
|
taskState: session.taskState,
|
|
4224
4413
|
emittedToolCalls: session.emittedToolCalls,
|
|
4414
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
4225
4415
|
messageId: currentStreamMessageId,
|
|
4226
4416
|
streamedToolInputs,
|
|
4227
4417
|
})) {
|
|
4418
|
+
// Nested text stays internal for an AIR client that does not
|
|
4419
|
+
// get it, like the consolidated subagent message below.
|
|
4420
|
+
if (message.parent_tool_use_id !== null &&
|
|
4421
|
+
airClient &&
|
|
4422
|
+
!forwardsSubagentText() &&
|
|
4423
|
+
(notification.update.sessionUpdate === "agent_message_chunk" ||
|
|
4424
|
+
notification.update.sessionUpdate === "agent_thought_chunk")) {
|
|
4425
|
+
continue;
|
|
4426
|
+
}
|
|
4228
4427
|
// sendUpdate records delivery; a subagent stream's chunks carry
|
|
4229
4428
|
// the stamped parentToolUseId meta and are excluded there.
|
|
4230
4429
|
await sendUpdate(notification);
|
|
@@ -4236,7 +4435,7 @@ export class ClaudeAcpAgent {
|
|
|
4236
4435
|
// Record the ACP messageId -> SDK uuid mapping for this message
|
|
4237
4436
|
// (including replays). The consolidated message carries both ids, so
|
|
4238
4437
|
// this is where we learn the uuid the SDK's rewind/resume APIs key on
|
|
4239
|
-
// for the id we hand clients.
|
|
4438
|
+
// for the id we hand clients. Fork reads it (see messageIdToUuid).
|
|
4240
4439
|
const mappedMessageId = messageIdForGrouping(message);
|
|
4241
4440
|
if (mappedMessageId && typeof message.uuid === "string" && message.uuid.length > 0) {
|
|
4242
4441
|
session.messageIdToUuid.set(mappedMessageId, message.uuid);
|
|
@@ -4457,73 +4656,22 @@ export class ClaudeAcpAgent {
|
|
|
4457
4656
|
break;
|
|
4458
4657
|
}
|
|
4459
4658
|
let content;
|
|
4460
|
-
if (message.type === "assistant" &&
|
|
4461
|
-
|
|
4462
|
-
//
|
|
4463
|
-
//
|
|
4464
|
-
// the un-streamed remainder
|
|
4465
|
-
//
|
|
4466
|
-
//
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
// the streamed/assembled pair) without advancing it.
|
|
4471
|
-
const blocks = message.message.content;
|
|
4472
|
-
const kept = [];
|
|
4473
|
-
let streamPos = 0;
|
|
4474
|
-
for (const item of blocks) {
|
|
4475
|
-
if (item.type !== "text" && item.type !== "thinking") {
|
|
4476
|
-
kept.push(item);
|
|
4477
|
-
continue;
|
|
4478
|
-
}
|
|
4479
|
-
const full = item.type === "text" ? item.text : item.thinking;
|
|
4480
|
-
// Empty assembled blocks carry nothing (some gateways emit an
|
|
4481
|
-
// empty `thinking` block before the real text) — drop them.
|
|
4482
|
-
if (full.length === 0) {
|
|
4483
|
-
continue;
|
|
4484
|
-
}
|
|
4485
|
-
// A streamed block of the same type whose accumulated text is a
|
|
4486
|
-
// prefix of this one was already (at least partly) delivered as
|
|
4487
|
-
// chunks; consume it and forward only what's left. A non-empty
|
|
4488
|
-
// streamed text is required so an empty/aborted streamed block
|
|
4489
|
-
// doesn't swallow the assembled copy.
|
|
4490
|
-
const streamed = streamedBlocks[streamPos];
|
|
4491
|
-
if (streamed &&
|
|
4492
|
-
streamed.type === item.type &&
|
|
4493
|
-
streamed.text.length > 0 &&
|
|
4494
|
-
full.startsWith(streamed.text)) {
|
|
4495
|
-
streamPos++;
|
|
4496
|
-
const remainder = full.slice(streamed.text.length);
|
|
4497
|
-
if (remainder.length === 0) {
|
|
4498
|
-
continue;
|
|
4499
|
-
}
|
|
4500
|
-
// Overwrite in place with just the un-streamed tail (the
|
|
4501
|
-
// assembled message isn't read again after this) so the block
|
|
4502
|
-
// keeps its exact SDK type.
|
|
4503
|
-
if (item.type === "text") {
|
|
4504
|
-
item.text = remainder;
|
|
4505
|
-
}
|
|
4506
|
-
else {
|
|
4507
|
-
item.thinking = remainder;
|
|
4508
|
-
}
|
|
4509
|
-
kept.push(item);
|
|
4510
|
-
continue;
|
|
4511
|
-
}
|
|
4512
|
-
// Not matched: never streamed (or the stream diverged from the
|
|
4513
|
-
// assembled text) — forward the block in full.
|
|
4514
|
-
kept.push(item);
|
|
4515
|
-
}
|
|
4516
|
-
content = kept;
|
|
4517
|
-
// Consumed: reset so the next message's blocks accumulate fresh and
|
|
4518
|
-
// the record stays bounded to the in-flight message.
|
|
4519
|
-
streamedBlocks.length = 0;
|
|
4659
|
+
if (message.type === "assistant" &&
|
|
4660
|
+
(message.parent_tool_use_id === null || forwardsSubagentText())) {
|
|
4661
|
+
// Each text/thinking block may have streamed live as deltas
|
|
4662
|
+
// already, for the top level and for a subagent. Forward only
|
|
4663
|
+
// the un-streamed remainder (see `unstreamedRemainder`), and
|
|
4664
|
+
// reset the record of the stream so the next message starts
|
|
4665
|
+
// fresh.
|
|
4666
|
+
const streamed = streamedBlocksOf(message.parent_tool_use_id);
|
|
4667
|
+
content = unstreamedRemainder(message.message.content, streamed);
|
|
4668
|
+
streamed.length = 0;
|
|
4520
4669
|
}
|
|
4521
|
-
else if (message.type === "assistant"
|
|
4522
|
-
|
|
4523
|
-
//
|
|
4524
|
-
// but nested text/thinking stays internal unless explicitly
|
|
4525
|
-
// requested through the historical transcript extension.
|
|
4670
|
+
else if (message.type === "assistant") {
|
|
4671
|
+
// Nested text/thinking stays internal for a client that does
|
|
4672
|
+
// not get subagent text.
|
|
4526
4673
|
content = message.message.content.filter((item) => item.type !== "text" && item.type !== "thinking");
|
|
4674
|
+
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4527
4675
|
}
|
|
4528
4676
|
else {
|
|
4529
4677
|
content = message.message.content;
|
|
@@ -4534,6 +4682,13 @@ export class ClaudeAcpAgent {
|
|
|
4534
4682
|
backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
4535
4683
|
if (backgroundBashTask)
|
|
4536
4684
|
await asyncTasks.taskBackgrounded(backgroundBashTask);
|
|
4685
|
+
const resumedAgentId = resumedNativeSubagentId(message.tool_use_result);
|
|
4686
|
+
if (resumedAgentId) {
|
|
4687
|
+
resumeLiveTask(resumedAgentId);
|
|
4688
|
+
await subagents.taskResumed(resumedAgentId, sendUpdate, sendMessageResumePrompt(session.toolUseCache, resumedAgentId, Array.isArray(content)
|
|
4689
|
+
? content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
4690
|
+
: []));
|
|
4691
|
+
}
|
|
4537
4692
|
}
|
|
4538
4693
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4539
4694
|
clientCapabilities: this.clientCapabilities,
|
|
@@ -4541,6 +4696,7 @@ export class ClaudeAcpAgent {
|
|
|
4541
4696
|
cwd: session.cwd,
|
|
4542
4697
|
taskState: session.taskState,
|
|
4543
4698
|
emittedToolCalls: session.emittedToolCalls,
|
|
4699
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
4544
4700
|
messageId: messageIdForGrouping(message),
|
|
4545
4701
|
toolUseResult: message.type === "user" ? message.tool_use_result : undefined,
|
|
4546
4702
|
// On the wire since CLI 2.1.216 but not in SDKUserMessage's
|
|
@@ -4582,42 +4738,29 @@ export class ClaudeAcpAgent {
|
|
|
4582
4738
|
? message.parent_tool_use_id
|
|
4583
4739
|
: undefined
|
|
4584
4740
|
: undefined;
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
elapsedTimeSeconds: message.elapsed_time_seconds,
|
|
4599
|
-
// For Agent/Task calls: the subagent's type, and — when
|
|
4600
|
-
// the subagent is waiting out an API rate-limit retry —
|
|
4601
|
-
// the SDK's retry counters (attempt, max_retries,
|
|
4602
|
-
// retry_delay_ms, …), forwarded verbatim so clients can
|
|
4603
|
-
// show why a spawn looks stalled.
|
|
4604
|
-
...(message.subagent_type !== undefined && {
|
|
4605
|
-
subagentType: message.subagent_type,
|
|
4606
|
-
}),
|
|
4607
|
-
...(message.subagent_retry !== undefined && {
|
|
4608
|
-
subagentRetry: message.subagent_retry,
|
|
4609
|
-
}),
|
|
4610
|
-
},
|
|
4611
|
-
},
|
|
4612
|
-
},
|
|
4613
|
-
},
|
|
4741
|
+
// `tool_name` names the tool that reports the beat. When the beat
|
|
4742
|
+
// falls back to the parent call, AIR gets the name of that call,
|
|
4743
|
+
// or no name. Every other client gets `tool_name`, like upstream.
|
|
4744
|
+
const toolName = toolCallId !== message.tool_use_id && this.toolCallCapabilities.air.client
|
|
4745
|
+
? session.toolUseCache[toolCallId]?.name
|
|
4746
|
+
: message.tool_name;
|
|
4747
|
+
const beat = new AcpToolCallRenderer(this.toolCallCapabilities).progress({
|
|
4748
|
+
toolCallId,
|
|
4749
|
+
toolName,
|
|
4750
|
+
parentToolUseId: subagentParentToolUseId,
|
|
4751
|
+
elapsedTimeSeconds: message.elapsed_time_seconds,
|
|
4752
|
+
subagentType: message.subagent_type,
|
|
4753
|
+
subagentRetry: message.subagent_retry,
|
|
4614
4754
|
});
|
|
4755
|
+
if (toolCallFieldsOf(session).apply(beat)) {
|
|
4756
|
+
await sendUpdate({ sessionId: params.sessionId, update: beat });
|
|
4757
|
+
}
|
|
4615
4758
|
break;
|
|
4616
4759
|
}
|
|
4617
4760
|
case "rate_limit_event": {
|
|
4618
4761
|
if (lastAssistantTotalUsage !== null) {
|
|
4619
4762
|
await sendUpdate({
|
|
4620
|
-
sessionId:
|
|
4763
|
+
sessionId: params.sessionId,
|
|
4621
4764
|
update: attachUsageModel({
|
|
4622
4765
|
sessionUpdate: "usage_update",
|
|
4623
4766
|
used: lastAssistantTotalUsage,
|
|
@@ -4637,6 +4780,7 @@ export class ClaudeAcpAgent {
|
|
|
4637
4780
|
subagents.clear();
|
|
4638
4781
|
asyncTasks.clear();
|
|
4639
4782
|
session.eagerToolCallSessions?.clear();
|
|
4783
|
+
session.toolCallFields?.clear();
|
|
4640
4784
|
clearHookCallbacks(params.sessionId);
|
|
4641
4785
|
session.taskState.clear();
|
|
4642
4786
|
await this.publishTaskPlan(params.sessionId, session.taskState);
|
|
@@ -4698,6 +4842,7 @@ export class ClaudeAcpAgent {
|
|
|
4698
4842
|
failAllTurns(RequestError.internalError(undefined, "The Claude Agent process exited unexpectedly. Please start a new session."));
|
|
4699
4843
|
this.closeQueryStream(session);
|
|
4700
4844
|
session.eagerToolCallSessions?.clear();
|
|
4845
|
+
session.toolCallFields?.clear();
|
|
4701
4846
|
clearHookCallbacks(params.sessionId);
|
|
4702
4847
|
session.nativeSubagentRuntime?.clear();
|
|
4703
4848
|
session.asyncTaskRuntime?.clear();
|
|
@@ -4753,6 +4898,7 @@ export class ClaudeAcpAgent {
|
|
|
4753
4898
|
// this fire-and-forget notification, so there is nothing to do here.
|
|
4754
4899
|
if (session.queryClosed) {
|
|
4755
4900
|
session.eagerToolCallSessions?.clear();
|
|
4901
|
+
session.toolCallFields?.clear();
|
|
4756
4902
|
clearHookCallbacks(params.sessionId);
|
|
4757
4903
|
return;
|
|
4758
4904
|
}
|
|
@@ -4769,6 +4915,7 @@ export class ClaudeAcpAgent {
|
|
|
4769
4915
|
}
|
|
4770
4916
|
finally {
|
|
4771
4917
|
session.eagerToolCallSessions?.clear();
|
|
4918
|
+
session.toolCallFields?.clear();
|
|
4772
4919
|
clearHookCallbacks(params.sessionId);
|
|
4773
4920
|
}
|
|
4774
4921
|
// A priority steer may still be queued in the SDK when cancellation
|
|
@@ -5066,6 +5213,7 @@ export class ClaudeAcpAgent {
|
|
|
5066
5213
|
// appropriate; query.close() above has already torn the subprocess down.
|
|
5067
5214
|
session.abortController.abort();
|
|
5068
5215
|
session.eagerToolCallSessions?.clear();
|
|
5216
|
+
session.toolCallFields?.clear();
|
|
5069
5217
|
clearHookCallbacks(sessionId);
|
|
5070
5218
|
session.nativeSubagentRuntime?.clear();
|
|
5071
5219
|
session.asyncTaskRuntime?.clear();
|
|
@@ -5183,6 +5331,9 @@ export class ClaudeAcpAgent {
|
|
|
5183
5331
|
const historyLoadedAt = performance.now();
|
|
5184
5332
|
this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
|
|
5185
5333
|
const session = this.sessions[sessionId];
|
|
5334
|
+
// A replay rebuilds the client view, so its plan goes out again.
|
|
5335
|
+
if (session?.taskState)
|
|
5336
|
+
forgetPublishedTaskPlan(session.taskState);
|
|
5186
5337
|
const forwardSubagentText = session?.forwardSubagentText ?? supportsSubagentTranscript(this.clientCapabilities);
|
|
5187
5338
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
5188
5339
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
@@ -5201,8 +5352,10 @@ export class ClaudeAcpAgent {
|
|
|
5201
5352
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
5202
5353
|
const replayTerminalStates = new Map();
|
|
5203
5354
|
const replayChildren = new Map();
|
|
5204
|
-
|
|
5205
|
-
|
|
5355
|
+
// Registers the Agent and Task launches of `list`, and the terminal state
|
|
5356
|
+
// of each launch from its tool_result.
|
|
5357
|
+
const registerLaunches = (list) => {
|
|
5358
|
+
for (const message of list) {
|
|
5206
5359
|
const content = message.message
|
|
5207
5360
|
?.content;
|
|
5208
5361
|
if (!Array.isArray(content))
|
|
@@ -5252,7 +5405,9 @@ export class ClaudeAcpAgent {
|
|
|
5252
5405
|
if (child)
|
|
5253
5406
|
child.terminalState = terminalState;
|
|
5254
5407
|
}
|
|
5255
|
-
}
|
|
5408
|
+
};
|
|
5409
|
+
if (nativeReplayEnabled)
|
|
5410
|
+
registerLaunches(messages);
|
|
5256
5411
|
const announceReplayChild = async (parentToolUseId, ancestors = new Set()) => {
|
|
5257
5412
|
let child = replayChildren.get(parentToolUseId);
|
|
5258
5413
|
if (!child) {
|
|
@@ -5293,7 +5448,12 @@ export class ClaudeAcpAgent {
|
|
|
5293
5448
|
}
|
|
5294
5449
|
return child.sessionId;
|
|
5295
5450
|
};
|
|
5296
|
-
|
|
5451
|
+
// Only a history with an Agent or Task launch has subagent transcripts to read.
|
|
5452
|
+
const subagents = nativeReplayEnabled && replayChildren.size > 0
|
|
5453
|
+
? await subagentHistory(sessionId)
|
|
5454
|
+
: { ids: new Map() };
|
|
5455
|
+
const replayedSubagents = new Set();
|
|
5456
|
+
const replayMessage = async (message) => {
|
|
5297
5457
|
if (message.type === "user" &&
|
|
5298
5458
|
message.parent_tool_use_id === null &&
|
|
5299
5459
|
typeof message.uuid === "string" &&
|
|
@@ -5313,7 +5473,7 @@ export class ClaudeAcpAgent {
|
|
|
5313
5473
|
// assistant message into an authRequired error instead of showing its
|
|
5314
5474
|
// TUI-specific text; skip it on replay too (issue #863).
|
|
5315
5475
|
if (message.type === "assistant" && isSyntheticLoginMessage(message.message)) {
|
|
5316
|
-
|
|
5476
|
+
return;
|
|
5317
5477
|
}
|
|
5318
5478
|
// Capable clients saw every synthetic usage-limit message as a typed
|
|
5319
5479
|
// failure live, so replay it at the same transcript position. The
|
|
@@ -5328,7 +5488,7 @@ export class ClaudeAcpAgent {
|
|
|
5328
5488
|
if (title) {
|
|
5329
5489
|
await sessionFailures.restore(replayTurnId ? `${replayTurnId}:error` : `${sessionId}:history-error:${message.uuid}`, "quota_exhausted", title, message.uuid === activeUsageLimit?.uuid);
|
|
5330
5490
|
}
|
|
5331
|
-
|
|
5491
|
+
return;
|
|
5332
5492
|
}
|
|
5333
5493
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5334
5494
|
let content = message.message.content;
|
|
@@ -5346,7 +5506,7 @@ export class ClaudeAcpAgent {
|
|
|
5346
5506
|
if (message.message.role === "user") {
|
|
5347
5507
|
content = stripLocalCommandMetadata(content);
|
|
5348
5508
|
if (content === null)
|
|
5349
|
-
|
|
5509
|
+
return;
|
|
5350
5510
|
}
|
|
5351
5511
|
// Claude persists the retained summary as a user message framed with
|
|
5352
5512
|
// continuation instructions for the model. Clients on the compaction
|
|
@@ -5365,10 +5525,14 @@ export class ClaudeAcpAgent {
|
|
|
5365
5525
|
parentToolUseId === null &&
|
|
5366
5526
|
typeof message.uuid === "string" &&
|
|
5367
5527
|
message.uuid.length > 0) {
|
|
5368
|
-
const replayCompaction = new ContextCompactionLifecycle((notification) => this.client.sessionUpdate(notification), {
|
|
5528
|
+
const replayCompaction = new ContextCompactionLifecycle((notification) => this.client.sessionUpdate(notification), {
|
|
5529
|
+
sessionId,
|
|
5530
|
+
presentation: "compaction_update",
|
|
5531
|
+
airClient: this.toolCallCapabilities.air.client,
|
|
5532
|
+
});
|
|
5369
5533
|
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
|
|
5370
5534
|
await replayCompaction.finish(message.uuid, "completed");
|
|
5371
|
-
|
|
5535
|
+
return;
|
|
5372
5536
|
}
|
|
5373
5537
|
}
|
|
5374
5538
|
for (const notification of toAcpNotifications(
|
|
@@ -5377,6 +5541,7 @@ export class ClaudeAcpAgent {
|
|
|
5377
5541
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5378
5542
|
message.message.role, sessionId, toolUseCache, this.client, this.logger, {
|
|
5379
5543
|
registerHooks: false,
|
|
5544
|
+
replay: true,
|
|
5380
5545
|
clientCapabilities: this.clientCapabilities,
|
|
5381
5546
|
cwd: this.sessions[sessionId]?.cwd,
|
|
5382
5547
|
taskState: this.sessions[sessionId]?.taskState,
|
|
@@ -5392,7 +5557,38 @@ export class ClaudeAcpAgent {
|
|
|
5392
5557
|
}
|
|
5393
5558
|
await this.client.sessionUpdate({ ...notification, sessionId: replayTargetSessionId });
|
|
5394
5559
|
}
|
|
5395
|
-
|
|
5560
|
+
// The history of each subagent is in its own transcript. The replay
|
|
5561
|
+
// sends it to the child session right after the launch, one message at
|
|
5562
|
+
// a time, and keeps no subagent history after that.
|
|
5563
|
+
if (nativeReplayEnabled && message.type === "assistant") {
|
|
5564
|
+
for (const toolUseId of subagentLaunchIds(content)) {
|
|
5565
|
+
await announceReplayChild(toolUseId);
|
|
5566
|
+
const agentId = subagents.ids.get(toolUseId);
|
|
5567
|
+
if (!agentId || replayedSubagents.has(agentId))
|
|
5568
|
+
continue;
|
|
5569
|
+
replayedSubagents.add(agentId);
|
|
5570
|
+
let childMessages = [];
|
|
5571
|
+
try {
|
|
5572
|
+
// With the project directory, the SDK skips its search of every project.
|
|
5573
|
+
// A session opened from another directory falls back to that search.
|
|
5574
|
+
if (subagents.dir) {
|
|
5575
|
+
childMessages = await getSubagentMessages(sessionId, agentId, { dir: subagents.dir });
|
|
5576
|
+
}
|
|
5577
|
+
if (childMessages.length === 0) {
|
|
5578
|
+
childMessages = await getSubagentMessages(sessionId, agentId);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
catch (error) {
|
|
5582
|
+
this.logger.error(`Failed to read the history of subagent ${agentId}:`, error);
|
|
5583
|
+
}
|
|
5584
|
+
registerLaunches(childMessages);
|
|
5585
|
+
for (const childMessage of childMessages)
|
|
5586
|
+
await replayMessage(childMessage);
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
};
|
|
5590
|
+
for (const message of messages)
|
|
5591
|
+
await replayMessage(message);
|
|
5396
5592
|
if (nativeReplayEnabled) {
|
|
5397
5593
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
5398
5594
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -5449,7 +5645,7 @@ export class ClaudeAcpAgent {
|
|
|
5449
5645
|
* `$/cancel_request`, and our local abort race settles even if the client
|
|
5450
5646
|
* ignores it. A `cancelled` outcome, request rejection, and local abort all
|
|
5451
5647
|
* surface the same "Tool use aborted" the callers already expect. */
|
|
5452
|
-
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId
|
|
5648
|
+
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, toolInput, previewContent) {
|
|
5453
5649
|
if (signal.aborted)
|
|
5454
5650
|
throw new Error("Tool use aborted");
|
|
5455
5651
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -5458,7 +5654,7 @@ export class ClaudeAcpAgent {
|
|
|
5458
5654
|
// so emit it now if it hasn't been sent yet. The streamed tool_use chunk
|
|
5459
5655
|
// later refines it with a `tool_call_update` rather than emitting a
|
|
5460
5656
|
// duplicate (see `emittedToolCalls` in `toAcpNotifications`).
|
|
5461
|
-
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId,
|
|
5657
|
+
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId, toolInput, parentToolUseId, signal, params.sessionId, previewContent);
|
|
5462
5658
|
if (signal.aborted)
|
|
5463
5659
|
throw new Error("Tool use aborted");
|
|
5464
5660
|
// Do not rely on every ACP client settling requestPermission after the
|
|
@@ -5486,28 +5682,29 @@ export class ClaudeAcpAgent {
|
|
|
5486
5682
|
* are resolved at tool_result time instead (see `toAcpNotifications`).
|
|
5487
5683
|
* `parentToolUseId` attributes a subagent's tool call to the Agent/Task call
|
|
5488
5684
|
* that spawned it, matching the streamed path's `_meta`. */
|
|
5489
|
-
async ensureToolCallEmitted(sessionId, toolName, toolCallId, toolInput, parentToolUseId, signal, notificationSessionId = sessionId) {
|
|
5685
|
+
async ensureToolCallEmitted(sessionId, toolName, toolCallId, toolInput, parentToolUseId, signal, notificationSessionId = sessionId, previewContent) {
|
|
5490
5686
|
const session = this.sessions[sessionId];
|
|
5491
5687
|
if (!session) {
|
|
5492
5688
|
return;
|
|
5493
5689
|
}
|
|
5690
|
+
// The permission request shows an exact approval patch. The streamed
|
|
5691
|
+
// tool input must not replace it with the standard diff of the snippet.
|
|
5692
|
+
const pinPreview = () => {
|
|
5693
|
+
if (previewContent) {
|
|
5694
|
+
toolCallFieldsOf(session).pinContent(toolCallId, previewContent);
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
5494
5697
|
if (session.emittedToolCalls.has(toolCallId)) {
|
|
5698
|
+
pinPreview();
|
|
5495
5699
|
return;
|
|
5496
5700
|
}
|
|
5497
5701
|
session.emittedToolCalls.add(toolCallId);
|
|
5498
5702
|
(session.eagerToolCallSessions ??= new Map()).set(toolCallId, notificationSessionId);
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
...update._meta,
|
|
5505
|
-
claudeCode: {
|
|
5506
|
-
...(update._meta?.claudeCode || {}),
|
|
5507
|
-
parentToolUseId,
|
|
5508
|
-
},
|
|
5509
|
-
};
|
|
5510
|
-
}
|
|
5703
|
+
const update = new AcpToolCallRenderer(this.toolCallCapabilities).toolCall({ id: toolCallId, name: toolName, input: toolInput }, { cwd: session.cwd, previewContent });
|
|
5704
|
+
if (parentToolUseId)
|
|
5705
|
+
stampParentToolUseId(update, parentToolUseId);
|
|
5706
|
+
toolCallFieldsOf(session).apply(update);
|
|
5707
|
+
pinPreview();
|
|
5511
5708
|
try {
|
|
5512
5709
|
const emission = this.client.sessionUpdate({ sessionId: notificationSessionId, update });
|
|
5513
5710
|
await (signal ? raceWithAbort(emission, signal) : emission);
|
|
@@ -5518,13 +5715,12 @@ export class ClaudeAcpAgent {
|
|
|
5518
5715
|
// be allowed to publish the tool call instead of refining a phantom one.
|
|
5519
5716
|
session.emittedToolCalls.delete(toolCallId);
|
|
5520
5717
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
5718
|
+
session.toolCallFields?.delete(toolCallId);
|
|
5521
5719
|
throw error;
|
|
5522
5720
|
}
|
|
5523
5721
|
}
|
|
5524
5722
|
canUseTool(sessionId) {
|
|
5525
5723
|
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID, matchedAskRule, blockedPath, decisionReason, title, displayName, description, defaultToNo, suppressAlwaysAllowRule, mcpServer, }) => {
|
|
5526
|
-
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true ||
|
|
5527
|
-
this.clientCapabilities?._meta?.["terminal_output_delta"] === true;
|
|
5528
5724
|
const session = this.sessions[sessionId];
|
|
5529
5725
|
if (!session) {
|
|
5530
5726
|
return {
|
|
@@ -5583,12 +5779,17 @@ export class ClaudeAcpAgent {
|
|
|
5583
5779
|
// Artifact publishes).
|
|
5584
5780
|
const noPersistentRule = matchedAskRule !== undefined || suppressAlwaysAllowRule === true;
|
|
5585
5781
|
const durableChangeSet = normalizeDurablePermissionChangeSet(suggestions, noPersistentRule);
|
|
5782
|
+
const capabilities = this.toolCallCapabilities;
|
|
5783
|
+
const previewContent = capabilities.diffPatch
|
|
5784
|
+
? await previewPatchContent(toolName, toolInput, session.cwd)
|
|
5785
|
+
: undefined;
|
|
5586
5786
|
const presentation = buildClaudePermissionPresentation({
|
|
5587
5787
|
toolName,
|
|
5588
5788
|
input: toolInput,
|
|
5589
5789
|
toolUseID,
|
|
5590
5790
|
cwd: session.cwd,
|
|
5591
|
-
|
|
5791
|
+
capabilities,
|
|
5792
|
+
previewContent,
|
|
5592
5793
|
blockedPath,
|
|
5593
5794
|
title,
|
|
5594
5795
|
displayName,
|
|
@@ -5602,11 +5803,14 @@ export class ClaudeAcpAgent {
|
|
|
5602
5803
|
// server; anything else is configuration) instead of parsing the
|
|
5603
5804
|
// tool-name prefix. The name is the config key as authored — untrusted
|
|
5604
5805
|
// text, so it rides `_meta` rather than the title.
|
|
5605
|
-
|
|
5806
|
+
// AIR already holds the tool name and the parent tool call. Every
|
|
5807
|
+
// other client gets them again, like upstream.
|
|
5808
|
+
const airClient = capabilities.air.client;
|
|
5809
|
+
if (mcpServer || (parentToolUseId && !airClient)) {
|
|
5606
5810
|
presentation.toolCall._meta = {
|
|
5607
5811
|
claudeCode: {
|
|
5608
|
-
toolName,
|
|
5609
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
5812
|
+
...(airClient ? {} : { toolName }),
|
|
5813
|
+
...(parentToolUseId && !airClient ? { parentToolUseId } : {}),
|
|
5610
5814
|
...(mcpServer ? { mcpServer: { name: mcpServer.name, source: mcpServer.source } } : {}),
|
|
5611
5815
|
},
|
|
5612
5816
|
};
|
|
@@ -5629,7 +5833,7 @@ export class ClaudeAcpAgent {
|
|
|
5629
5833
|
...presentation,
|
|
5630
5834
|
options: permissionOptions,
|
|
5631
5835
|
sessionId: permissionSessionId,
|
|
5632
|
-
}, toolName, signal, parentToolUseId, sessionId);
|
|
5836
|
+
}, toolName, signal, parentToolUseId, sessionId, toolInput, previewContent);
|
|
5633
5837
|
if (signal.aborted)
|
|
5634
5838
|
throw new Error("Tool use aborted");
|
|
5635
5839
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
@@ -5720,7 +5924,7 @@ export class ClaudeAcpAgent {
|
|
|
5720
5924
|
if (!questions) {
|
|
5721
5925
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
5722
5926
|
}
|
|
5723
|
-
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID);
|
|
5927
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
5724
5928
|
let response;
|
|
5725
5929
|
try {
|
|
5726
5930
|
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
@@ -6097,7 +6301,7 @@ export class ClaudeAcpAgent {
|
|
|
6097
6301
|
}
|
|
6098
6302
|
const resumedModelHint = resumedSession
|
|
6099
6303
|
? resumedSession.model
|
|
6100
|
-
:
|
|
6304
|
+
: await readResumedModel(params.sessionId, this.logger);
|
|
6101
6305
|
const response = await this.createSession({
|
|
6102
6306
|
cwd: params.cwd,
|
|
6103
6307
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -6174,7 +6378,7 @@ export class ClaudeAcpAgent {
|
|
|
6174
6378
|
let resumedModelHint = creationOpts.resumedModelHint;
|
|
6175
6379
|
if (creationOpts.resume !== undefined &&
|
|
6176
6380
|
!Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint")) {
|
|
6177
|
-
resumedModelHint =
|
|
6381
|
+
resumedModelHint = await readResumedModel(creationOpts.resume, this.logger);
|
|
6178
6382
|
timing.phase("resume-transcript");
|
|
6179
6383
|
}
|
|
6180
6384
|
const input = new Pushable();
|
|
@@ -7426,164 +7630,9 @@ function isTaskTool(toolName) {
|
|
|
7426
7630
|
function shouldEmitToolCall(toolName) {
|
|
7427
7631
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
7428
7632
|
}
|
|
7429
|
-
/**
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
* access to Claude's concise human-readable title. */
|
|
7433
|
-
function claudeCodeMetaFromToolUse(toolUse, cwd) {
|
|
7434
|
-
const description = (toolUse.name === "Bash" || toolUse.name === "PowerShell") &&
|
|
7435
|
-
toolUse.input !== null &&
|
|
7436
|
-
typeof toolUse.input === "object" &&
|
|
7437
|
-
"description" in toolUse.input &&
|
|
7438
|
-
typeof toolUse.input.description === "string"
|
|
7439
|
-
? toolUse.input.description
|
|
7440
|
-
: undefined;
|
|
7441
|
-
const skillName = toolUse.name === "Skill"
|
|
7442
|
-
? toolUse.input?.skill
|
|
7443
|
-
: undefined;
|
|
7444
|
-
const skillPath = skillName ? resolveSkillPath(skillName, cwd) : undefined;
|
|
7445
|
-
return {
|
|
7446
|
-
toolName: toolUse.name,
|
|
7447
|
-
...(description ? { title: description } : {}),
|
|
7448
|
-
...((toolUse.name === "Agent" || toolUse.name === "Task") && { subagent: true }),
|
|
7449
|
-
...(skillName ? { skill: skillName } : {}),
|
|
7450
|
-
...(skillPath ? { skillPath } : {}),
|
|
7451
|
-
};
|
|
7452
|
-
}
|
|
7453
|
-
/**
|
|
7454
|
-
* Marks the Bash `tool_call_update` whose command detached into the background.
|
|
7455
|
-
*
|
|
7456
|
-
* A backgrounded Bash call returns as soon as the command is handed off, so the
|
|
7457
|
-
* card reaches `completed` while the command itself runs on for minutes. ACP has
|
|
7458
|
-
* no tool-call status for "still running elsewhere", so this marker is what lets
|
|
7459
|
-
* a client render the card as backgrounded work instead of finished work. It
|
|
7460
|
-
* rides the update the tool result already emits, so it costs no extra
|
|
7461
|
-
* notification and cannot arrive out of order.
|
|
7462
|
-
*
|
|
7463
|
-
* The command's own lifecycle -- progress, completion, the stop control -- is
|
|
7464
|
-
* published separately as an async task; this says only that the card has one.
|
|
7465
|
-
* Hence the AIR namespace rather than `claudeCode`: to a client without the
|
|
7466
|
-
* `asyncTasks` capability, which is never sent that lifecycle, the marker would
|
|
7467
|
-
* promise a card state it has no way to ever resolve.
|
|
7468
|
-
*/
|
|
7469
|
-
function backgroundedBashToolCall(notification, task, asyncTasksSupported) {
|
|
7470
|
-
const update = notification.update;
|
|
7471
|
-
const toolCallId = task ? nonBlankTaskField(task.toolCallId ?? task.tool_use_id) : undefined;
|
|
7472
|
-
if (!asyncTasksSupported ||
|
|
7473
|
-
!toolCallId ||
|
|
7474
|
-
update.sessionUpdate !== "tool_call_update" ||
|
|
7475
|
-
update.toolCallId !== toolCallId) {
|
|
7476
|
-
return notification;
|
|
7477
|
-
}
|
|
7478
|
-
return {
|
|
7479
|
-
...notification,
|
|
7480
|
-
update: {
|
|
7481
|
-
...update,
|
|
7482
|
-
_meta: withAirMeta(update._meta, AIR_ASYNC_TASKS_CAPABILITY, { backgrounded: true }),
|
|
7483
|
-
},
|
|
7484
|
-
};
|
|
7485
|
-
}
|
|
7486
|
-
/** The task fields arrive as `unknown` off the wire; only non-blank strings carry a link. */
|
|
7487
|
-
function nonBlankTaskField(value) {
|
|
7488
|
-
if (typeof value !== "string")
|
|
7489
|
-
return undefined;
|
|
7490
|
-
const trimmed = value.trim();
|
|
7491
|
-
return trimmed ? trimmed : undefined;
|
|
7492
|
-
}
|
|
7493
|
-
/** Roots a skill's directory may sit under, relative to the directory the scope resolves to. */
|
|
7494
|
-
const SKILL_CONTAINER_DIRS = [".claude/skills", ".agents/skills"];
|
|
7495
|
-
/**
|
|
7496
|
-
* Absolute path of a skill's `SKILL.md`, or `undefined` when none of the known layouts holds one.
|
|
7497
|
-
*
|
|
7498
|
-
* The `Skill` tool reports only the skill's name, so the file has to be located by probing the layouts skills
|
|
7499
|
-
* actually use: project- and user-level `.claude/skills` (plus this repo's `.agents/skills` source of truth), and
|
|
7500
|
-
* for a `<prefix>:<name>` spelling either a plugin (`.claude/plugins/<prefix>/skills/<name>`) or a
|
|
7501
|
-
* directory-scoped skill (`<prefix>/.claude/skills/<name>`), which share that spelling. Only a path that exists
|
|
7502
|
-
* on disk is returned, so a wrong guess costs nothing and clients never render a link to a missing file.
|
|
7503
|
-
*/
|
|
7504
|
-
function resolveSkillPath(skillName, cwd) {
|
|
7505
|
-
if (!cwd) {
|
|
7506
|
-
return undefined;
|
|
7507
|
-
}
|
|
7508
|
-
const colon = skillName.indexOf(":");
|
|
7509
|
-
const scope = colon < 0 ? undefined : skillName.slice(0, colon);
|
|
7510
|
-
const name = colon < 0 ? skillName : skillName.slice(colon + 1);
|
|
7511
|
-
if (!name) {
|
|
7512
|
-
return undefined;
|
|
7513
|
-
}
|
|
7514
|
-
const candidates = [];
|
|
7515
|
-
const addCandidates = (base) => {
|
|
7516
|
-
for (const container of SKILL_CONTAINER_DIRS) {
|
|
7517
|
-
candidates.push(path.join(base, container, name, "SKILL.md"));
|
|
7518
|
-
}
|
|
7519
|
-
};
|
|
7520
|
-
if (scope) {
|
|
7521
|
-
// A `<prefix>:<name>` skill is either directory-scoped or a plugin's; both spellings look identical.
|
|
7522
|
-
addCandidates(path.join(cwd, scope));
|
|
7523
|
-
candidates.push(path.join(cwd, ".claude/plugins", scope, "skills", name, "SKILL.md"));
|
|
7524
|
-
}
|
|
7525
|
-
addCandidates(cwd);
|
|
7526
|
-
addCandidates(os.homedir());
|
|
7527
|
-
return candidates.find((candidate) => existsSync(candidate));
|
|
7528
|
-
}
|
|
7529
|
-
/** Build the `tool_call` (or, with `refine`, the `tool_call_update`)
|
|
7530
|
-
* notification for a tool_use. Shared by every site that surfaces a tool call:
|
|
7531
|
-
* the streamed tool_use path (first encounter → tool_call, later encounter →
|
|
7532
|
-
* refine) and the permission flow (`ensureToolCallEmitted`), so they can't
|
|
7533
|
-
* drift. The initial `tool_call` carries `status: "pending"` and, for shell tools,
|
|
7534
|
-
* the `terminal_info` _meta that the later `terminal_output`/`terminal_exit`
|
|
7535
|
-
* updates key off of, and the programmatic tool `name` (ACP's tool-call-name
|
|
7536
|
-
* RFD); a refining `tool_call_update` carries none of these. `name` is set
|
|
7537
|
-
* once at first report — on a v1 update, omitting it means "unchanged", and
|
|
7538
|
-
* the tool behind a `toolCallId` never changes. */
|
|
7539
|
-
function toolCallNotification(toolUse, rawInput, supportsTerminalOutput, cwd, refine = false) {
|
|
7540
|
-
if (refine) {
|
|
7541
|
-
return {
|
|
7542
|
-
_meta: { claudeCode: claudeCodeMetaFromToolUse(toolUse, cwd) },
|
|
7543
|
-
toolCallId: toolUse.id,
|
|
7544
|
-
sessionUpdate: "tool_call_update",
|
|
7545
|
-
rawInput,
|
|
7546
|
-
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
|
|
7547
|
-
};
|
|
7548
|
-
}
|
|
7549
|
-
return {
|
|
7550
|
-
_meta: {
|
|
7551
|
-
claudeCode: claudeCodeMetaFromToolUse(toolUse, cwd),
|
|
7552
|
-
...((toolUse.name === "Bash" || toolUse.name === "PowerShell") && supportsTerminalOutput
|
|
7553
|
-
? { terminal_info: { terminal_id: toolUse.id } }
|
|
7554
|
-
: {}),
|
|
7555
|
-
},
|
|
7556
|
-
toolCallId: toolUse.id,
|
|
7557
|
-
sessionUpdate: "tool_call",
|
|
7558
|
-
name: toolUse.name,
|
|
7559
|
-
rawInput,
|
|
7560
|
-
status: "pending",
|
|
7561
|
-
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
|
|
7562
|
-
};
|
|
7563
|
-
}
|
|
7564
|
-
/** Refine a pending tool call from the complete top-level fields recovered
|
|
7565
|
-
* from its still-streaming input. Shares `toolInfoFromToolUse` with the
|
|
7566
|
-
* consolidated path but never carries `content`: content built from partial
|
|
7567
|
-
* input is misleading (an Edit missing its `new_string` renders as a pure
|
|
7568
|
-
* deletion) or invalid (a Write diff without `content` lacks the required
|
|
7569
|
-
* `newText`), and the consolidated message supplies it moments later. */
|
|
7570
|
-
function streamedInputRefinement(toolUse, input, supportsTerminalOutput, cwd) {
|
|
7571
|
-
// TodoWrite/Task* never surfaced a tool_call to refine (plan lane).
|
|
7572
|
-
if (!shouldEmitToolCall(toolUse.name)) {
|
|
7573
|
-
return undefined;
|
|
7574
|
-
}
|
|
7575
|
-
const { title, kind, locations } = toolInfoFromToolUse({ ...toolUse, input }, supportsTerminalOutput, cwd);
|
|
7576
|
-
return {
|
|
7577
|
-
_meta: {
|
|
7578
|
-
claudeCode: claudeCodeMetaFromToolUse({ ...toolUse, input }, cwd),
|
|
7579
|
-
},
|
|
7580
|
-
toolCallId: toolUse.id,
|
|
7581
|
-
sessionUpdate: "tool_call_update",
|
|
7582
|
-
rawInput: input,
|
|
7583
|
-
title,
|
|
7584
|
-
kind,
|
|
7585
|
-
...(locations ? { locations } : {}),
|
|
7586
|
-
};
|
|
7633
|
+
/** The tool-call field tracker of a session, created on first use. */
|
|
7634
|
+
function toolCallFieldsOf(session) {
|
|
7635
|
+
return (session.toolCallFields ??= new ToolCallFieldTracker());
|
|
7587
7636
|
}
|
|
7588
7637
|
/**
|
|
7589
7638
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
@@ -7592,8 +7641,7 @@ function streamedInputRefinement(toolUse, input, supportsTerminalOutput, cwd) {
|
|
|
7592
7641
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
7593
7642
|
const taskState = options?.taskState ?? new Map();
|
|
7594
7643
|
const registerHooks = options?.registerHooks !== false;
|
|
7595
|
-
const
|
|
7596
|
-
const supportsTerminalOutput = supportsTerminalOutputDelta || options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
7644
|
+
const renderer = AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
|
|
7597
7645
|
if (typeof content === "string") {
|
|
7598
7646
|
if (content.length === 0) {
|
|
7599
7647
|
return [];
|
|
@@ -7606,15 +7654,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7606
7654
|
},
|
|
7607
7655
|
};
|
|
7608
7656
|
applyMessageId(update, options?.messageId);
|
|
7609
|
-
if (options?.parentToolUseId)
|
|
7610
|
-
update.
|
|
7611
|
-
...update._meta,
|
|
7612
|
-
claudeCode: {
|
|
7613
|
-
...(update._meta?.claudeCode || {}),
|
|
7614
|
-
parentToolUseId: options.parentToolUseId,
|
|
7615
|
-
},
|
|
7616
|
-
};
|
|
7617
|
-
}
|
|
7657
|
+
if (options?.parentToolUseId)
|
|
7658
|
+
stampParentToolUseId(update, options.parentToolUseId);
|
|
7618
7659
|
return [{ sessionId, update }];
|
|
7619
7660
|
}
|
|
7620
7661
|
// `tool_use_result` is message-level and carries no tool_use_id of its own:
|
|
@@ -7633,6 +7674,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7633
7674
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
7634
7675
|
for (const chunk of content) {
|
|
7635
7676
|
let update = null;
|
|
7677
|
+
// The tool call that this chunk finishes, if it is a tool result.
|
|
7678
|
+
let finishedToolCallId;
|
|
7636
7679
|
switch (chunk.type) {
|
|
7637
7680
|
case "text":
|
|
7638
7681
|
case "text_delta": {
|
|
@@ -7701,38 +7744,27 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7701
7744
|
// closing over the name keeps the diff working without depending on
|
|
7702
7745
|
// (or pinning) the cache entry's lifetime.
|
|
7703
7746
|
const toolName = chunk.name;
|
|
7747
|
+
const hookToolCallId = chunk.id;
|
|
7704
7748
|
registerHookCallback(chunk.id, {
|
|
7705
|
-
onPostToolUseHook: async (toolUseId,
|
|
7706
|
-
//
|
|
7707
|
-
//
|
|
7708
|
-
//
|
|
7709
|
-
//
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
...(options?.parentToolUseId
|
|
7723
|
-
? { parentToolUseId: options.parentToolUseId }
|
|
7724
|
-
: {}),
|
|
7725
|
-
},
|
|
7726
|
-
},
|
|
7727
|
-
toolCallId: toolUseId,
|
|
7728
|
-
sessionUpdate: "tool_call_update",
|
|
7729
|
-
...editDiff,
|
|
7730
|
-
};
|
|
7731
|
-
await client.sessionUpdate({
|
|
7732
|
-
sessionId,
|
|
7733
|
-
update,
|
|
7734
|
-
});
|
|
7749
|
+
onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => {
|
|
7750
|
+
// The final diff of an Edit or a Write replaces the
|
|
7751
|
+
// optimistic content built from the input. Only the marker
|
|
7752
|
+
// fields of the tool_response travel: the rest repeats
|
|
7753
|
+
// output that the content already carries.
|
|
7754
|
+
const update = await renderer.hookResult({ id: toolUseId, name: toolName }, toolResponse, options?.cwd);
|
|
7755
|
+
if (!update)
|
|
7756
|
+
return;
|
|
7757
|
+
if (options?.parentToolUseId) {
|
|
7758
|
+
stampParentToolUseId(update, options.parentToolUseId);
|
|
7759
|
+
}
|
|
7760
|
+
// The final result may replace a pinned approval patch.
|
|
7761
|
+
if (options?.toolCallFields &&
|
|
7762
|
+
!options.toolCallFields.apply(update, { replacePinnedContent: true })) {
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
7765
|
+
await client.sessionUpdate({ sessionId, update });
|
|
7735
7766
|
},
|
|
7767
|
+
onRelease: () => options?.toolCallFields?.finishHook(hookToolCallId),
|
|
7736
7768
|
}, sessionId);
|
|
7737
7769
|
}
|
|
7738
7770
|
let rawInput;
|
|
@@ -7742,6 +7774,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7742
7774
|
catch {
|
|
7743
7775
|
// ignore if we can't turn it to JSON
|
|
7744
7776
|
}
|
|
7777
|
+
const toolUse = { id: chunk.id, name: chunk.name, input: rawInput };
|
|
7745
7778
|
// Emit a `tool_call` only the first time this id surfaces to the
|
|
7746
7779
|
// client; afterwards refine it with a `tool_call_update`. The first
|
|
7747
7780
|
// surface may be this stream chunk OR an earlier permission request
|
|
@@ -7751,17 +7784,20 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7751
7784
|
const emittedToolCalls = options?.emittedToolCalls;
|
|
7752
7785
|
const alreadyEmitted = emittedToolCalls ? emittedToolCalls.has(chunk.id) : alreadyCached;
|
|
7753
7786
|
emittedToolCalls?.add(chunk.id);
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7787
|
+
// A permission request surfaced the tool call with its complete
|
|
7788
|
+
// input. The empty input at the stream start has nothing to add.
|
|
7789
|
+
if (alreadyEmitted && options?.inputComplete === false)
|
|
7790
|
+
break;
|
|
7791
|
+
update = alreadyEmitted
|
|
7792
|
+
? // Already surfaced (full assistant message after streaming, or a
|
|
7793
|
+
// permission request emitted it first): refine it with the
|
|
7794
|
+
// complete input.
|
|
7795
|
+
renderer.refinement(toolUse, options?.cwd)
|
|
7796
|
+
: // First surface (streaming content_block_start or replay).
|
|
7797
|
+
renderer.toolCall(toolUse, {
|
|
7798
|
+
cwd: options?.cwd,
|
|
7799
|
+
inputComplete: options?.inputComplete,
|
|
7800
|
+
});
|
|
7765
7801
|
}
|
|
7766
7802
|
break;
|
|
7767
7803
|
}
|
|
@@ -7776,6 +7812,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7776
7812
|
const wasEmitted = options?.emittedToolCalls?.has(chunk.tool_use_id) === true;
|
|
7777
7813
|
options?.emittedToolCalls?.delete(chunk.tool_use_id);
|
|
7778
7814
|
completeHookCallback(chunk.tool_use_id);
|
|
7815
|
+
finishedToolCallId = chunk.tool_use_id;
|
|
7779
7816
|
// Why this is_error result carries harness prose instead of tool
|
|
7780
7817
|
// output (user-rejected / interrupted / …), when the SDK said so.
|
|
7781
7818
|
// Spread into the claudeCode meta of every update emitted below; the
|
|
@@ -7869,58 +7906,27 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7869
7906
|
}
|
|
7870
7907
|
}
|
|
7871
7908
|
}
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
};
|
|
7877
|
-
}
|
|
7909
|
+
const entries = shouldEmitTaskPlan
|
|
7910
|
+
? changedTaskPlanEntries(taskState, renderer.capabilities.air.client)
|
|
7911
|
+
: undefined;
|
|
7912
|
+
if (entries)
|
|
7913
|
+
update = { sessionUpdate: "plan", entries };
|
|
7878
7914
|
}
|
|
7879
7915
|
else if (toolUse.name !== "TodoWrite") {
|
|
7880
|
-
|
|
7881
|
-
const
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
:
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
output.push({
|
|
7892
|
-
sessionId,
|
|
7893
|
-
update: {
|
|
7894
|
-
_meta: {
|
|
7895
|
-
[terminalOutputKey]: terminalOutput,
|
|
7896
|
-
...(options?.parentToolUseId
|
|
7897
|
-
? { claudeCode: { parentToolUseId: options.parentToolUseId } }
|
|
7898
|
-
: {}),
|
|
7899
|
-
},
|
|
7900
|
-
toolCallId: chunk.tool_use_id,
|
|
7901
|
-
sessionUpdate: "tool_call_update",
|
|
7902
|
-
},
|
|
7903
|
-
});
|
|
7916
|
+
// A command sends its output first, then the exit and the status.
|
|
7917
|
+
const [finalUpdate, ...rest] = renderer
|
|
7918
|
+
.result(toolUse, chunk, {
|
|
7919
|
+
structured: toolUseResult,
|
|
7920
|
+
nonExecution: nonExecution,
|
|
7921
|
+
})
|
|
7922
|
+
.reverse();
|
|
7923
|
+
for (const outputUpdate of rest.reverse()) {
|
|
7924
|
+
if (options?.parentToolUseId) {
|
|
7925
|
+
stampParentToolUseId(outputUpdate, options.parentToolUseId);
|
|
7926
|
+
}
|
|
7927
|
+
output.push({ sessionId, update: outputUpdate });
|
|
7904
7928
|
}
|
|
7905
|
-
update =
|
|
7906
|
-
_meta: {
|
|
7907
|
-
claudeCode: {
|
|
7908
|
-
toolName: toolUse.name,
|
|
7909
|
-
...(nonExecution ?? {}),
|
|
7910
|
-
},
|
|
7911
|
-
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
7912
|
-
},
|
|
7913
|
-
toolCallId: chunk.tool_use_id,
|
|
7914
|
-
sessionUpdate: "tool_call_update",
|
|
7915
|
-
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
7916
|
-
// The terminal output already carried the exact bytes in the preceding
|
|
7917
|
-
// update. Repeating them as rawOutput wastes bandwidth and lets a
|
|
7918
|
-
// client accidentally render the same output twice.
|
|
7919
|
-
...(terminalOutput
|
|
7920
|
-
? {}
|
|
7921
|
-
: { rawOutput: exitPlanModeRawOutput(toolUse.name, chunk.content) }),
|
|
7922
|
-
...toolUpdate,
|
|
7923
|
-
};
|
|
7929
|
+
update = finalUpdate;
|
|
7924
7930
|
}
|
|
7925
7931
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
7926
7932
|
// retain every tool call. The PostToolUse hook (Edit/Write diffs) closes
|
|
@@ -7946,17 +7952,22 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7946
7952
|
break;
|
|
7947
7953
|
}
|
|
7948
7954
|
if (update) {
|
|
7949
|
-
if (options?.parentToolUseId)
|
|
7950
|
-
update.
|
|
7951
|
-
...update._meta,
|
|
7952
|
-
claudeCode: {
|
|
7953
|
-
...(update._meta?.claudeCode || {}),
|
|
7954
|
-
parentToolUseId: options.parentToolUseId,
|
|
7955
|
-
},
|
|
7956
|
-
};
|
|
7957
|
-
}
|
|
7955
|
+
if (options?.parentToolUseId)
|
|
7956
|
+
stampParentToolUseId(update, options.parentToolUseId);
|
|
7958
7957
|
applyMessageId(update, options?.messageId);
|
|
7959
|
-
|
|
7958
|
+
// A tool result is final, so it may replace a pinned approval patch,
|
|
7959
|
+
// for example with the error text of a rejected Edit.
|
|
7960
|
+
if (!options?.toolCallFields ||
|
|
7961
|
+
options.toolCallFields.apply(update, {
|
|
7962
|
+
replacePinnedContent: finishedToolCallId !== undefined,
|
|
7963
|
+
})) {
|
|
7964
|
+
output.push({ sessionId, update });
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
7967
|
+
if (finishedToolCallId !== undefined) {
|
|
7968
|
+
// The PostToolUse hook can still send the final diff, so the fields
|
|
7969
|
+
// stay tracked until its callback leaves the registry.
|
|
7970
|
+
options?.toolCallFields?.finishResult(finishedToolCallId, hasHookCallback(finishedToolCallId));
|
|
7960
7971
|
}
|
|
7961
7972
|
}
|
|
7962
7973
|
return output;
|
|
@@ -7971,6 +7982,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
7971
7982
|
cwd: options?.cwd,
|
|
7972
7983
|
taskState: options?.taskState,
|
|
7973
7984
|
emittedToolCalls: options?.emittedToolCalls,
|
|
7985
|
+
toolCallFields: options?.toolCallFields,
|
|
7974
7986
|
messageId: options?.messageId,
|
|
7975
7987
|
};
|
|
7976
7988
|
switch (event.type) {
|
|
@@ -7989,7 +8001,6 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
7989
8001
|
id: block.id,
|
|
7990
8002
|
name: block.name,
|
|
7991
8003
|
partialJson: "",
|
|
7992
|
-
scannedTo: 0,
|
|
7993
8004
|
inString: false,
|
|
7994
8005
|
escaped: false,
|
|
7995
8006
|
objectDepth: 0,
|
|
@@ -7998,15 +8009,19 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
7998
8009
|
emittedThroughComma: -1,
|
|
7999
8010
|
});
|
|
8000
8011
|
}
|
|
8001
|
-
|
|
8012
|
+
// The input of a streamed tool_use starts empty and streams after this
|
|
8013
|
+
// event, so the tool_call waits for the complete input.
|
|
8014
|
+
return toAcpNotifications([block], "assistant", sessionId, toolUseCache, client, logger, {
|
|
8015
|
+
...forwardedOptions,
|
|
8016
|
+
inputComplete: false,
|
|
8017
|
+
});
|
|
8002
8018
|
}
|
|
8003
8019
|
case "content_block_delta": {
|
|
8004
8020
|
if (event.delta.type === "input_json_delta") {
|
|
8005
8021
|
const streamedInput = streamedToolInputs?.get(streamKey)?.get(event.index);
|
|
8006
8022
|
if (!streamedInput)
|
|
8007
8023
|
return [];
|
|
8008
|
-
streamedInput
|
|
8009
|
-
if (scanStreamedToolInput(streamedInput)) {
|
|
8024
|
+
if (scanStreamedToolInput(streamedInput, event.delta.partial_json)) {
|
|
8010
8025
|
// Input complete: the consolidated assistant message replays the
|
|
8011
8026
|
// block with its full input and refines the call there; emitting
|
|
8012
8027
|
// here too would send a duplicate identical update.
|
|
@@ -8023,20 +8038,17 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8023
8038
|
const input = recoveredToolInput(streamedInput.partialJson.slice(0, streamedInput.lastTopLevelComma));
|
|
8024
8039
|
if (!input)
|
|
8025
8040
|
return [];
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
if (!update)
|
|
8041
|
+
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
|
|
8042
|
+
if (!shouldEmitToolCall(streamedInput.name))
|
|
8029
8043
|
return [];
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
claudeCode: {
|
|
8034
|
-
...(update._meta?.claudeCode || {}),
|
|
8035
|
-
parentToolUseId: message.parent_tool_use_id,
|
|
8036
|
-
},
|
|
8037
|
-
};
|
|
8038
|
-
}
|
|
8044
|
+
const update = AcpToolCallRenderer.for(options?.clientCapabilities).partialRefinement(streamedInput, input, options?.cwd);
|
|
8045
|
+
if (message.parent_tool_use_id)
|
|
8046
|
+
stampParentToolUseId(update, message.parent_tool_use_id);
|
|
8039
8047
|
applyMessageId(update, options?.messageId);
|
|
8048
|
+
// A refinement resends only what changed: rawInput grows with every
|
|
8049
|
+
// field, and title, kind, and locations usually stay the same.
|
|
8050
|
+
if (options?.toolCallFields && !options.toolCallFields.apply(update))
|
|
8051
|
+
return [];
|
|
8040
8052
|
return [{ sessionId, update }];
|
|
8041
8053
|
}
|
|
8042
8054
|
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, forwardedOptions);
|