@agentclientprotocol/claude-agent-acp 0.81.2 → 0.81.3-preview.2
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 +32 -40
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +557 -577
- 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/hide-claude-auth.d.ts +2 -6
- package/dist/hide-claude-auth.d.ts.map +1 -1
- package/dist/hide-claude-auth.js +3 -21
- 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-failure-extension.js +1 -1
- 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";
|
|
@@ -26,14 +25,19 @@ import { buildClaudePermissionPresentation } from "./permissions/presentation.js
|
|
|
26
25
|
import { decodeClaudePermissionResponse } from "./permissions/response.js";
|
|
27
26
|
import { SettingsManager } from "./settings.js";
|
|
28
27
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
29
|
-
import { billsClaudeSubscription,
|
|
28
|
+
import { billsClaudeSubscription, claudeLoginRequiredError, claudeSubscriptionNotSupportedError, holdsNonSubscriptionCredential, refuseClaudeSubscriptionTurn, shouldHideClaudeAuth, warnClaudeSubscriptionGuardDegraded, } from "./hide-claude-auth.js";
|
|
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
|
|
@@ -1791,14 +1962,6 @@ export class ClaudeAcpAgent {
|
|
|
1791
1962
|
// The guard reads the account anyway; reuse that read to keep the
|
|
1792
1963
|
// reported identity current, refusal or not.
|
|
1793
1964
|
onAccount: (account) => this.publishSessionAccountIdentity(account),
|
|
1794
|
-
sessionFailures: () => new SessionFailureController({
|
|
1795
|
-
sessionId,
|
|
1796
|
-
state: session.sessionFailureState,
|
|
1797
|
-
capabilities: this.clientCapabilities,
|
|
1798
|
-
isCurrent: () => this.sessions[sessionId] === session,
|
|
1799
|
-
sendUpdate: (notification) => this.client.sessionUpdate(notification),
|
|
1800
|
-
logger: this.logger,
|
|
1801
|
-
}),
|
|
1802
1965
|
});
|
|
1803
1966
|
}
|
|
1804
1967
|
async goal(params) {
|
|
@@ -1819,21 +1982,24 @@ export class ClaudeAcpAgent {
|
|
|
1819
1982
|
if (session) {
|
|
1820
1983
|
session.lastPublishedGoal = goal;
|
|
1821
1984
|
}
|
|
1985
|
+
// The goal is an AIR extension: only AIR gets it.
|
|
1986
|
+
if (!this.toolCallCapabilities.air.client)
|
|
1987
|
+
return;
|
|
1822
1988
|
await this.client.sessionUpdate({
|
|
1823
1989
|
sessionId,
|
|
1824
1990
|
update: {
|
|
1825
1991
|
sessionUpdate: "session_info_update",
|
|
1826
|
-
_meta:
|
|
1992
|
+
_meta: withAirMeta(undefined, AIR_GOAL_KEY, goal),
|
|
1827
1993
|
},
|
|
1828
1994
|
});
|
|
1829
1995
|
}
|
|
1830
1996
|
async publishTaskPlan(sessionId, taskState) {
|
|
1997
|
+
const entries = changedTaskPlanEntries(taskState, this.toolCallCapabilities.air.client);
|
|
1998
|
+
if (!entries)
|
|
1999
|
+
return;
|
|
1831
2000
|
await this.client.sessionUpdate({
|
|
1832
2001
|
sessionId,
|
|
1833
|
-
update: {
|
|
1834
|
-
sessionUpdate: "plan",
|
|
1835
|
-
entries: taskStateToPlanEntries(taskState),
|
|
1836
|
-
},
|
|
2002
|
+
update: { sessionUpdate: "plan", entries },
|
|
1837
2003
|
});
|
|
1838
2004
|
}
|
|
1839
2005
|
async publishGoalFromPrompt(sessionId, prompt, commandUuid) {
|
|
@@ -2033,7 +2199,28 @@ export class ClaudeAcpAgent {
|
|
|
2033
2199
|
// on content rather than the Anthropic message id makes dedupe robust to
|
|
2034
2200
|
// gateways that don't carry a stable/matching id across the stream and the
|
|
2035
2201
|
// consolidated message. Reset after each consolidated message consumes it.
|
|
2036
|
-
|
|
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));
|
|
2037
2224
|
// Tool-use blocks start streaming before their JSON input. Keep the
|
|
2038
2225
|
// partial input per parent message and block index so completed top-level
|
|
2039
2226
|
// fields can refine the pending tool call while it streams. Entries are
|
|
@@ -2062,9 +2249,15 @@ export class ClaudeAcpAgent {
|
|
|
2062
2249
|
* `tool_use_id` so hosts can collapse them; a notice has no lifecycle to
|
|
2063
2250
|
* update, so only the first becomes one. */
|
|
2064
2251
|
const noticedToolUses = new Set();
|
|
2065
|
-
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
|
+
}));
|
|
2066
2258
|
const compaction = new ContextCompactionLifecycle((notification) => sendUpdate(notification), {
|
|
2067
2259
|
sessionId: params.sessionId,
|
|
2260
|
+
airClient: this.toolCallCapabilities.air.client,
|
|
2068
2261
|
presentation: clientSupportsCompactionUpdates(this.clientCapabilities)
|
|
2069
2262
|
? "compaction_update"
|
|
2070
2263
|
: "tool_call",
|
|
@@ -2188,12 +2381,12 @@ export class ClaudeAcpAgent {
|
|
|
2188
2381
|
lastAssistantWasUsageLimit = false;
|
|
2189
2382
|
lastAssistantFailureTitle = undefined;
|
|
2190
2383
|
lastRefusalExplanation = null;
|
|
2191
|
-
// Do NOT reset currentStreamMessageId or
|
|
2384
|
+
// Do NOT reset currentStreamMessageId or the streamed blocks here. Turn
|
|
2192
2385
|
// activation can fire mid-message (the replayed user echo with
|
|
2193
2386
|
// --replay-user-messages lands between a message's blocks); clearing the
|
|
2194
2387
|
// streamed-content record on activation would drop the blocks that
|
|
2195
2388
|
// streamed before the echo, so the consolidated assistant message would
|
|
2196
|
-
// re-emit them as duplicates.
|
|
2389
|
+
// re-emit them as duplicates. The streamed blocks are bounded instead by being
|
|
2197
2390
|
// cleared when each consolidated message consumes it. #785 stopped
|
|
2198
2391
|
// resetting the streamed-content tracking here but left this line.
|
|
2199
2392
|
stopReason = "end_turn";
|
|
@@ -2241,7 +2434,7 @@ export class ClaudeAcpAgent {
|
|
|
2241
2434
|
continue;
|
|
2242
2435
|
}
|
|
2243
2436
|
if (record.endedPerLevel === "sweep-armed") {
|
|
2244
|
-
session
|
|
2437
|
+
settleLiveBackgroundTask(session, taskId);
|
|
2245
2438
|
}
|
|
2246
2439
|
else {
|
|
2247
2440
|
record.endedPerLevel = "sweep-armed";
|
|
@@ -2431,6 +2624,28 @@ export class ClaudeAcpAgent {
|
|
|
2431
2624
|
turn.usageOriginalOutput = originalOutput;
|
|
2432
2625
|
return markdown;
|
|
2433
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
|
+
};
|
|
2434
2649
|
/** Whether any background subagent this turn spawned is still live —
|
|
2435
2650
|
* while true, the turn's settlement stays deferred so the subagent's
|
|
2436
2651
|
* output and permission requests land inside it (see
|
|
@@ -2561,34 +2776,16 @@ export class ClaudeAcpAgent {
|
|
|
2561
2776
|
* signal that starts the client's own auth flow (AIR parks the refused
|
|
2562
2777
|
* prompt, shows its method chooser, and resumes the prompt after sign-in),
|
|
2563
2778
|
* so replacing it with a successful `end_turn` would disable that flow.
|
|
2564
|
-
* Every client keeps the rejection
|
|
2565
|
-
* the signed-out *state* as one session-scoped failure whose `login`
|
|
2566
|
-
* action remains the way back in after the client's auth prompt is
|
|
2567
|
-
* dismissed. Its title stays the policy's client-neutral fallback — the
|
|
2568
|
-
* CLI's own text ("… Please run /login") is TUI advice, meaningless in an
|
|
2569
|
-
* ACP client, so it travels as expandable details instead. */
|
|
2779
|
+
* Every client keeps the rejection. The client owns the login UI. */
|
|
2570
2780
|
const failActiveWithSessionFailure = async (kind, error, title) => {
|
|
2571
2781
|
if (session.activeTurn && !session.activeTurn.settled) {
|
|
2572
2782
|
await compaction.interrupt();
|
|
2573
2783
|
}
|
|
2574
2784
|
if (kind === "auth_required") {
|
|
2575
|
-
//
|
|
2576
|
-
//
|
|
2577
|
-
// signed-out state does not change in between: publish once, and skip
|
|
2578
|
-
// republishing while the previous sign-out is still active. A retry
|
|
2579
|
-
// warning of the same kind is not a sign-out and must not suppress it.
|
|
2580
|
-
if (!sessionFailures.hasActiveSessionError(kind)) {
|
|
2581
|
-
await publishSessionFailure(kind, { turnScoped: false, details: title });
|
|
2582
|
-
}
|
|
2583
|
-
// Preserve legacy codes: only explicit `/login` signals trigger ACP's auth flow.
|
|
2584
|
-
// The first delivery rejects the turn, so the second one finds it settled.
|
|
2585
|
-
// That is the normal course here, not the lost-failure case `failActive`
|
|
2586
|
-
// logs an error for.
|
|
2785
|
+
// The auth error starts the client's login flow. Do not send an access
|
|
2786
|
+
// failure as a second notification or render login text in the chat.
|
|
2587
2787
|
if (session.activeTurn && !session.activeTurn.settled)
|
|
2588
|
-
failActive(
|
|
2589
|
-
// The row is published and the turn is rejected. Under
|
|
2590
|
-
// `--hide-claude-auth` the account cached at `initialize` is now
|
|
2591
|
-
// wrong, so end the query and let the next turn recreate it.
|
|
2788
|
+
failActive(RequestError.authRequired());
|
|
2592
2789
|
this.markSessionForSignOutRespawn(params.sessionId, session);
|
|
2593
2790
|
return;
|
|
2594
2791
|
}
|
|
@@ -2907,7 +3104,7 @@ export class ClaudeAcpAgent {
|
|
|
2907
3104
|
// A fresh `system`/init (e.g. after reinitialize) can carry an
|
|
2908
3105
|
// updated Fast mode state; reconcile it with what we seeded at
|
|
2909
3106
|
// session creation.
|
|
2910
|
-
await this.syncFastModeState(
|
|
3107
|
+
await this.syncFastModeState(params.sessionId, session, message.fast_mode_state, message.fast_mode_disabled_reason);
|
|
2911
3108
|
// Terminal-bound slash commands (absent when none, and on
|
|
2912
3109
|
// older CLIs). The session/new advertisement runs before any
|
|
2913
3110
|
// init frame can be observed, so the first latch (or a
|
|
@@ -2917,7 +3114,7 @@ export class ClaudeAcpAgent {
|
|
|
2917
3114
|
JSON.stringify(session.terminalSlashCommands)) {
|
|
2918
3115
|
session.terminalSlashCommands = message.terminal_slash_commands;
|
|
2919
3116
|
try {
|
|
2920
|
-
await this.sendAvailableCommandsUpdate(
|
|
3117
|
+
await this.sendAvailableCommandsUpdate(params.sessionId);
|
|
2921
3118
|
}
|
|
2922
3119
|
catch (error) {
|
|
2923
3120
|
// Advisory reconcile only — the client keeps its current
|
|
@@ -2963,7 +3160,7 @@ export class ClaudeAcpAgent {
|
|
|
2963
3160
|
lastAssistantTotalUsage = usedTokens;
|
|
2964
3161
|
session.contextUsedTokens = usedTokens;
|
|
2965
3162
|
await sendUpdate({
|
|
2966
|
-
sessionId:
|
|
3163
|
+
sessionId: params.sessionId,
|
|
2967
3164
|
update: attachUsageModel({
|
|
2968
3165
|
sessionUpdate: "usage_update",
|
|
2969
3166
|
used: lastAssistantTotalUsage,
|
|
@@ -2983,7 +3180,7 @@ export class ClaudeAcpAgent {
|
|
|
2983
3180
|
if (usageMarkdown === null)
|
|
2984
3181
|
break;
|
|
2985
3182
|
await sendUpdate({
|
|
2986
|
-
sessionId:
|
|
3183
|
+
sessionId: params.sessionId,
|
|
2987
3184
|
update: {
|
|
2988
3185
|
sessionUpdate: "agent_message_chunk",
|
|
2989
3186
|
content: { type: "text", text: usageMarkdown ?? message.content },
|
|
@@ -3132,39 +3329,9 @@ export class ClaudeAcpAgent {
|
|
|
3132
3329
|
break;
|
|
3133
3330
|
}
|
|
3134
3331
|
case "memory_recall": {
|
|
3135
|
-
const isSynthesis = message.mode === "synthesize";
|
|
3136
|
-
const locations = isSynthesis
|
|
3137
|
-
? []
|
|
3138
|
-
: message.memories.map((m) => ({ path: m.path }));
|
|
3139
|
-
const content = isSynthesis
|
|
3140
|
-
? message.memories
|
|
3141
|
-
.filter((m) => typeof m.content === "string")
|
|
3142
|
-
.map((m) => ({
|
|
3143
|
-
type: "content",
|
|
3144
|
-
content: { type: "text", text: m.content },
|
|
3145
|
-
}))
|
|
3146
|
-
: [];
|
|
3147
|
-
const count = message.memories.length;
|
|
3148
|
-
const title = isSynthesis
|
|
3149
|
-
? "Recalled synthesized memory"
|
|
3150
|
-
: `Recalled ${count} ${count === 1 ? "memory" : "memories"}`;
|
|
3151
3332
|
await sendUpdate({
|
|
3152
|
-
sessionId:
|
|
3153
|
-
update:
|
|
3154
|
-
sessionUpdate: "tool_call",
|
|
3155
|
-
toolCallId: message.uuid,
|
|
3156
|
-
title,
|
|
3157
|
-
kind: "read",
|
|
3158
|
-
status: "completed",
|
|
3159
|
-
...(locations.length > 0 && { locations }),
|
|
3160
|
-
...(content.length > 0 && { content }),
|
|
3161
|
-
_meta: {
|
|
3162
|
-
claudeCode: {
|
|
3163
|
-
toolName: "memory_recall",
|
|
3164
|
-
toolResponse: { mode: message.mode },
|
|
3165
|
-
},
|
|
3166
|
-
},
|
|
3167
|
-
},
|
|
3333
|
+
sessionId: params.sessionId,
|
|
3334
|
+
update: new AcpToolCallRenderer(this.toolCallCapabilities).memoryRecall(message),
|
|
3168
3335
|
});
|
|
3169
3336
|
break;
|
|
3170
3337
|
}
|
|
@@ -3176,7 +3343,7 @@ export class ClaudeAcpAgent {
|
|
|
3176
3343
|
// it's authoritative, and re-querying supportedCommands()
|
|
3177
3344
|
// would just return the same list with an extra round-trip.
|
|
3178
3345
|
await sendUpdate({
|
|
3179
|
-
sessionId:
|
|
3346
|
+
sessionId: params.sessionId,
|
|
3180
3347
|
update: {
|
|
3181
3348
|
sessionUpdate: "available_commands_update",
|
|
3182
3349
|
availableCommands: getAvailableSlashCommands(message.commands, session.terminalSlashCommands),
|
|
@@ -3231,32 +3398,18 @@ export class ClaudeAcpAgent {
|
|
|
3231
3398
|
// child tool call was not announced there.
|
|
3232
3399
|
break;
|
|
3233
3400
|
}
|
|
3234
|
-
const
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
content: [
|
|
3242
|
-
{
|
|
3243
|
-
type: "content",
|
|
3244
|
-
content: { type: "text", text: `Permission denied: ${reason}` },
|
|
3245
|
-
},
|
|
3246
|
-
],
|
|
3247
|
-
_meta: {
|
|
3248
|
-
claudeCode: {
|
|
3249
|
-
toolName: message.tool_name,
|
|
3250
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
3251
|
-
toolResponse: {
|
|
3252
|
-
decisionReasonType: message.decision_reason_type,
|
|
3253
|
-
decisionReason: message.decision_reason,
|
|
3254
|
-
message: message.message,
|
|
3255
|
-
},
|
|
3256
|
-
},
|
|
3257
|
-
},
|
|
3258
|
-
},
|
|
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,
|
|
3259
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
|
+
}
|
|
3260
3413
|
break;
|
|
3261
3414
|
}
|
|
3262
3415
|
case "informational": {
|
|
@@ -3291,7 +3444,7 @@ export class ClaudeAcpAgent {
|
|
|
3291
3444
|
? message.content
|
|
3292
3445
|
: `**${sentenceCase(message.level)}:** ${message.content}`;
|
|
3293
3446
|
await sendUpdate({
|
|
3294
|
-
sessionId:
|
|
3447
|
+
sessionId: params.sessionId,
|
|
3295
3448
|
update: noticeOrTranscriptUpdate({
|
|
3296
3449
|
severity,
|
|
3297
3450
|
...splitNoticeText(message.content, severity === "warning"
|
|
@@ -3317,6 +3470,7 @@ export class ClaudeAcpAgent {
|
|
|
3317
3470
|
summary: message.summary,
|
|
3318
3471
|
last_tool_name: message.last_tool_name,
|
|
3319
3472
|
usage: message.usage,
|
|
3473
|
+
tool_use_id: message.tool_use_id,
|
|
3320
3474
|
});
|
|
3321
3475
|
break;
|
|
3322
3476
|
case "task_started":
|
|
@@ -3338,10 +3492,7 @@ export class ClaudeAcpAgent {
|
|
|
3338
3492
|
// turn: a turn only ever waits on its own subagents, and a
|
|
3339
3493
|
// spawn during a held-open drain window (an agent chain)
|
|
3340
3494
|
// extends that turn's hold.
|
|
3341
|
-
|
|
3342
|
-
parentToolUseId: message.tool_use_id,
|
|
3343
|
-
isSubagent: !!message.subagent_type,
|
|
3344
|
-
});
|
|
3495
|
+
registerLiveTask(message.task_id, message.tool_use_id, !!message.subagent_type);
|
|
3345
3496
|
await subagents.taskStarted({
|
|
3346
3497
|
taskId: message.task_id,
|
|
3347
3498
|
toolUseId: message.tool_use_id,
|
|
@@ -3359,9 +3510,6 @@ export class ClaudeAcpAgent {
|
|
|
3359
3510
|
skip_transcript: message.skip_transcript,
|
|
3360
3511
|
tool_use_id: message.tool_use_id,
|
|
3361
3512
|
});
|
|
3362
|
-
if (message.subagent_type && session.activeTurn && !session.activeTurn.settled) {
|
|
3363
|
-
(session.activeTurn.spawnedTaskIds ??= new Set()).add(message.task_id);
|
|
3364
|
-
}
|
|
3365
3513
|
break;
|
|
3366
3514
|
case "task_notification":
|
|
3367
3515
|
// The task settled — no further tool calls can originate
|
|
@@ -3372,10 +3520,14 @@ export class ClaudeAcpAgent {
|
|
|
3372
3520
|
status: message.status,
|
|
3373
3521
|
summary: message.summary,
|
|
3374
3522
|
output_file: message.output_file,
|
|
3523
|
+
tool_use_id: message.tool_use_id,
|
|
3375
3524
|
});
|
|
3376
|
-
if (message.tool_use_id)
|
|
3525
|
+
if (message.tool_use_id) {
|
|
3377
3526
|
subagents.discardPending(message.tool_use_id);
|
|
3378
|
-
|
|
3527
|
+
// The subagent streams no more text under this parent.
|
|
3528
|
+
streamedBlocksByParent.delete(message.tool_use_id);
|
|
3529
|
+
}
|
|
3530
|
+
settleLiveTask(message.task_id);
|
|
3379
3531
|
break;
|
|
3380
3532
|
case "task_updated":
|
|
3381
3533
|
await asyncTasks.taskUpdated(message.task_id, message.patch);
|
|
@@ -3388,7 +3540,17 @@ export class ClaudeAcpAgent {
|
|
|
3388
3540
|
message.patch.status === "failed" ||
|
|
3389
3541
|
message.patch.status === "killed") {
|
|
3390
3542
|
await subagents.finishTask(message.task_id, message.patch.status, sendUpdate);
|
|
3391
|
-
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));
|
|
3392
3554
|
}
|
|
3393
3555
|
break;
|
|
3394
3556
|
case "worker_shutting_down":
|
|
@@ -3506,7 +3668,7 @@ export class ClaudeAcpAgent {
|
|
|
3506
3668
|
...(explanation ? { description: explanation } : {}),
|
|
3507
3669
|
};
|
|
3508
3670
|
await sendUpdate({
|
|
3509
|
-
sessionId:
|
|
3671
|
+
sessionId: params.sessionId,
|
|
3510
3672
|
update: noticeOrTranscriptUpdate({ severity: "warning", ...notice }, supportsNotices, `**Model fallback:** ${fallbackNotice}`),
|
|
3511
3673
|
});
|
|
3512
3674
|
}
|
|
@@ -3588,6 +3750,9 @@ export class ClaudeAcpAgent {
|
|
|
3588
3750
|
}
|
|
3589
3751
|
break;
|
|
3590
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();
|
|
3591
3756
|
// A result from an autonomous cycle — a task-notification
|
|
3592
3757
|
// followup, or a peer/coordinator/observer message the model
|
|
3593
3758
|
// handled on its own (see AUTONOMOUS_RESULT_ORIGINS) — is not
|
|
@@ -3984,9 +4149,7 @@ export class ClaudeAcpAgent {
|
|
|
3984
4149
|
// it reports a login the query process itself runs, and a
|
|
3985
4150
|
// client can sign the user in out of band (AIR runs
|
|
3986
4151
|
// `claude /login` in a terminal, in a separate process).
|
|
3987
|
-
//
|
|
3988
|
-
// sign-out and suppress the next one, which the
|
|
3989
|
-
// `auth_required` dedupe below reads.
|
|
4152
|
+
// Clear any older signed-out record after a real answer.
|
|
3990
4153
|
failure.recoveryPolicy === "auth_status" ||
|
|
3991
4154
|
(failure.severity === "warning" && failure.turnId === activeTurnId));
|
|
3992
4155
|
}
|
|
@@ -4158,19 +4321,16 @@ export class ClaudeAcpAgent {
|
|
|
4158
4321
|
// the top-level record. Fires once, before any of this message's
|
|
4159
4322
|
// blocks, so it doesn't disturb the mid-message turn-activation
|
|
4160
4323
|
// path the way resetting on turn activation would.
|
|
4161
|
-
|
|
4162
|
-
streamedBlocks.length = 0;
|
|
4163
|
-
}
|
|
4324
|
+
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4164
4325
|
}
|
|
4165
4326
|
// Accumulate the text/thinking actually streamed live, so the
|
|
4166
4327
|
// `assistant` case below can diff its assembled blocks against what
|
|
4167
4328
|
// already reached the client as chunks and forward only the
|
|
4168
|
-
// remainder.
|
|
4169
|
-
//
|
|
4329
|
+
// remainder. Each stream (top level or one subagent) keeps its own
|
|
4330
|
+
// record, so a subagent cannot attribute content to another stream.
|
|
4170
4331
|
// Contiguous deltas of the same block (same index and type) extend
|
|
4171
4332
|
// the current entry; anything else opens a new one.
|
|
4172
|
-
if (message.
|
|
4173
|
-
message.event.type === "content_block_delta") {
|
|
4333
|
+
if (message.event.type === "content_block_delta") {
|
|
4174
4334
|
const delta = message.event.delta;
|
|
4175
4335
|
const chunk = delta.type === "text_delta"
|
|
4176
4336
|
? { type: "text", text: delta.text }
|
|
@@ -4184,6 +4344,7 @@ export class ClaudeAcpAgent {
|
|
|
4184
4344
|
// re-emitting the next block as a duplicate.
|
|
4185
4345
|
if (chunk?.text) {
|
|
4186
4346
|
const index = message.event.index;
|
|
4347
|
+
const streamedBlocks = streamedBlocksOf(message.parent_tool_use_id);
|
|
4187
4348
|
const last = streamedBlocks[streamedBlocks.length - 1];
|
|
4188
4349
|
if (last && last.index === index && last.type === chunk.type) {
|
|
4189
4350
|
last.text += chunk.text;
|
|
@@ -4250,9 +4411,19 @@ export class ClaudeAcpAgent {
|
|
|
4250
4411
|
cwd: session.cwd,
|
|
4251
4412
|
taskState: session.taskState,
|
|
4252
4413
|
emittedToolCalls: session.emittedToolCalls,
|
|
4414
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
4253
4415
|
messageId: currentStreamMessageId,
|
|
4254
4416
|
streamedToolInputs,
|
|
4255
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
|
+
}
|
|
4256
4427
|
// sendUpdate records delivery; a subagent stream's chunks carry
|
|
4257
4428
|
// the stamped parentToolUseId meta and are excluded there.
|
|
4258
4429
|
await sendUpdate(notification);
|
|
@@ -4264,7 +4435,7 @@ export class ClaudeAcpAgent {
|
|
|
4264
4435
|
// Record the ACP messageId -> SDK uuid mapping for this message
|
|
4265
4436
|
// (including replays). The consolidated message carries both ids, so
|
|
4266
4437
|
// this is where we learn the uuid the SDK's rewind/resume APIs key on
|
|
4267
|
-
// for the id we hand clients.
|
|
4438
|
+
// for the id we hand clients. Fork reads it (see messageIdToUuid).
|
|
4268
4439
|
const mappedMessageId = messageIdForGrouping(message);
|
|
4269
4440
|
if (mappedMessageId && typeof message.uuid === "string" && message.uuid.length > 0) {
|
|
4270
4441
|
session.messageIdToUuid.set(mappedMessageId, message.uuid);
|
|
@@ -4485,73 +4656,22 @@ export class ClaudeAcpAgent {
|
|
|
4485
4656
|
break;
|
|
4486
4657
|
}
|
|
4487
4658
|
let content;
|
|
4488
|
-
if (message.type === "assistant" &&
|
|
4489
|
-
|
|
4490
|
-
//
|
|
4491
|
-
//
|
|
4492
|
-
// the un-streamed remainder
|
|
4493
|
-
//
|
|
4494
|
-
//
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
// the streamed/assembled pair) without advancing it.
|
|
4499
|
-
const blocks = message.message.content;
|
|
4500
|
-
const kept = [];
|
|
4501
|
-
let streamPos = 0;
|
|
4502
|
-
for (const item of blocks) {
|
|
4503
|
-
if (item.type !== "text" && item.type !== "thinking") {
|
|
4504
|
-
kept.push(item);
|
|
4505
|
-
continue;
|
|
4506
|
-
}
|
|
4507
|
-
const full = item.type === "text" ? item.text : item.thinking;
|
|
4508
|
-
// Empty assembled blocks carry nothing (some gateways emit an
|
|
4509
|
-
// empty `thinking` block before the real text) — drop them.
|
|
4510
|
-
if (full.length === 0) {
|
|
4511
|
-
continue;
|
|
4512
|
-
}
|
|
4513
|
-
// A streamed block of the same type whose accumulated text is a
|
|
4514
|
-
// prefix of this one was already (at least partly) delivered as
|
|
4515
|
-
// chunks; consume it and forward only what's left. A non-empty
|
|
4516
|
-
// streamed text is required so an empty/aborted streamed block
|
|
4517
|
-
// doesn't swallow the assembled copy.
|
|
4518
|
-
const streamed = streamedBlocks[streamPos];
|
|
4519
|
-
if (streamed &&
|
|
4520
|
-
streamed.type === item.type &&
|
|
4521
|
-
streamed.text.length > 0 &&
|
|
4522
|
-
full.startsWith(streamed.text)) {
|
|
4523
|
-
streamPos++;
|
|
4524
|
-
const remainder = full.slice(streamed.text.length);
|
|
4525
|
-
if (remainder.length === 0) {
|
|
4526
|
-
continue;
|
|
4527
|
-
}
|
|
4528
|
-
// Overwrite in place with just the un-streamed tail (the
|
|
4529
|
-
// assembled message isn't read again after this) so the block
|
|
4530
|
-
// keeps its exact SDK type.
|
|
4531
|
-
if (item.type === "text") {
|
|
4532
|
-
item.text = remainder;
|
|
4533
|
-
}
|
|
4534
|
-
else {
|
|
4535
|
-
item.thinking = remainder;
|
|
4536
|
-
}
|
|
4537
|
-
kept.push(item);
|
|
4538
|
-
continue;
|
|
4539
|
-
}
|
|
4540
|
-
// Not matched: never streamed (or the stream diverged from the
|
|
4541
|
-
// assembled text) — forward the block in full.
|
|
4542
|
-
kept.push(item);
|
|
4543
|
-
}
|
|
4544
|
-
content = kept;
|
|
4545
|
-
// Consumed: reset so the next message's blocks accumulate fresh and
|
|
4546
|
-
// the record stays bounded to the in-flight message.
|
|
4547
|
-
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;
|
|
4548
4669
|
}
|
|
4549
|
-
else if (message.type === "assistant"
|
|
4550
|
-
|
|
4551
|
-
//
|
|
4552
|
-
// but nested text/thinking stays internal unless explicitly
|
|
4553
|
-
// 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.
|
|
4554
4673
|
content = message.message.content.filter((item) => item.type !== "text" && item.type !== "thinking");
|
|
4674
|
+
streamedBlocksOf(message.parent_tool_use_id).length = 0;
|
|
4555
4675
|
}
|
|
4556
4676
|
else {
|
|
4557
4677
|
content = message.message.content;
|
|
@@ -4562,6 +4682,13 @@ export class ClaudeAcpAgent {
|
|
|
4562
4682
|
backgroundBashTask = backgroundBashTaskFromToolResult(content, message.tool_use_result, session.toolUseCache);
|
|
4563
4683
|
if (backgroundBashTask)
|
|
4564
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
|
+
}
|
|
4565
4692
|
}
|
|
4566
4693
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, routedNotificationClient, this.logger, {
|
|
4567
4694
|
clientCapabilities: this.clientCapabilities,
|
|
@@ -4569,6 +4696,7 @@ export class ClaudeAcpAgent {
|
|
|
4569
4696
|
cwd: session.cwd,
|
|
4570
4697
|
taskState: session.taskState,
|
|
4571
4698
|
emittedToolCalls: session.emittedToolCalls,
|
|
4699
|
+
toolCallFields: toolCallFieldsOf(session),
|
|
4572
4700
|
messageId: messageIdForGrouping(message),
|
|
4573
4701
|
toolUseResult: message.type === "user" ? message.tool_use_result : undefined,
|
|
4574
4702
|
// On the wire since CLI 2.1.216 but not in SDKUserMessage's
|
|
@@ -4610,42 +4738,29 @@ export class ClaudeAcpAgent {
|
|
|
4610
4738
|
? message.parent_tool_use_id
|
|
4611
4739
|
: undefined
|
|
4612
4740
|
: undefined;
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
elapsedTimeSeconds: message.elapsed_time_seconds,
|
|
4627
|
-
// For Agent/Task calls: the subagent's type, and — when
|
|
4628
|
-
// the subagent is waiting out an API rate-limit retry —
|
|
4629
|
-
// the SDK's retry counters (attempt, max_retries,
|
|
4630
|
-
// retry_delay_ms, …), forwarded verbatim so clients can
|
|
4631
|
-
// show why a spawn looks stalled.
|
|
4632
|
-
...(message.subagent_type !== undefined && {
|
|
4633
|
-
subagentType: message.subagent_type,
|
|
4634
|
-
}),
|
|
4635
|
-
...(message.subagent_retry !== undefined && {
|
|
4636
|
-
subagentRetry: message.subagent_retry,
|
|
4637
|
-
}),
|
|
4638
|
-
},
|
|
4639
|
-
},
|
|
4640
|
-
},
|
|
4641
|
-
},
|
|
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,
|
|
4642
4754
|
});
|
|
4755
|
+
if (toolCallFieldsOf(session).apply(beat)) {
|
|
4756
|
+
await sendUpdate({ sessionId: params.sessionId, update: beat });
|
|
4757
|
+
}
|
|
4643
4758
|
break;
|
|
4644
4759
|
}
|
|
4645
4760
|
case "rate_limit_event": {
|
|
4646
4761
|
if (lastAssistantTotalUsage !== null) {
|
|
4647
4762
|
await sendUpdate({
|
|
4648
|
-
sessionId:
|
|
4763
|
+
sessionId: params.sessionId,
|
|
4649
4764
|
update: attachUsageModel({
|
|
4650
4765
|
sessionUpdate: "usage_update",
|
|
4651
4766
|
used: lastAssistantTotalUsage,
|
|
@@ -4665,6 +4780,7 @@ export class ClaudeAcpAgent {
|
|
|
4665
4780
|
subagents.clear();
|
|
4666
4781
|
asyncTasks.clear();
|
|
4667
4782
|
session.eagerToolCallSessions?.clear();
|
|
4783
|
+
session.toolCallFields?.clear();
|
|
4668
4784
|
clearHookCallbacks(params.sessionId);
|
|
4669
4785
|
session.taskState.clear();
|
|
4670
4786
|
await this.publishTaskPlan(params.sessionId, session.taskState);
|
|
@@ -4726,6 +4842,7 @@ export class ClaudeAcpAgent {
|
|
|
4726
4842
|
failAllTurns(RequestError.internalError(undefined, "The Claude Agent process exited unexpectedly. Please start a new session."));
|
|
4727
4843
|
this.closeQueryStream(session);
|
|
4728
4844
|
session.eagerToolCallSessions?.clear();
|
|
4845
|
+
session.toolCallFields?.clear();
|
|
4729
4846
|
clearHookCallbacks(params.sessionId);
|
|
4730
4847
|
session.nativeSubagentRuntime?.clear();
|
|
4731
4848
|
session.asyncTaskRuntime?.clear();
|
|
@@ -4781,6 +4898,7 @@ export class ClaudeAcpAgent {
|
|
|
4781
4898
|
// this fire-and-forget notification, so there is nothing to do here.
|
|
4782
4899
|
if (session.queryClosed) {
|
|
4783
4900
|
session.eagerToolCallSessions?.clear();
|
|
4901
|
+
session.toolCallFields?.clear();
|
|
4784
4902
|
clearHookCallbacks(params.sessionId);
|
|
4785
4903
|
return;
|
|
4786
4904
|
}
|
|
@@ -4797,6 +4915,7 @@ export class ClaudeAcpAgent {
|
|
|
4797
4915
|
}
|
|
4798
4916
|
finally {
|
|
4799
4917
|
session.eagerToolCallSessions?.clear();
|
|
4918
|
+
session.toolCallFields?.clear();
|
|
4800
4919
|
clearHookCallbacks(params.sessionId);
|
|
4801
4920
|
}
|
|
4802
4921
|
// A priority steer may still be queued in the SDK when cancellation
|
|
@@ -5094,6 +5213,7 @@ export class ClaudeAcpAgent {
|
|
|
5094
5213
|
// appropriate; query.close() above has already torn the subprocess down.
|
|
5095
5214
|
session.abortController.abort();
|
|
5096
5215
|
session.eagerToolCallSessions?.clear();
|
|
5216
|
+
session.toolCallFields?.clear();
|
|
5097
5217
|
clearHookCallbacks(sessionId);
|
|
5098
5218
|
session.nativeSubagentRuntime?.clear();
|
|
5099
5219
|
session.asyncTaskRuntime?.clear();
|
|
@@ -5211,6 +5331,9 @@ export class ClaudeAcpAgent {
|
|
|
5211
5331
|
const historyLoadedAt = performance.now();
|
|
5212
5332
|
this.logger.log(`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`);
|
|
5213
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);
|
|
5214
5337
|
const forwardSubagentText = session?.forwardSubagentText ?? supportsSubagentTranscript(this.clientCapabilities);
|
|
5215
5338
|
const supportsTypedFailures = supportsAirSessionFailures(this.clientCapabilities);
|
|
5216
5339
|
const activeUsageLimit = supportsTypedFailures ? activeUsageLimitMessage(messages) : undefined;
|
|
@@ -5229,8 +5352,10 @@ export class ClaudeAcpAgent {
|
|
|
5229
5352
|
const replayCompactionUpdates = clientSupportsCompactionUpdates(this.clientCapabilities);
|
|
5230
5353
|
const replayTerminalStates = new Map();
|
|
5231
5354
|
const replayChildren = new Map();
|
|
5232
|
-
|
|
5233
|
-
|
|
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) {
|
|
5234
5359
|
const content = message.message
|
|
5235
5360
|
?.content;
|
|
5236
5361
|
if (!Array.isArray(content))
|
|
@@ -5280,7 +5405,9 @@ export class ClaudeAcpAgent {
|
|
|
5280
5405
|
if (child)
|
|
5281
5406
|
child.terminalState = terminalState;
|
|
5282
5407
|
}
|
|
5283
|
-
}
|
|
5408
|
+
};
|
|
5409
|
+
if (nativeReplayEnabled)
|
|
5410
|
+
registerLaunches(messages);
|
|
5284
5411
|
const announceReplayChild = async (parentToolUseId, ancestors = new Set()) => {
|
|
5285
5412
|
let child = replayChildren.get(parentToolUseId);
|
|
5286
5413
|
if (!child) {
|
|
@@ -5321,7 +5448,12 @@ export class ClaudeAcpAgent {
|
|
|
5321
5448
|
}
|
|
5322
5449
|
return child.sessionId;
|
|
5323
5450
|
};
|
|
5324
|
-
|
|
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) => {
|
|
5325
5457
|
if (message.type === "user" &&
|
|
5326
5458
|
message.parent_tool_use_id === null &&
|
|
5327
5459
|
typeof message.uuid === "string" &&
|
|
@@ -5341,7 +5473,7 @@ export class ClaudeAcpAgent {
|
|
|
5341
5473
|
// assistant message into an authRequired error instead of showing its
|
|
5342
5474
|
// TUI-specific text; skip it on replay too (issue #863).
|
|
5343
5475
|
if (message.type === "assistant" && isSyntheticLoginMessage(message.message)) {
|
|
5344
|
-
|
|
5476
|
+
return;
|
|
5345
5477
|
}
|
|
5346
5478
|
// Capable clients saw every synthetic usage-limit message as a typed
|
|
5347
5479
|
// failure live, so replay it at the same transcript position. The
|
|
@@ -5356,7 +5488,7 @@ export class ClaudeAcpAgent {
|
|
|
5356
5488
|
if (title) {
|
|
5357
5489
|
await sessionFailures.restore(replayTurnId ? `${replayTurnId}:error` : `${sessionId}:history-error:${message.uuid}`, "quota_exhausted", title, message.uuid === activeUsageLimit?.uuid);
|
|
5358
5490
|
}
|
|
5359
|
-
|
|
5491
|
+
return;
|
|
5360
5492
|
}
|
|
5361
5493
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5362
5494
|
let content = message.message.content;
|
|
@@ -5374,7 +5506,7 @@ export class ClaudeAcpAgent {
|
|
|
5374
5506
|
if (message.message.role === "user") {
|
|
5375
5507
|
content = stripLocalCommandMetadata(content);
|
|
5376
5508
|
if (content === null)
|
|
5377
|
-
|
|
5509
|
+
return;
|
|
5378
5510
|
}
|
|
5379
5511
|
// Claude persists the retained summary as a user message framed with
|
|
5380
5512
|
// continuation instructions for the model. Clients on the compaction
|
|
@@ -5393,10 +5525,14 @@ export class ClaudeAcpAgent {
|
|
|
5393
5525
|
parentToolUseId === null &&
|
|
5394
5526
|
typeof message.uuid === "string" &&
|
|
5395
5527
|
message.uuid.length > 0) {
|
|
5396
|
-
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
|
+
});
|
|
5397
5533
|
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
|
|
5398
5534
|
await replayCompaction.finish(message.uuid, "completed");
|
|
5399
|
-
|
|
5535
|
+
return;
|
|
5400
5536
|
}
|
|
5401
5537
|
}
|
|
5402
5538
|
for (const notification of toAcpNotifications(
|
|
@@ -5405,6 +5541,7 @@ export class ClaudeAcpAgent {
|
|
|
5405
5541
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
5406
5542
|
message.message.role, sessionId, toolUseCache, this.client, this.logger, {
|
|
5407
5543
|
registerHooks: false,
|
|
5544
|
+
replay: true,
|
|
5408
5545
|
clientCapabilities: this.clientCapabilities,
|
|
5409
5546
|
cwd: this.sessions[sessionId]?.cwd,
|
|
5410
5547
|
taskState: this.sessions[sessionId]?.taskState,
|
|
@@ -5420,7 +5557,38 @@ export class ClaudeAcpAgent {
|
|
|
5420
5557
|
}
|
|
5421
5558
|
await this.client.sessionUpdate({ ...notification, sessionId: replayTargetSessionId });
|
|
5422
5559
|
}
|
|
5423
|
-
|
|
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);
|
|
5424
5592
|
if (nativeReplayEnabled) {
|
|
5425
5593
|
// Claude history persists sidechain messages and Agent/Task tool uses,
|
|
5426
5594
|
// but not task_started/task_updated lifecycle frames. Recover terminal
|
|
@@ -5477,7 +5645,7 @@ export class ClaudeAcpAgent {
|
|
|
5477
5645
|
* `$/cancel_request`, and our local abort race settles even if the client
|
|
5478
5646
|
* ignores it. A `cancelled` outcome, request rejection, and local abort all
|
|
5479
5647
|
* surface the same "Tool use aborted" the callers already expect. */
|
|
5480
|
-
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId
|
|
5648
|
+
async requestPermissionFromClient(params, toolName, signal, parentToolUseId, ownerSessionId, toolInput, previewContent) {
|
|
5481
5649
|
if (signal.aborted)
|
|
5482
5650
|
throw new Error("Tool use aborted");
|
|
5483
5651
|
// The SDK may invoke `canUseTool` (and therefore this permission request)
|
|
@@ -5486,7 +5654,7 @@ export class ClaudeAcpAgent {
|
|
|
5486
5654
|
// so emit it now if it hasn't been sent yet. The streamed tool_use chunk
|
|
5487
5655
|
// later refines it with a `tool_call_update` rather than emitting a
|
|
5488
5656
|
// duplicate (see `emittedToolCalls` in `toAcpNotifications`).
|
|
5489
|
-
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId,
|
|
5657
|
+
await this.ensureToolCallEmitted(ownerSessionId, toolName, params.toolCall.toolCallId, toolInput, parentToolUseId, signal, params.sessionId, previewContent);
|
|
5490
5658
|
if (signal.aborted)
|
|
5491
5659
|
throw new Error("Tool use aborted");
|
|
5492
5660
|
// Do not rely on every ACP client settling requestPermission after the
|
|
@@ -5514,28 +5682,29 @@ export class ClaudeAcpAgent {
|
|
|
5514
5682
|
* are resolved at tool_result time instead (see `toAcpNotifications`).
|
|
5515
5683
|
* `parentToolUseId` attributes a subagent's tool call to the Agent/Task call
|
|
5516
5684
|
* that spawned it, matching the streamed path's `_meta`. */
|
|
5517
|
-
async ensureToolCallEmitted(sessionId, toolName, toolCallId, toolInput, parentToolUseId, signal, notificationSessionId = sessionId) {
|
|
5685
|
+
async ensureToolCallEmitted(sessionId, toolName, toolCallId, toolInput, parentToolUseId, signal, notificationSessionId = sessionId, previewContent) {
|
|
5518
5686
|
const session = this.sessions[sessionId];
|
|
5519
5687
|
if (!session) {
|
|
5520
5688
|
return;
|
|
5521
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
|
+
};
|
|
5522
5697
|
if (session.emittedToolCalls.has(toolCallId)) {
|
|
5698
|
+
pinPreview();
|
|
5523
5699
|
return;
|
|
5524
5700
|
}
|
|
5525
5701
|
session.emittedToolCalls.add(toolCallId);
|
|
5526
5702
|
(session.eagerToolCallSessions ??= new Map()).set(toolCallId, notificationSessionId);
|
|
5527
|
-
const
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
...update._meta,
|
|
5533
|
-
claudeCode: {
|
|
5534
|
-
...(update._meta?.claudeCode || {}),
|
|
5535
|
-
parentToolUseId,
|
|
5536
|
-
},
|
|
5537
|
-
};
|
|
5538
|
-
}
|
|
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();
|
|
5539
5708
|
try {
|
|
5540
5709
|
const emission = this.client.sessionUpdate({ sessionId: notificationSessionId, update });
|
|
5541
5710
|
await (signal ? raceWithAbort(emission, signal) : emission);
|
|
@@ -5546,13 +5715,12 @@ export class ClaudeAcpAgent {
|
|
|
5546
5715
|
// be allowed to publish the tool call instead of refining a phantom one.
|
|
5547
5716
|
session.emittedToolCalls.delete(toolCallId);
|
|
5548
5717
|
session.eagerToolCallSessions?.delete(toolCallId);
|
|
5718
|
+
session.toolCallFields?.delete(toolCallId);
|
|
5549
5719
|
throw error;
|
|
5550
5720
|
}
|
|
5551
5721
|
}
|
|
5552
5722
|
canUseTool(sessionId) {
|
|
5553
5723
|
return async (toolName, toolInput, { signal, suggestions, toolUseID, agentID, matchedAskRule, blockedPath, decisionReason, title, displayName, description, defaultToNo, suppressAlwaysAllowRule, mcpServer, }) => {
|
|
5554
|
-
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true ||
|
|
5555
|
-
this.clientCapabilities?._meta?.["terminal_output_delta"] === true;
|
|
5556
5724
|
const session = this.sessions[sessionId];
|
|
5557
5725
|
if (!session) {
|
|
5558
5726
|
return {
|
|
@@ -5611,12 +5779,17 @@ export class ClaudeAcpAgent {
|
|
|
5611
5779
|
// Artifact publishes).
|
|
5612
5780
|
const noPersistentRule = matchedAskRule !== undefined || suppressAlwaysAllowRule === true;
|
|
5613
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;
|
|
5614
5786
|
const presentation = buildClaudePermissionPresentation({
|
|
5615
5787
|
toolName,
|
|
5616
5788
|
input: toolInput,
|
|
5617
5789
|
toolUseID,
|
|
5618
5790
|
cwd: session.cwd,
|
|
5619
|
-
|
|
5791
|
+
capabilities,
|
|
5792
|
+
previewContent,
|
|
5620
5793
|
blockedPath,
|
|
5621
5794
|
title,
|
|
5622
5795
|
displayName,
|
|
@@ -5630,11 +5803,14 @@ export class ClaudeAcpAgent {
|
|
|
5630
5803
|
// server; anything else is configuration) instead of parsing the
|
|
5631
5804
|
// tool-name prefix. The name is the config key as authored — untrusted
|
|
5632
5805
|
// text, so it rides `_meta` rather than the title.
|
|
5633
|
-
|
|
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)) {
|
|
5634
5810
|
presentation.toolCall._meta = {
|
|
5635
5811
|
claudeCode: {
|
|
5636
|
-
toolName,
|
|
5637
|
-
...(parentToolUseId ? { parentToolUseId } : {}),
|
|
5812
|
+
...(airClient ? {} : { toolName }),
|
|
5813
|
+
...(parentToolUseId && !airClient ? { parentToolUseId } : {}),
|
|
5638
5814
|
...(mcpServer ? { mcpServer: { name: mcpServer.name, source: mcpServer.source } } : {}),
|
|
5639
5815
|
},
|
|
5640
5816
|
};
|
|
@@ -5657,7 +5833,7 @@ export class ClaudeAcpAgent {
|
|
|
5657
5833
|
...presentation,
|
|
5658
5834
|
options: permissionOptions,
|
|
5659
5835
|
sessionId: permissionSessionId,
|
|
5660
|
-
}, toolName, signal, parentToolUseId, sessionId);
|
|
5836
|
+
}, toolName, signal, parentToolUseId, sessionId, toolInput, previewContent);
|
|
5661
5837
|
if (signal.aborted)
|
|
5662
5838
|
throw new Error("Tool use aborted");
|
|
5663
5839
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
@@ -5748,7 +5924,7 @@ export class ClaudeAcpAgent {
|
|
|
5748
5924
|
if (!questions) {
|
|
5749
5925
|
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
5750
5926
|
}
|
|
5751
|
-
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID);
|
|
5927
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID, this.toolCallCapabilities.air.client);
|
|
5752
5928
|
let response;
|
|
5753
5929
|
try {
|
|
5754
5930
|
response = await this.withPendingUserInput(sessionId, () => this.client.createElicitation(createRequest, signal));
|
|
@@ -6125,7 +6301,7 @@ export class ClaudeAcpAgent {
|
|
|
6125
6301
|
}
|
|
6126
6302
|
const resumedModelHint = resumedSession
|
|
6127
6303
|
? resumedSession.model
|
|
6128
|
-
:
|
|
6304
|
+
: await readResumedModel(params.sessionId, this.logger);
|
|
6129
6305
|
const response = await this.createSession({
|
|
6130
6306
|
cwd: params.cwd,
|
|
6131
6307
|
mcpServers: params.mcpServers ?? [],
|
|
@@ -6202,7 +6378,7 @@ export class ClaudeAcpAgent {
|
|
|
6202
6378
|
let resumedModelHint = creationOpts.resumedModelHint;
|
|
6203
6379
|
if (creationOpts.resume !== undefined &&
|
|
6204
6380
|
!Object.prototype.hasOwnProperty.call(creationOpts, "resumedModelHint")) {
|
|
6205
|
-
resumedModelHint =
|
|
6381
|
+
resumedModelHint = await readResumedModel(creationOpts.resume, this.logger);
|
|
6206
6382
|
timing.phase("resume-transcript");
|
|
6207
6383
|
}
|
|
6208
6384
|
const input = new Pushable();
|
|
@@ -6815,12 +6991,12 @@ export class ClaudeAcpAgent {
|
|
|
6815
6991
|
}
|
|
6816
6992
|
}
|
|
6817
6993
|
}
|
|
6818
|
-
/** Tell a capable client
|
|
6819
|
-
* session is already removed, so the failure is published on the state it
|
|
6820
|
-
* left behind, with the refusal reason when the error carries one. */
|
|
6994
|
+
/** Tell a capable client when a non-auth error ends a session during a provider switch. */
|
|
6821
6995
|
reportSessionLostOnProviderUpdate(sessionId, session, error) {
|
|
6822
6996
|
this.logger.error(`Session ${sessionId}: could not be recreated for the provider update: ${error}`);
|
|
6823
6997
|
const isAuthRequired = error instanceof RequestError && error.code === AUTH_REQUIRED_CODE;
|
|
6998
|
+
if (isAuthRequired)
|
|
6999
|
+
return;
|
|
6824
7000
|
const reason = error instanceof RequestError &&
|
|
6825
7001
|
typeof error.data === "object" &&
|
|
6826
7002
|
error.data !== null &&
|
|
@@ -6828,11 +7004,7 @@ export class ClaudeAcpAgent {
|
|
|
6828
7004
|
typeof error.data.reason === "string"
|
|
6829
7005
|
? error.data.reason
|
|
6830
7006
|
: undefined;
|
|
6831
|
-
const details =
|
|
6832
|
-
? CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE
|
|
6833
|
-
: error instanceof Error
|
|
6834
|
-
? error.message
|
|
6835
|
-
: String(error);
|
|
7007
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
6836
7008
|
const controller = new SessionFailureController({
|
|
6837
7009
|
sessionId,
|
|
6838
7010
|
state: session.sessionFailureState,
|
|
@@ -6844,7 +7016,7 @@ export class ClaudeAcpAgent {
|
|
|
6844
7016
|
logger: this.logger,
|
|
6845
7017
|
});
|
|
6846
7018
|
void controller
|
|
6847
|
-
.publish(
|
|
7019
|
+
.publish("internal_error", {
|
|
6848
7020
|
sessionScoped: true,
|
|
6849
7021
|
details,
|
|
6850
7022
|
...(reason ? { reason } : {}),
|
|
@@ -7458,164 +7630,9 @@ function isTaskTool(toolName) {
|
|
|
7458
7630
|
function shouldEmitToolCall(toolName) {
|
|
7459
7631
|
return toolName !== "TodoWrite" && !isTaskTool(toolName);
|
|
7460
7632
|
}
|
|
7461
|
-
/**
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
* access to Claude's concise human-readable title. */
|
|
7465
|
-
function claudeCodeMetaFromToolUse(toolUse, cwd) {
|
|
7466
|
-
const description = (toolUse.name === "Bash" || toolUse.name === "PowerShell") &&
|
|
7467
|
-
toolUse.input !== null &&
|
|
7468
|
-
typeof toolUse.input === "object" &&
|
|
7469
|
-
"description" in toolUse.input &&
|
|
7470
|
-
typeof toolUse.input.description === "string"
|
|
7471
|
-
? toolUse.input.description
|
|
7472
|
-
: undefined;
|
|
7473
|
-
const skillName = toolUse.name === "Skill"
|
|
7474
|
-
? toolUse.input?.skill
|
|
7475
|
-
: undefined;
|
|
7476
|
-
const skillPath = skillName ? resolveSkillPath(skillName, cwd) : undefined;
|
|
7477
|
-
return {
|
|
7478
|
-
toolName: toolUse.name,
|
|
7479
|
-
...(description ? { title: description } : {}),
|
|
7480
|
-
...((toolUse.name === "Agent" || toolUse.name === "Task") && { subagent: true }),
|
|
7481
|
-
...(skillName ? { skill: skillName } : {}),
|
|
7482
|
-
...(skillPath ? { skillPath } : {}),
|
|
7483
|
-
};
|
|
7484
|
-
}
|
|
7485
|
-
/**
|
|
7486
|
-
* Marks the Bash `tool_call_update` whose command detached into the background.
|
|
7487
|
-
*
|
|
7488
|
-
* A backgrounded Bash call returns as soon as the command is handed off, so the
|
|
7489
|
-
* card reaches `completed` while the command itself runs on for minutes. ACP has
|
|
7490
|
-
* no tool-call status for "still running elsewhere", so this marker is what lets
|
|
7491
|
-
* a client render the card as backgrounded work instead of finished work. It
|
|
7492
|
-
* rides the update the tool result already emits, so it costs no extra
|
|
7493
|
-
* notification and cannot arrive out of order.
|
|
7494
|
-
*
|
|
7495
|
-
* The command's own lifecycle -- progress, completion, the stop control -- is
|
|
7496
|
-
* published separately as an async task; this says only that the card has one.
|
|
7497
|
-
* Hence the AIR namespace rather than `claudeCode`: to a client without the
|
|
7498
|
-
* `asyncTasks` capability, which is never sent that lifecycle, the marker would
|
|
7499
|
-
* promise a card state it has no way to ever resolve.
|
|
7500
|
-
*/
|
|
7501
|
-
function backgroundedBashToolCall(notification, task, asyncTasksSupported) {
|
|
7502
|
-
const update = notification.update;
|
|
7503
|
-
const toolCallId = task ? nonBlankTaskField(task.toolCallId ?? task.tool_use_id) : undefined;
|
|
7504
|
-
if (!asyncTasksSupported ||
|
|
7505
|
-
!toolCallId ||
|
|
7506
|
-
update.sessionUpdate !== "tool_call_update" ||
|
|
7507
|
-
update.toolCallId !== toolCallId) {
|
|
7508
|
-
return notification;
|
|
7509
|
-
}
|
|
7510
|
-
return {
|
|
7511
|
-
...notification,
|
|
7512
|
-
update: {
|
|
7513
|
-
...update,
|
|
7514
|
-
_meta: withAirMeta(update._meta, AIR_ASYNC_TASKS_CAPABILITY, { backgrounded: true }),
|
|
7515
|
-
},
|
|
7516
|
-
};
|
|
7517
|
-
}
|
|
7518
|
-
/** The task fields arrive as `unknown` off the wire; only non-blank strings carry a link. */
|
|
7519
|
-
function nonBlankTaskField(value) {
|
|
7520
|
-
if (typeof value !== "string")
|
|
7521
|
-
return undefined;
|
|
7522
|
-
const trimmed = value.trim();
|
|
7523
|
-
return trimmed ? trimmed : undefined;
|
|
7524
|
-
}
|
|
7525
|
-
/** Roots a skill's directory may sit under, relative to the directory the scope resolves to. */
|
|
7526
|
-
const SKILL_CONTAINER_DIRS = [".claude/skills", ".agents/skills"];
|
|
7527
|
-
/**
|
|
7528
|
-
* Absolute path of a skill's `SKILL.md`, or `undefined` when none of the known layouts holds one.
|
|
7529
|
-
*
|
|
7530
|
-
* The `Skill` tool reports only the skill's name, so the file has to be located by probing the layouts skills
|
|
7531
|
-
* actually use: project- and user-level `.claude/skills` (plus this repo's `.agents/skills` source of truth), and
|
|
7532
|
-
* for a `<prefix>:<name>` spelling either a plugin (`.claude/plugins/<prefix>/skills/<name>`) or a
|
|
7533
|
-
* directory-scoped skill (`<prefix>/.claude/skills/<name>`), which share that spelling. Only a path that exists
|
|
7534
|
-
* on disk is returned, so a wrong guess costs nothing and clients never render a link to a missing file.
|
|
7535
|
-
*/
|
|
7536
|
-
function resolveSkillPath(skillName, cwd) {
|
|
7537
|
-
if (!cwd) {
|
|
7538
|
-
return undefined;
|
|
7539
|
-
}
|
|
7540
|
-
const colon = skillName.indexOf(":");
|
|
7541
|
-
const scope = colon < 0 ? undefined : skillName.slice(0, colon);
|
|
7542
|
-
const name = colon < 0 ? skillName : skillName.slice(colon + 1);
|
|
7543
|
-
if (!name) {
|
|
7544
|
-
return undefined;
|
|
7545
|
-
}
|
|
7546
|
-
const candidates = [];
|
|
7547
|
-
const addCandidates = (base) => {
|
|
7548
|
-
for (const container of SKILL_CONTAINER_DIRS) {
|
|
7549
|
-
candidates.push(path.join(base, container, name, "SKILL.md"));
|
|
7550
|
-
}
|
|
7551
|
-
};
|
|
7552
|
-
if (scope) {
|
|
7553
|
-
// A `<prefix>:<name>` skill is either directory-scoped or a plugin's; both spellings look identical.
|
|
7554
|
-
addCandidates(path.join(cwd, scope));
|
|
7555
|
-
candidates.push(path.join(cwd, ".claude/plugins", scope, "skills", name, "SKILL.md"));
|
|
7556
|
-
}
|
|
7557
|
-
addCandidates(cwd);
|
|
7558
|
-
addCandidates(os.homedir());
|
|
7559
|
-
return candidates.find((candidate) => existsSync(candidate));
|
|
7560
|
-
}
|
|
7561
|
-
/** Build the `tool_call` (or, with `refine`, the `tool_call_update`)
|
|
7562
|
-
* notification for a tool_use. Shared by every site that surfaces a tool call:
|
|
7563
|
-
* the streamed tool_use path (first encounter → tool_call, later encounter →
|
|
7564
|
-
* refine) and the permission flow (`ensureToolCallEmitted`), so they can't
|
|
7565
|
-
* drift. The initial `tool_call` carries `status: "pending"` and, for shell tools,
|
|
7566
|
-
* the `terminal_info` _meta that the later `terminal_output`/`terminal_exit`
|
|
7567
|
-
* updates key off of, and the programmatic tool `name` (ACP's tool-call-name
|
|
7568
|
-
* RFD); a refining `tool_call_update` carries none of these. `name` is set
|
|
7569
|
-
* once at first report — on a v1 update, omitting it means "unchanged", and
|
|
7570
|
-
* the tool behind a `toolCallId` never changes. */
|
|
7571
|
-
function toolCallNotification(toolUse, rawInput, supportsTerminalOutput, cwd, refine = false) {
|
|
7572
|
-
if (refine) {
|
|
7573
|
-
return {
|
|
7574
|
-
_meta: { claudeCode: claudeCodeMetaFromToolUse(toolUse, cwd) },
|
|
7575
|
-
toolCallId: toolUse.id,
|
|
7576
|
-
sessionUpdate: "tool_call_update",
|
|
7577
|
-
rawInput,
|
|
7578
|
-
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
|
|
7579
|
-
};
|
|
7580
|
-
}
|
|
7581
|
-
return {
|
|
7582
|
-
_meta: {
|
|
7583
|
-
claudeCode: claudeCodeMetaFromToolUse(toolUse, cwd),
|
|
7584
|
-
...((toolUse.name === "Bash" || toolUse.name === "PowerShell") && supportsTerminalOutput
|
|
7585
|
-
? { terminal_info: { terminal_id: toolUse.id } }
|
|
7586
|
-
: {}),
|
|
7587
|
-
},
|
|
7588
|
-
toolCallId: toolUse.id,
|
|
7589
|
-
sessionUpdate: "tool_call",
|
|
7590
|
-
name: toolUse.name,
|
|
7591
|
-
rawInput,
|
|
7592
|
-
status: "pending",
|
|
7593
|
-
...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd),
|
|
7594
|
-
};
|
|
7595
|
-
}
|
|
7596
|
-
/** Refine a pending tool call from the complete top-level fields recovered
|
|
7597
|
-
* from its still-streaming input. Shares `toolInfoFromToolUse` with the
|
|
7598
|
-
* consolidated path but never carries `content`: content built from partial
|
|
7599
|
-
* input is misleading (an Edit missing its `new_string` renders as a pure
|
|
7600
|
-
* deletion) or invalid (a Write diff without `content` lacks the required
|
|
7601
|
-
* `newText`), and the consolidated message supplies it moments later. */
|
|
7602
|
-
function streamedInputRefinement(toolUse, input, supportsTerminalOutput, cwd) {
|
|
7603
|
-
// TodoWrite/Task* never surfaced a tool_call to refine (plan lane).
|
|
7604
|
-
if (!shouldEmitToolCall(toolUse.name)) {
|
|
7605
|
-
return undefined;
|
|
7606
|
-
}
|
|
7607
|
-
const { title, kind, locations } = toolInfoFromToolUse({ ...toolUse, input }, supportsTerminalOutput, cwd);
|
|
7608
|
-
return {
|
|
7609
|
-
_meta: {
|
|
7610
|
-
claudeCode: claudeCodeMetaFromToolUse({ ...toolUse, input }, cwd),
|
|
7611
|
-
},
|
|
7612
|
-
toolCallId: toolUse.id,
|
|
7613
|
-
sessionUpdate: "tool_call_update",
|
|
7614
|
-
rawInput: input,
|
|
7615
|
-
title,
|
|
7616
|
-
kind,
|
|
7617
|
-
...(locations ? { locations } : {}),
|
|
7618
|
-
};
|
|
7633
|
+
/** The tool-call field tracker of a session, created on first use. */
|
|
7634
|
+
function toolCallFieldsOf(session) {
|
|
7635
|
+
return (session.toolCallFields ??= new ToolCallFieldTracker());
|
|
7619
7636
|
}
|
|
7620
7637
|
/**
|
|
7621
7638
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
@@ -7624,8 +7641,7 @@ function streamedInputRefinement(toolUse, input, supportsTerminalOutput, cwd) {
|
|
|
7624
7641
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
7625
7642
|
const taskState = options?.taskState ?? new Map();
|
|
7626
7643
|
const registerHooks = options?.registerHooks !== false;
|
|
7627
|
-
const
|
|
7628
|
-
const supportsTerminalOutput = supportsTerminalOutputDelta || options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
7644
|
+
const renderer = AcpToolCallRenderer.for(options?.clientCapabilities, options?.replay);
|
|
7629
7645
|
if (typeof content === "string") {
|
|
7630
7646
|
if (content.length === 0) {
|
|
7631
7647
|
return [];
|
|
@@ -7638,15 +7654,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7638
7654
|
},
|
|
7639
7655
|
};
|
|
7640
7656
|
applyMessageId(update, options?.messageId);
|
|
7641
|
-
if (options?.parentToolUseId)
|
|
7642
|
-
update.
|
|
7643
|
-
...update._meta,
|
|
7644
|
-
claudeCode: {
|
|
7645
|
-
...(update._meta?.claudeCode || {}),
|
|
7646
|
-
parentToolUseId: options.parentToolUseId,
|
|
7647
|
-
},
|
|
7648
|
-
};
|
|
7649
|
-
}
|
|
7657
|
+
if (options?.parentToolUseId)
|
|
7658
|
+
stampParentToolUseId(update, options.parentToolUseId);
|
|
7650
7659
|
return [{ sessionId, update }];
|
|
7651
7660
|
}
|
|
7652
7661
|
// `tool_use_result` is message-level and carries no tool_use_id of its own:
|
|
@@ -7665,6 +7674,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7665
7674
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
7666
7675
|
for (const chunk of content) {
|
|
7667
7676
|
let update = null;
|
|
7677
|
+
// The tool call that this chunk finishes, if it is a tool result.
|
|
7678
|
+
let finishedToolCallId;
|
|
7668
7679
|
switch (chunk.type) {
|
|
7669
7680
|
case "text":
|
|
7670
7681
|
case "text_delta": {
|
|
@@ -7733,38 +7744,27 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7733
7744
|
// closing over the name keeps the diff working without depending on
|
|
7734
7745
|
// (or pinning) the cache entry's lifetime.
|
|
7735
7746
|
const toolName = chunk.name;
|
|
7747
|
+
const hookToolCallId = chunk.id;
|
|
7736
7748
|
registerHookCallback(chunk.id, {
|
|
7737
|
-
onPostToolUseHook: async (toolUseId,
|
|
7738
|
-
//
|
|
7739
|
-
//
|
|
7740
|
-
//
|
|
7741
|
-
//
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
...(options?.parentToolUseId
|
|
7755
|
-
? { parentToolUseId: options.parentToolUseId }
|
|
7756
|
-
: {}),
|
|
7757
|
-
},
|
|
7758
|
-
},
|
|
7759
|
-
toolCallId: toolUseId,
|
|
7760
|
-
sessionUpdate: "tool_call_update",
|
|
7761
|
-
...editDiff,
|
|
7762
|
-
};
|
|
7763
|
-
await client.sessionUpdate({
|
|
7764
|
-
sessionId,
|
|
7765
|
-
update,
|
|
7766
|
-
});
|
|
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 });
|
|
7767
7766
|
},
|
|
7767
|
+
onRelease: () => options?.toolCallFields?.finishHook(hookToolCallId),
|
|
7768
7768
|
}, sessionId);
|
|
7769
7769
|
}
|
|
7770
7770
|
let rawInput;
|
|
@@ -7774,6 +7774,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7774
7774
|
catch {
|
|
7775
7775
|
// ignore if we can't turn it to JSON
|
|
7776
7776
|
}
|
|
7777
|
+
const toolUse = { id: chunk.id, name: chunk.name, input: rawInput };
|
|
7777
7778
|
// Emit a `tool_call` only the first time this id surfaces to the
|
|
7778
7779
|
// client; afterwards refine it with a `tool_call_update`. The first
|
|
7779
7780
|
// surface may be this stream chunk OR an earlier permission request
|
|
@@ -7783,17 +7784,20 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7783
7784
|
const emittedToolCalls = options?.emittedToolCalls;
|
|
7784
7785
|
const alreadyEmitted = emittedToolCalls ? emittedToolCalls.has(chunk.id) : alreadyCached;
|
|
7785
7786
|
emittedToolCalls?.add(chunk.id);
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
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
|
+
});
|
|
7797
7801
|
}
|
|
7798
7802
|
break;
|
|
7799
7803
|
}
|
|
@@ -7808,6 +7812,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7808
7812
|
const wasEmitted = options?.emittedToolCalls?.has(chunk.tool_use_id) === true;
|
|
7809
7813
|
options?.emittedToolCalls?.delete(chunk.tool_use_id);
|
|
7810
7814
|
completeHookCallback(chunk.tool_use_id);
|
|
7815
|
+
finishedToolCallId = chunk.tool_use_id;
|
|
7811
7816
|
// Why this is_error result carries harness prose instead of tool
|
|
7812
7817
|
// output (user-rejected / interrupted / …), when the SDK said so.
|
|
7813
7818
|
// Spread into the claudeCode meta of every update emitted below; the
|
|
@@ -7901,58 +7906,27 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7901
7906
|
}
|
|
7902
7907
|
}
|
|
7903
7908
|
}
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
};
|
|
7909
|
-
}
|
|
7909
|
+
const entries = shouldEmitTaskPlan
|
|
7910
|
+
? changedTaskPlanEntries(taskState, renderer.capabilities.air.client)
|
|
7911
|
+
: undefined;
|
|
7912
|
+
if (entries)
|
|
7913
|
+
update = { sessionUpdate: "plan", entries };
|
|
7910
7914
|
}
|
|
7911
7915
|
else if (toolUse.name !== "TodoWrite") {
|
|
7912
|
-
|
|
7913
|
-
const
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
:
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
output.push({
|
|
7924
|
-
sessionId,
|
|
7925
|
-
update: {
|
|
7926
|
-
_meta: {
|
|
7927
|
-
[terminalOutputKey]: terminalOutput,
|
|
7928
|
-
...(options?.parentToolUseId
|
|
7929
|
-
? { claudeCode: { parentToolUseId: options.parentToolUseId } }
|
|
7930
|
-
: {}),
|
|
7931
|
-
},
|
|
7932
|
-
toolCallId: chunk.tool_use_id,
|
|
7933
|
-
sessionUpdate: "tool_call_update",
|
|
7934
|
-
},
|
|
7935
|
-
});
|
|
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 });
|
|
7936
7928
|
}
|
|
7937
|
-
update =
|
|
7938
|
-
_meta: {
|
|
7939
|
-
claudeCode: {
|
|
7940
|
-
toolName: toolUse.name,
|
|
7941
|
-
...(nonExecution ?? {}),
|
|
7942
|
-
},
|
|
7943
|
-
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
7944
|
-
},
|
|
7945
|
-
toolCallId: chunk.tool_use_id,
|
|
7946
|
-
sessionUpdate: "tool_call_update",
|
|
7947
|
-
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
7948
|
-
// The terminal output already carried the exact bytes in the preceding
|
|
7949
|
-
// update. Repeating them as rawOutput wastes bandwidth and lets a
|
|
7950
|
-
// client accidentally render the same output twice.
|
|
7951
|
-
...(terminalOutput
|
|
7952
|
-
? {}
|
|
7953
|
-
: { rawOutput: exitPlanModeRawOutput(toolUse.name, chunk.content) }),
|
|
7954
|
-
...toolUpdate,
|
|
7955
|
-
};
|
|
7929
|
+
update = finalUpdate;
|
|
7956
7930
|
}
|
|
7957
7931
|
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
7958
7932
|
// retain every tool call. The PostToolUse hook (Edit/Write diffs) closes
|
|
@@ -7978,17 +7952,22 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
7978
7952
|
break;
|
|
7979
7953
|
}
|
|
7980
7954
|
if (update) {
|
|
7981
|
-
if (options?.parentToolUseId)
|
|
7982
|
-
update.
|
|
7983
|
-
...update._meta,
|
|
7984
|
-
claudeCode: {
|
|
7985
|
-
...(update._meta?.claudeCode || {}),
|
|
7986
|
-
parentToolUseId: options.parentToolUseId,
|
|
7987
|
-
},
|
|
7988
|
-
};
|
|
7989
|
-
}
|
|
7955
|
+
if (options?.parentToolUseId)
|
|
7956
|
+
stampParentToolUseId(update, options.parentToolUseId);
|
|
7990
7957
|
applyMessageId(update, options?.messageId);
|
|
7991
|
-
|
|
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));
|
|
7992
7971
|
}
|
|
7993
7972
|
}
|
|
7994
7973
|
return output;
|
|
@@ -8003,6 +7982,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8003
7982
|
cwd: options?.cwd,
|
|
8004
7983
|
taskState: options?.taskState,
|
|
8005
7984
|
emittedToolCalls: options?.emittedToolCalls,
|
|
7985
|
+
toolCallFields: options?.toolCallFields,
|
|
8006
7986
|
messageId: options?.messageId,
|
|
8007
7987
|
};
|
|
8008
7988
|
switch (event.type) {
|
|
@@ -8021,7 +8001,6 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8021
8001
|
id: block.id,
|
|
8022
8002
|
name: block.name,
|
|
8023
8003
|
partialJson: "",
|
|
8024
|
-
scannedTo: 0,
|
|
8025
8004
|
inString: false,
|
|
8026
8005
|
escaped: false,
|
|
8027
8006
|
objectDepth: 0,
|
|
@@ -8030,15 +8009,19 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8030
8009
|
emittedThroughComma: -1,
|
|
8031
8010
|
});
|
|
8032
8011
|
}
|
|
8033
|
-
|
|
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
|
+
});
|
|
8034
8018
|
}
|
|
8035
8019
|
case "content_block_delta": {
|
|
8036
8020
|
if (event.delta.type === "input_json_delta") {
|
|
8037
8021
|
const streamedInput = streamedToolInputs?.get(streamKey)?.get(event.index);
|
|
8038
8022
|
if (!streamedInput)
|
|
8039
8023
|
return [];
|
|
8040
|
-
streamedInput
|
|
8041
|
-
if (scanStreamedToolInput(streamedInput)) {
|
|
8024
|
+
if (scanStreamedToolInput(streamedInput, event.delta.partial_json)) {
|
|
8042
8025
|
// Input complete: the consolidated assistant message replays the
|
|
8043
8026
|
// block with its full input and refines the call there; emitting
|
|
8044
8027
|
// here too would send a duplicate identical update.
|
|
@@ -8055,20 +8038,17 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
8055
8038
|
const input = recoveredToolInput(streamedInput.partialJson.slice(0, streamedInput.lastTopLevelComma));
|
|
8056
8039
|
if (!input)
|
|
8057
8040
|
return [];
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
if (!update)
|
|
8041
|
+
// TodoWrite and the Task* tools never surfaced a tool_call to refine.
|
|
8042
|
+
if (!shouldEmitToolCall(streamedInput.name))
|
|
8061
8043
|
return [];
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
claudeCode: {
|
|
8066
|
-
...(update._meta?.claudeCode || {}),
|
|
8067
|
-
parentToolUseId: message.parent_tool_use_id,
|
|
8068
|
-
},
|
|
8069
|
-
};
|
|
8070
|
-
}
|
|
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);
|
|
8071
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 [];
|
|
8072
8052
|
return [{ sessionId, update }];
|
|
8073
8053
|
}
|
|
8074
8054
|
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, forwardedOptions);
|