@lucascouts/claude-agent-acp-plus 0.34.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-agent.d.ts +49 -5
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +288 -66
- package/dist/child-sessions.d.ts +14 -1
- package/dist/child-sessions.d.ts.map +1 -1
- package/dist/child-sessions.js +41 -1
- package/dist/clear-context-coordinator.d.ts +4 -1
- package/dist/clear-context-coordinator.d.ts.map +1 -1
- package/dist/clear-context-coordinator.js +1 -1
- package/dist/exit-plan.d.ts.map +1 -1
- package/dist/exit-plan.js +8 -1
- package/dist/keyed-plan.d.ts +23 -0
- package/dist/keyed-plan.d.ts.map +1 -0
- package/dist/keyed-plan.js +32 -0
- package/dist/session-notices.d.ts +10 -0
- package/dist/session-notices.d.ts.map +1 -1
- package/dist/session-notices.js +22 -1
- package/dist/startup-failure.d.ts +16 -0
- package/dist/startup-failure.d.ts.map +1 -0
- package/dist/startup-failure.js +38 -0
- package/dist/task-feed.d.ts +9 -0
- package/dist/task-feed.d.ts.map +1 -1
- package/dist/task-feed.js +28 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +28 -0
- package/package.json +3 -3
package/dist/acp-agent.js
CHANGED
|
@@ -22,6 +22,7 @@ import { createThinkingConfigOption, effectiveThinkingConfig, modelCanDisableThi
|
|
|
22
22
|
import { applyLegacyUltracodeEffort, applyUltracodeReadback, applyUltracodeToggle, effortFlagSettings, isLegacyUltracodeEffort, isUltracodeAvailable, readUltracodeState, resolveUltracodeRequested, ULTRACODE_CONFIG_ID, ultracodeConfigOption, } from "./ultracode.js";
|
|
23
23
|
import { deleteSessionIfStored } from "./session-delete.js";
|
|
24
24
|
import { applyPlanUpdatesToOptions, createPlanUpdatesConfigOption, resolvePlanUpdatesAtSpawn, resolvePlanUpdatesRequested, TODO_TOOLS_ENV, } from "./plan-updates.js";
|
|
25
|
+
import { planRemovedUpdate, planSessionUpdate, supportsPlanOperations } from "./keyed-plan.js";
|
|
25
26
|
import { handleRewindCommand, parseRewindInvocation } from "./rewind-command.js";
|
|
26
27
|
import { resolvePermissionMode, sessionAllowsBypass } from "./permissions/modes.js";
|
|
27
28
|
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
|
|
@@ -30,8 +31,8 @@ import { buildClaudePermissionPresentation } from "./permissions/presentation.js
|
|
|
30
31
|
import { decodeClaudePermissionResponse } from "./permissions/response.js";
|
|
31
32
|
import { activeUsageLimitMessage, airSessionFailureCapabilityMeta, assistantMessageText, createSessionFailureState, isSyntheticUsageLimitMessage, providerFailureCategory, SessionFailureController, sessionFailureMeta, supportsAirSessionFailures, } from "./session-failure-extension.js";
|
|
32
33
|
import { AGENT_FILE_CHANGE_REPORT_CAPABILITY, agentFileChangeReportMeta, agentFileChangeReportRequestId, containsFileChangeAuditMarker, createFileChangeAuditSupport, createFileChangeAuditTurnState, FILE_CHANGE_AUDIT_SERVER_NAME, isFileChangeAuditReportPhase, isFileChangeAuditTool, supportsAgentFileChangeReport, } from "./file-change-audit.js";
|
|
33
|
-
import { clientSupportsNotices, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
34
|
-
import { applyTaskCreate, applyTaskList, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, shellDescription, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
34
|
+
import { clientSupportsNotices, collapseHookSpeaker, MAX_NOTICE_TITLE_LENGTH, normalizeNoticeText, noticeOrTranscriptUpdate, sentenceCase, splitNoticeText, } from "./session-notices.js";
|
|
35
|
+
import { applyTaskCreate, applyTaskList, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, parseTaskListOutput, parseTaskUpdateOutput, planEntries, registerHookCallback, shellDescription, unregisterHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
35
36
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
36
37
|
import { acceptedPlanToolResult, ExitPlanCoordinator, exitPlanModeRawOutput, isExitPlanInterruptionResult, observeExitPlanToolResults, } from "./exit-plan.js";
|
|
37
38
|
import { DEFAULT_AGENT_ID, EFFORT_CONFIG_ID, PLAN_UPDATES_CONFIG_ID, } from "./session-config-ids.js";
|
|
@@ -45,6 +46,7 @@ import { AccountUsageTracker } from "./account-usage.js";
|
|
|
45
46
|
import { readFreshLimits, sharedSampleMaxAgeMs, writeLimits } from "./quota-cache.js";
|
|
46
47
|
import { ACCOUNT_CONFIG_ID, accountForValue, accountInForce, createAccountConfigOption, envForAccount, readDeclaredAccounts, } from "./accounts.js";
|
|
47
48
|
import { isUsageCommandText, structuredUsageMarkdown, syntheticLocalCommand, } from "./usage-markdown.js";
|
|
49
|
+
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "./startup-failure.js";
|
|
48
50
|
export { DEFAULT_AGENT_ID, EFFORT_CONFIG_ID } from "./session-config-ids.js";
|
|
49
51
|
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
|
|
50
52
|
import { TASKS_CONTRACT_VERSION, TASKS_META_KEY, normaliseStatus, TailPoller, TaskFeed, TaskFeedPublisher, } from "./task-feed.js";
|
|
@@ -418,30 +420,55 @@ function subagentLaunchOf(parentSessionId, session, message) {
|
|
|
418
420
|
launcherTaskId: message.parent_task_id,
|
|
419
421
|
toolUseId: message.tool_use_id,
|
|
420
422
|
agentId: message.task_id,
|
|
421
|
-
|
|
422
|
-
description: message.description,
|
|
423
|
+
...subagentLabels(name, message.description, message.subagent_type),
|
|
423
424
|
prompt,
|
|
424
425
|
cancellable: typeof session.query.stopTask === "function",
|
|
425
426
|
};
|
|
426
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* The RFD labels for a child (upstream #1479). The title is what tells siblings apart: the
|
|
430
|
+
* agent's name when it has one, else the task's short description (never the prompt), else its
|
|
431
|
+
* role. The description is the role, its `subagent_type`, unless the role is already the title.
|
|
432
|
+
* Before this, siblings of one type all read "Explore".
|
|
433
|
+
*/
|
|
434
|
+
export function subagentLabels(name, task, role) {
|
|
435
|
+
const text = (value) => (value && value.trim() ? value : undefined);
|
|
436
|
+
const agentRole = text(role);
|
|
437
|
+
const title = text(name) ?? text(task) ?? agentRole ?? "Subagent";
|
|
438
|
+
return { title, ...(agentRole && agentRole !== title ? { description: agentRole } : {}) };
|
|
439
|
+
}
|
|
427
440
|
/** A child read back from a transcript, in the shape `ChildSessionRouter`
|
|
428
441
|
* announces: the same title rule as a live launch (`subagentLaunchOf`). */
|
|
429
442
|
function childLaunchOf(parentSessionId, session, toolUseId, record, input) {
|
|
430
443
|
const text = (value) => (typeof value === "string" && value.trim() ? value : undefined);
|
|
431
|
-
const description = record.description ?? text(input.description) ?? "";
|
|
432
444
|
return {
|
|
433
445
|
parentSessionId,
|
|
434
446
|
launcherTaskId: record.launcherAgentId,
|
|
435
447
|
toolUseId,
|
|
436
448
|
agentId: record.agentId,
|
|
437
|
-
|
|
438
|
-
description,
|
|
449
|
+
...subagentLabels(record.name ?? text(input.name), record.description ?? text(input.description), record.agentType ?? text(input.subagent_type)),
|
|
439
450
|
prompt: text(input.prompt),
|
|
440
451
|
// History, not a task the CLI holds: no Stop until it runs again, when its
|
|
441
452
|
// live launch advertises what the SDK can stop (design.md, Security).
|
|
442
453
|
cancellable: false,
|
|
443
454
|
};
|
|
444
455
|
}
|
|
456
|
+
/** Whether a permission request from `agentID` belongs to work that blocks the turn: the main
|
|
457
|
+
* thread (no agent id), a foreground subagent, or a subagent launched by one. A task this
|
|
458
|
+
* session never registered is not foreground work (upstream #1466, `blocksForeground`). */
|
|
459
|
+
function blocksForeground(session, agentID) {
|
|
460
|
+
const seen = new Set();
|
|
461
|
+
for (let id = agentID; id !== undefined && !seen.has(id);) {
|
|
462
|
+
seen.add(id);
|
|
463
|
+
const task = session.liveBackgroundTasks.get(id);
|
|
464
|
+
if (!task)
|
|
465
|
+
return false;
|
|
466
|
+
if (task.backgrounded)
|
|
467
|
+
return false;
|
|
468
|
+
id = task.parentTaskId;
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
445
472
|
/** The text of a user message whose content is a string or one text block. */
|
|
446
473
|
function userText(content) {
|
|
447
474
|
if (typeof content === "string")
|
|
@@ -750,6 +777,16 @@ export function isReplayHiddenMetaMessage(message) {
|
|
|
750
777
|
export function isLocalCommandMetadata(content) {
|
|
751
778
|
return stripLocalCommandMetadata(content) === null;
|
|
752
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* True when CLI error text tells the user to sign in with `/login`. The CLI
|
|
782
|
+
* words this several ways — "Not logged in · Please run /login", "Not logged
|
|
783
|
+
* in · Run /login", "Login expired · Run /login to sign in again, or
|
|
784
|
+
* re-authenticate your Anthropic profile" — so match the "run /login"
|
|
785
|
+
* instruction rather than one sentence.
|
|
786
|
+
*/
|
|
787
|
+
export function isLoginRequiredText(text) {
|
|
788
|
+
return /\brun \/login\b/i.test(text);
|
|
789
|
+
}
|
|
753
790
|
/**
|
|
754
791
|
* True for the synthetic assistant message the CLI injects into the transcript
|
|
755
792
|
* when a turn fails authentication (e.g. "Not logged in · Please run /login",
|
|
@@ -762,9 +799,13 @@ export function isLocalCommandMetadata(content) {
|
|
|
762
799
|
* session/load even after the user has logged back in.
|
|
763
800
|
*
|
|
764
801
|
* Takes the API message (`message.message`), which replay only knows as
|
|
765
|
-
* `unknown`. The persisted record's structured `error
|
|
766
|
-
*
|
|
767
|
-
*
|
|
802
|
+
* `unknown`. The persisted record's structured `error` marker is stripped by
|
|
803
|
+
* `getSessionMessages`, and the CLI does not always set it to
|
|
804
|
+
* `authentication_failed` anyway: an expired login with no refresh token
|
|
805
|
+
* ("Login expired · Run /login to sign in again, …") arrives as
|
|
806
|
+
* `invalid_request`. So the synthetic model + text is what both paths match on.
|
|
807
|
+
* Usage-limit banners are excluded because some of them also mention `/login`
|
|
808
|
+
* (to switch accounts) and have their own handling.
|
|
768
809
|
*/
|
|
769
810
|
export function isSyntheticLoginMessage(apiMessage) {
|
|
770
811
|
if (!apiMessage || typeof apiMessage !== "object") {
|
|
@@ -778,7 +819,8 @@ export function isSyntheticLoginMessage(apiMessage) {
|
|
|
778
819
|
return (!!block &&
|
|
779
820
|
block.type === "text" &&
|
|
780
821
|
typeof block.text === "string" &&
|
|
781
|
-
block.text
|
|
822
|
+
isLoginRequiredText(block.text) &&
|
|
823
|
+
!isSyntheticUsageLimitMessage(apiMessage));
|
|
782
824
|
}
|
|
783
825
|
/**
|
|
784
826
|
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
|
|
@@ -1483,13 +1525,14 @@ export class ClaudeAcpAgent {
|
|
|
1483
1525
|
});
|
|
1484
1526
|
}
|
|
1485
1527
|
async publishTaskPlan(sessionId, taskState) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1528
|
+
const keyed = supportsPlanOperations(this.clientCapabilities);
|
|
1529
|
+
// An emptied list (a conversation reset) removes the keyed plan rather
|
|
1530
|
+
// than leaving an empty one on screen; a legacy client gets the empty plan
|
|
1531
|
+
// it always got.
|
|
1532
|
+
const update = keyed && taskState.size === 0
|
|
1533
|
+
? planRemovedUpdate()
|
|
1534
|
+
: planSessionUpdate(taskStateToPlanEntries(taskState), keyed);
|
|
1535
|
+
await this.client.sessionUpdate({ sessionId, update });
|
|
1493
1536
|
}
|
|
1494
1537
|
/** This session's quota-window mapper, created on first use (see
|
|
1495
1538
|
* {@link Session.accountUsage} for why it lives on the Session). */
|
|
@@ -1702,6 +1745,44 @@ export class ClaudeAcpAgent {
|
|
|
1702
1745
|
log(refreshed ? "published" : "nothing", coalesced ? "coalesced" : undefined);
|
|
1703
1746
|
return { refreshed };
|
|
1704
1747
|
}
|
|
1748
|
+
/** Upstream #1464: a session notice when a client-provided MCP server reports `failed` or
|
|
1749
|
+
* `needs-auth` at init, once per server per session. Plus has no MCP OAuth flow, so a
|
|
1750
|
+
* `needs-auth` server is always reported; its tools stay unavailable until it is authenticated. */
|
|
1751
|
+
async reportMcpServerStartupFailures(session, servers, supportsNotices, sendUpdate, sessionId) {
|
|
1752
|
+
if (!servers?.length)
|
|
1753
|
+
return;
|
|
1754
|
+
const clientServers = new Set((session.creationParams?.mcpServers ?? []).map((server) => server.name));
|
|
1755
|
+
for (const server of servers) {
|
|
1756
|
+
if (!clientServers.has(server.name) || session.reportedMcpServers?.has(server.name)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
let notice;
|
|
1760
|
+
if (server.status === "failed") {
|
|
1761
|
+
notice = {
|
|
1762
|
+
severity: "warning",
|
|
1763
|
+
title: `MCP server "${server.name}" failed to start`,
|
|
1764
|
+
description: "Its tools are unavailable in this session. Run /mcp for the error.",
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
else if (server.status === "needs-auth") {
|
|
1768
|
+
notice = {
|
|
1769
|
+
severity: "info",
|
|
1770
|
+
title: `MCP server "${server.name}" needs authentication`,
|
|
1771
|
+
description: "Its tools are unavailable until the server is authenticated.",
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
else {
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
(session.reportedMcpServers ??= new Set()).add(server.name);
|
|
1778
|
+
await sendUpdate({
|
|
1779
|
+
sessionId,
|
|
1780
|
+
update: noticeOrTranscriptUpdate(notice, supportsNotices, undefined, {
|
|
1781
|
+
claudeCode: { kind: "mcpServerStatus", server: server.name, status: server.status },
|
|
1782
|
+
}),
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1705
1786
|
async publishGoalFromPrompt(sessionId, prompt, commandUuid) {
|
|
1706
1787
|
const goalUpdate = goalUpdateFromPrompt(prompt);
|
|
1707
1788
|
if (goalUpdate !== undefined) {
|
|
@@ -1948,6 +2029,11 @@ export class ClaudeAcpAgent {
|
|
|
1948
2029
|
endTurnScope(session, turn, reason) {
|
|
1949
2030
|
session.classifierGrants.clearOnce();
|
|
1950
2031
|
this.finishFileChangeAudit(session, turn, reason);
|
|
2032
|
+
// Every cancel path settles through here. The turn's foreground tasks end
|
|
2033
|
+
// with it even when the CLI omits their closing event (F3, §5.3).
|
|
2034
|
+
if (reason === "cancelled" && session.taskFeed && session.taskFeedPublisher) {
|
|
2035
|
+
session.taskFeedPublisher.notify(session.taskFeed.onTurnCancelled());
|
|
2036
|
+
}
|
|
1951
2037
|
}
|
|
1952
2038
|
/** Publish the audit terminal for every turn path that did not reach the
|
|
1953
2039
|
* report tool. The support flips the turn state synchronously before its
|
|
@@ -1986,6 +2072,10 @@ export class ClaudeAcpAgent {
|
|
|
1986
2072
|
let lastAssistantTotalUsage = null;
|
|
1987
2073
|
let lastAssistantUsage = null;
|
|
1988
2074
|
let lastAssistantModel = null;
|
|
2075
|
+
// A `rate_limit_event` that arrived before the turn had a context total to
|
|
2076
|
+
// send it with (upstream #1534): with partial messages on, the SDK sends it
|
|
2077
|
+
// ahead of the first assistant frame. It rides the turn's next usage_update.
|
|
2078
|
+
let pendingRateLimit = null;
|
|
1989
2079
|
// When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
|
|
1990
2080
|
// problem, billing), it sets a categorical `error` field on the
|
|
1991
2081
|
// `SDKAssistantMessage` that precedes the final `result` message. We capture
|
|
@@ -2141,10 +2231,23 @@ export class ClaudeAcpAgent {
|
|
|
2141
2231
|
await sessionFailures.clear((failure) => failure.recoveryPolicy === "next_attempt" && failure.turnId !== activeTurnId);
|
|
2142
2232
|
};
|
|
2143
2233
|
const internalErrorForClient = (data, rawDetail) => RequestError.internalError(data, supportsAirSessionFailures(this.clientCapabilities) ? undefined : rawDetail);
|
|
2234
|
+
/** Attach a pending rate limit to a usage_update, once. A `_claude/rateLimit`
|
|
2235
|
+
* the update already carries wins, the same precedence upstream uses. */
|
|
2236
|
+
const withPendingRateLimit = (update) => {
|
|
2237
|
+
if (pendingRateLimit === null)
|
|
2238
|
+
return update;
|
|
2239
|
+
const rateLimit = pendingRateLimit;
|
|
2240
|
+
pendingRateLimit = null;
|
|
2241
|
+
return { ...update, _meta: { "_claude/rateLimit": rateLimit, ...(update._meta ?? {}) } };
|
|
2242
|
+
};
|
|
2144
2243
|
const resetTurnScratch = () => {
|
|
2145
2244
|
lastAssistantTotalUsage = null;
|
|
2146
2245
|
lastAssistantUsage = null;
|
|
2147
2246
|
lastAssistantModel = null;
|
|
2247
|
+
// A pending event belongs to the turn that measured it; one that found no
|
|
2248
|
+
// usage_update to ride must not be attributed to the next turn. The
|
|
2249
|
+
// tracker already remembers its status (R1.6).
|
|
2250
|
+
pendingRateLimit = null;
|
|
2148
2251
|
lastAssistantError = undefined;
|
|
2149
2252
|
lastAssistantWasUsageLimit = false;
|
|
2150
2253
|
lastAssistantFailureTitle = undefined;
|
|
@@ -3051,6 +3154,7 @@ export class ClaudeAcpAgent {
|
|
|
3051
3154
|
}
|
|
3052
3155
|
}
|
|
3053
3156
|
}
|
|
3157
|
+
await this.reportMcpServerStartupFailures(session, message.mcp_servers, supportsNotices, sendUpdate, params.sessionId);
|
|
3054
3158
|
break;
|
|
3055
3159
|
case "status": {
|
|
3056
3160
|
// Compaction is reported as ONE tool lifecycle rather than the
|
|
@@ -3110,11 +3214,11 @@ export class ClaudeAcpAgent {
|
|
|
3110
3214
|
session.contextUsedTokens = usedTokens ?? 0;
|
|
3111
3215
|
await sendUpdate({
|
|
3112
3216
|
sessionId: params.sessionId,
|
|
3113
|
-
update: {
|
|
3217
|
+
update: withPendingRateLimit({
|
|
3114
3218
|
sessionUpdate: "usage_update",
|
|
3115
3219
|
used: lastAssistantTotalUsage,
|
|
3116
3220
|
size: session.contextWindowSize,
|
|
3117
|
-
},
|
|
3221
|
+
}),
|
|
3118
3222
|
});
|
|
3119
3223
|
break;
|
|
3120
3224
|
}
|
|
@@ -3474,9 +3578,10 @@ export class ClaudeAcpAgent {
|
|
|
3474
3578
|
}
|
|
3475
3579
|
}
|
|
3476
3580
|
const severity = message.level === "warning" ? "warning" : "info";
|
|
3581
|
+
const transcriptContent = collapseHookSpeaker(message.content);
|
|
3477
3582
|
const transcriptText = message.level === "info"
|
|
3478
|
-
?
|
|
3479
|
-
: `**${sentenceCase(message.level)}:** ${
|
|
3583
|
+
? transcriptContent
|
|
3584
|
+
: `**${sentenceCase(message.level)}:** ${transcriptContent}`;
|
|
3480
3585
|
await sendUpdate({
|
|
3481
3586
|
sessionId: params.sessionId,
|
|
3482
3587
|
update: noticeOrTranscriptUpdate({
|
|
@@ -3530,6 +3635,10 @@ export class ClaudeAcpAgent {
|
|
|
3530
3635
|
session.liveBackgroundTasks.set(message.task_id, {
|
|
3531
3636
|
parentToolUseId: message.tool_use_id,
|
|
3532
3637
|
isSubagent: !!message.subagent_type,
|
|
3638
|
+
...(message.is_backgrounded !== undefined
|
|
3639
|
+
? { backgrounded: message.is_backgrounded }
|
|
3640
|
+
: {}),
|
|
3641
|
+
...(message.parent_task_id ? { parentTaskId: message.parent_task_id } : {}),
|
|
3533
3642
|
});
|
|
3534
3643
|
if (message.subagent_type && session.activeTurn && !session.activeTurn.settled) {
|
|
3535
3644
|
(session.activeTurn.spawnedTaskIds ??= new Set()).add(message.task_id);
|
|
@@ -3568,6 +3677,12 @@ export class ClaudeAcpAgent {
|
|
|
3568
3677
|
this.logger.log(`[task/late-run] dropped=task_updated task=${message.task_id} run=${message.run_id}`);
|
|
3569
3678
|
break;
|
|
3570
3679
|
}
|
|
3680
|
+
// A task moved to the background no longer blocks the foreground (#1466).
|
|
3681
|
+
if (message.patch.is_backgrounded !== undefined) {
|
|
3682
|
+
const task = session.liveBackgroundTasks.get(message.task_id);
|
|
3683
|
+
if (task)
|
|
3684
|
+
task.backgrounded = message.patch.is_backgrounded;
|
|
3685
|
+
}
|
|
3571
3686
|
// terminal-status task_updated patch and a (deduplicated)
|
|
3572
3687
|
// task_notification when a task settles, but only the patch is
|
|
3573
3688
|
// guaranteed per transition — prune on it too so the registry
|
|
@@ -3847,7 +3962,7 @@ export class ClaudeAcpAgent {
|
|
|
3847
3962
|
message.stop_reason === "refusal" ||
|
|
3848
3963
|
("result" in message &&
|
|
3849
3964
|
message.is_error &&
|
|
3850
|
-
message.result
|
|
3965
|
+
isLoginRequiredText(message.result));
|
|
3851
3966
|
if (goalCommandFailed) {
|
|
3852
3967
|
await this.publishGoal(params.sessionId, pendingGoalUpdate.previous ?? null);
|
|
3853
3968
|
}
|
|
@@ -3973,7 +4088,7 @@ export class ClaudeAcpAgent {
|
|
|
3973
4088
|
if (lastAssistantTotalUsage !== null) {
|
|
3974
4089
|
await sendUpdate({
|
|
3975
4090
|
sessionId: params.sessionId,
|
|
3976
|
-
update: {
|
|
4091
|
+
update: withPendingRateLimit({
|
|
3977
4092
|
sessionUpdate: "usage_update",
|
|
3978
4093
|
used: lastAssistantTotalUsage,
|
|
3979
4094
|
size: session.contextWindowSize,
|
|
@@ -3984,7 +4099,7 @@ export class ClaudeAcpAgent {
|
|
|
3984
4099
|
...(message.origin && {
|
|
3985
4100
|
_meta: { "_claude/origin": message.origin },
|
|
3986
4101
|
}),
|
|
3987
|
-
},
|
|
4102
|
+
}),
|
|
3988
4103
|
});
|
|
3989
4104
|
}
|
|
3990
4105
|
if (session.cancelled) {
|
|
@@ -4008,7 +4123,7 @@ export class ClaudeAcpAgent {
|
|
|
4008
4123
|
// drained hold at their results is as good as the idle
|
|
4009
4124
|
// fallback.) Then stop: everything below is user-turn
|
|
4010
4125
|
// lifecycle, and an autonomous outcome must never touch it —
|
|
4011
|
-
// its is_error or "
|
|
4126
|
+
// its is_error or "run /login" text would otherwise
|
|
4012
4127
|
// failActive a live turn (the held one, or the user's next
|
|
4013
4128
|
// prompt) whose own result recorded a different outcome.
|
|
4014
4129
|
if (isAutonomousResult) {
|
|
@@ -4128,7 +4243,7 @@ export class ClaudeAcpAgent {
|
|
|
4128
4243
|
}
|
|
4129
4244
|
switch (message.subtype) {
|
|
4130
4245
|
case "success": {
|
|
4131
|
-
if (message.is_error && message.result
|
|
4246
|
+
if (message.is_error && isLoginRequiredText(message.result)) {
|
|
4132
4247
|
await failActiveWithSessionFailure("auth_required", RequestError.authRequired(), message.result);
|
|
4133
4248
|
break;
|
|
4134
4249
|
}
|
|
@@ -4391,11 +4506,11 @@ export class ClaudeAcpAgent {
|
|
|
4391
4506
|
session.contextUsedTokens = nextUsage;
|
|
4392
4507
|
await sendUpdate({
|
|
4393
4508
|
sessionId: params.sessionId,
|
|
4394
|
-
update: {
|
|
4509
|
+
update: withPendingRateLimit({
|
|
4395
4510
|
sessionUpdate: "usage_update",
|
|
4396
4511
|
used: nextUsage,
|
|
4397
4512
|
size: session.contextWindowSize,
|
|
4398
|
-
},
|
|
4513
|
+
}),
|
|
4399
4514
|
});
|
|
4400
4515
|
}
|
|
4401
4516
|
}
|
|
@@ -4532,6 +4647,13 @@ export class ClaudeAcpAgent {
|
|
|
4532
4647
|
message.parent_tool_use_id === null &&
|
|
4533
4648
|
message.message.model === "<synthetic>" &&
|
|
4534
4649
|
compaction.hasDeliveredOutput) {
|
|
4650
|
+
if (isSyntheticUsageLimitMessage(message.message)) {
|
|
4651
|
+
lastAssistantWasUsageLimit = true;
|
|
4652
|
+
lastAssistantFailureTitle = assistantMessageText(message.message);
|
|
4653
|
+
if (message.error) {
|
|
4654
|
+
lastAssistantError = message.error;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4535
4657
|
break;
|
|
4536
4658
|
}
|
|
4537
4659
|
// Snapshot the latest top-level assistant usage and model so the
|
|
@@ -4660,6 +4782,7 @@ export class ClaudeAcpAgent {
|
|
|
4660
4782
|
}
|
|
4661
4783
|
}
|
|
4662
4784
|
let content;
|
|
4785
|
+
let toolUseResult = message.type === "user" ? message.tool_use_result : undefined;
|
|
4663
4786
|
if (message.type === "assistant" && message.parent_tool_use_id === null) {
|
|
4664
4787
|
// Top-level assistant message: each text/thinking block may have
|
|
4665
4788
|
// already been streamed live as deltas. Diff each against what
|
|
@@ -4734,6 +4857,18 @@ export class ClaudeAcpAgent {
|
|
|
4734
4857
|
// `parentToolUseId` stamped by toAcpNotifications.
|
|
4735
4858
|
content = message.message.content.filter((item) => item.type !== "text" && item.type !== "thinking");
|
|
4736
4859
|
}
|
|
4860
|
+
else if (message.parent_tool_use_id !== null &&
|
|
4861
|
+
Array.isArray(message.message.content)) {
|
|
4862
|
+
// Upstream #1431 (#1389): a foreground fork inherits a placeholder result for its
|
|
4863
|
+
// own launch. It completed the Agent call and dropped its tracked state, so the
|
|
4864
|
+
// fork's real report, arriving later, was lost. Only the launching side completes
|
|
4865
|
+
// that call. The message-level sidecar cannot describe a remaining result once its
|
|
4866
|
+
// launch result is removed.
|
|
4867
|
+
const launchId = message.parent_tool_use_id;
|
|
4868
|
+
content = message.message.content.filter((block) => block.type !== "tool_result" || block.tool_use_id !== launchId);
|
|
4869
|
+
if (content.length !== message.message.content.length)
|
|
4870
|
+
toolUseResult = undefined;
|
|
4871
|
+
}
|
|
4737
4872
|
else {
|
|
4738
4873
|
content = message.message.content;
|
|
4739
4874
|
}
|
|
@@ -4761,7 +4896,7 @@ export class ClaudeAcpAgent {
|
|
|
4761
4896
|
taskState: session.taskState,
|
|
4762
4897
|
emittedToolCalls: session.emittedToolCalls,
|
|
4763
4898
|
messageId: messageIdForGrouping(message),
|
|
4764
|
-
toolUseResult
|
|
4899
|
+
toolUseResult,
|
|
4765
4900
|
// On the wire since CLI 2.1.216 but not in SDKUserMessage's
|
|
4766
4901
|
// type, hence the cast. Validated by parseToolResultMeta.
|
|
4767
4902
|
toolResultMeta: message.type === "user"
|
|
@@ -4846,12 +4981,15 @@ export class ClaudeAcpAgent {
|
|
|
4846
4981
|
//
|
|
4847
4982
|
// Recorded OUTSIDE the emission guard below, and before it: the
|
|
4848
4983
|
// memory is fed by the event itself, not by our ability to forward
|
|
4849
|
-
// it, and before the first assistant message of a
|
|
4850
|
-
//
|
|
4851
|
-
//
|
|
4852
|
-
//
|
|
4984
|
+
// it, and before the first assistant message of a turn that guard
|
|
4985
|
+
// is false. The forwarding still sends `rate_limit_info` verbatim on
|
|
4986
|
+
// the same notification shape (R1.8); when the turn has no context
|
|
4987
|
+
// total yet, it waits for the turn's next usage_update (#1534).
|
|
4853
4988
|
this.accountUsageTracker(session).recordMeasured(message.rate_limit_info);
|
|
4854
|
-
if (lastAssistantTotalUsage
|
|
4989
|
+
if (lastAssistantTotalUsage === null) {
|
|
4990
|
+
pendingRateLimit = message.rate_limit_info;
|
|
4991
|
+
}
|
|
4992
|
+
else {
|
|
4855
4993
|
await sendUpdate({
|
|
4856
4994
|
sessionId: params.sessionId,
|
|
4857
4995
|
update: {
|
|
@@ -5378,9 +5516,9 @@ export class ClaudeAcpAgent {
|
|
|
5378
5516
|
// here the client has asked us to close the session, so signalling abort is
|
|
5379
5517
|
// appropriate; query.close() above has already torn the subprocess down.
|
|
5380
5518
|
session.abortController.abort();
|
|
5381
|
-
//
|
|
5382
|
-
//
|
|
5383
|
-
|
|
5519
|
+
// The publisher was disposed once, with its final flush, by
|
|
5520
|
+
// closeQueryStream → retireTaskFeed above (or when the stream closed
|
|
5521
|
+
// earlier). Disposing it again here used to drop that flush (F3).
|
|
5384
5522
|
delete this.sessions[sessionId];
|
|
5385
5523
|
}
|
|
5386
5524
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
@@ -5510,16 +5648,6 @@ export class ClaudeAcpAgent {
|
|
|
5510
5648
|
? option.options.flatMap((o) => ("options" in o ? o.options : [o]))
|
|
5511
5649
|
: [];
|
|
5512
5650
|
let validValue = allValues.find((o) => o.value === params.value);
|
|
5513
|
-
// The option's reported currentValue is always a valid target, even when
|
|
5514
|
-
// it has no options entry: a session running an out-of-picker model
|
|
5515
|
-
// (resumed onto an allowlist-excluded model, or a refusal fallback)
|
|
5516
|
-
// reports a currentValue that isn't selectable, and a client
|
|
5517
|
-
// round-tripping it must not get "Invalid value". It flows through the
|
|
5518
|
-
// normal apply path below — re-asserting an already-current value is
|
|
5519
|
-
// harmless and can repair SDK drift.
|
|
5520
|
-
if (!validValue && option.currentValue === params.value) {
|
|
5521
|
-
validValue = { value: params.value, name: params.value };
|
|
5522
|
-
}
|
|
5523
5651
|
// For model options, fall back to resolveModelPreference when the exact
|
|
5524
5652
|
// value doesn't match. This lets callers use human-friendly aliases like
|
|
5525
5653
|
// "opus" or "sonnet" instead of full model IDs like "claude-opus-4-6".
|
|
@@ -5703,7 +5831,7 @@ export class ClaudeAcpAgent {
|
|
|
5703
5831
|
if (replaySession && replayMessageId && message.uuid) {
|
|
5704
5832
|
replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
|
|
5705
5833
|
}
|
|
5706
|
-
// The live prompt loop converts the synthetic "
|
|
5834
|
+
// The live prompt loop converts the synthetic "run /login"
|
|
5707
5835
|
// assistant message into an authRequired error instead of showing its
|
|
5708
5836
|
// TUI-specific text; skip it on replay too (issue #863).
|
|
5709
5837
|
if (message.type === "assistant" && isSyntheticLoginMessage(message.message)) {
|
|
@@ -6056,6 +6184,21 @@ export class ClaudeAcpAgent {
|
|
|
6056
6184
|
}
|
|
6057
6185
|
if (signal.aborted)
|
|
6058
6186
|
throw new Error("Tool use aborted");
|
|
6187
|
+
// On `session/cancel` the client answers every open permission request
|
|
6188
|
+
// `cancelled`, including those of a background subagent, which the
|
|
6189
|
+
// cancel does not stop. Answer such a request with a denial so the
|
|
6190
|
+
// subagent reads "the user refused" and goes on. Throwing instead makes
|
|
6191
|
+
// Claude Code deny it as "Tool permission request failed".
|
|
6192
|
+
if (response.outcome?.outcome === "cancelled" &&
|
|
6193
|
+
this.sessions[sessionId] === session &&
|
|
6194
|
+
!blocksForeground(session, agentID)) {
|
|
6195
|
+
return {
|
|
6196
|
+
behavior: "deny",
|
|
6197
|
+
message: "User refused permission to run tool",
|
|
6198
|
+
toolUseID,
|
|
6199
|
+
decisionClassification: "user_reject",
|
|
6200
|
+
};
|
|
6201
|
+
}
|
|
6059
6202
|
const decodedPermission = decodeClaudePermissionResponse(response, toolName, toolInput, toolUseID, permissionOptions, durableChangeSet);
|
|
6060
6203
|
let permissionResult = decodedPermission.permissionResult;
|
|
6061
6204
|
const autoFallback = this.sessionModes.applyPermissionFallback(session, permissionResult);
|
|
@@ -6103,18 +6246,20 @@ export class ClaudeAcpAgent {
|
|
|
6103
6246
|
}
|
|
6104
6247
|
/**
|
|
6105
6248
|
* Handle elicitation requests that originate from MCP servers by forwarding
|
|
6106
|
-
* them to the client over ACP.
|
|
6107
|
-
*
|
|
6249
|
+
* them to the client over ACP. Only the user's own decline reaches the MCP
|
|
6250
|
+
* server as `decline`. A request the user never saw, because the client did
|
|
6251
|
+
* not advertise its mode, we can't represent it, or the client answered with
|
|
6252
|
+
* an error, is a `cancel`.
|
|
6108
6253
|
*/
|
|
6109
6254
|
handleMcpElicitation(sessionId, support) {
|
|
6110
6255
|
return async (request, { signal }) => {
|
|
6111
6256
|
const isUrl = request.mode === "url";
|
|
6112
6257
|
if ((isUrl && !support.url) || (!isUrl && !support.form)) {
|
|
6113
|
-
return { action: "
|
|
6258
|
+
return { action: "cancel" };
|
|
6114
6259
|
}
|
|
6115
6260
|
const createRequest = mcpElicitationToCreateRequest(request, sessionId);
|
|
6116
6261
|
if (!createRequest) {
|
|
6117
|
-
return { action: "
|
|
6262
|
+
return { action: "cancel" };
|
|
6118
6263
|
}
|
|
6119
6264
|
try {
|
|
6120
6265
|
const response = await this.withPendingUserInput(sessionId, () => this.client.unstable_createElicitation(createRequest, signal));
|
|
@@ -6129,8 +6274,11 @@ export class ClaudeAcpAgent {
|
|
|
6129
6274
|
if (signal.aborted) {
|
|
6130
6275
|
return { action: "cancel" };
|
|
6131
6276
|
}
|
|
6277
|
+
// The client answered with an error, e.g. -32602 for a form it cannot
|
|
6278
|
+
// render, so the user never saw the request. MCP's `decline` means the
|
|
6279
|
+
// user refused; `cancel` means they made no explicit choice.
|
|
6132
6280
|
this.logger.error(`Failed to forward MCP elicitation: ${error}`);
|
|
6133
|
-
return { action: "
|
|
6281
|
+
return { action: "cancel" };
|
|
6134
6282
|
}
|
|
6135
6283
|
};
|
|
6136
6284
|
}
|
|
@@ -7188,6 +7336,7 @@ export class ClaudeAcpAgent {
|
|
|
7188
7336
|
...providerEnv,
|
|
7189
7337
|
// Opt-in to session state events like when the agent is idle
|
|
7190
7338
|
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
7339
|
+
...STARTUP_FAILURE_RESULTS_ENV,
|
|
7191
7340
|
};
|
|
7192
7341
|
// Plan updates (story 062): the todo tools are on unless an env the user
|
|
7193
7342
|
// wrote -- this spawn env or the settings tier the CLI applies after it --
|
|
@@ -7308,6 +7457,30 @@ export class ClaudeAcpAgent {
|
|
|
7308
7457
|
],
|
|
7309
7458
|
}
|
|
7310
7459
|
: {}),
|
|
7460
|
+
// Upstream #1496: a subagent's working directory, from its own SubagentStart hook,
|
|
7461
|
+
// which Claude Code runs (and waits for) before the child's first model request.
|
|
7462
|
+
...(this.childSessions
|
|
7463
|
+
? {
|
|
7464
|
+
SubagentStart: [
|
|
7465
|
+
...(userProvidedOptions?.hooks?.SubagentStart || []),
|
|
7466
|
+
{
|
|
7467
|
+
hooks: [
|
|
7468
|
+
async (input) => {
|
|
7469
|
+
const childSessions = this.childSessions;
|
|
7470
|
+
if (childSessions &&
|
|
7471
|
+
typeof input.agent_id === "string" &&
|
|
7472
|
+
typeof input.cwd === "string") {
|
|
7473
|
+
await childSessions
|
|
7474
|
+
.reportCwd(input.agent_id, input.cwd, params.cwd)
|
|
7475
|
+
.catch((error) => this.logger.error(`Session ${sessionId}: failed to report subagent cwd: ${error}`));
|
|
7476
|
+
}
|
|
7477
|
+
return { continue: true };
|
|
7478
|
+
},
|
|
7479
|
+
],
|
|
7480
|
+
},
|
|
7481
|
+
],
|
|
7482
|
+
}
|
|
7483
|
+
: {}),
|
|
7311
7484
|
TaskCreated: [
|
|
7312
7485
|
...(userProvidedOptions?.hooks?.TaskCreated || []),
|
|
7313
7486
|
{
|
|
@@ -7376,13 +7549,23 @@ export class ClaudeAcpAgent {
|
|
|
7376
7549
|
initializationResult = await q.initializationResult();
|
|
7377
7550
|
}
|
|
7378
7551
|
catch (error) {
|
|
7552
|
+
// The CLI refused or exited: release the query so nothing of it is left behind (upstream
|
|
7553
|
+
// closes it here too).
|
|
7554
|
+
try {
|
|
7555
|
+
q.close();
|
|
7556
|
+
}
|
|
7557
|
+
catch (closeError) {
|
|
7558
|
+
this.logger.error(`Failed to close a query that did not initialize: ${closeError}`);
|
|
7559
|
+
}
|
|
7379
7560
|
if (creationOpts.resume &&
|
|
7380
7561
|
error instanceof Error &&
|
|
7381
7562
|
(error.message === "Query closed before response received" ||
|
|
7382
7563
|
error.message.includes("No conversation found with session ID"))) {
|
|
7383
7564
|
throw RequestError.resourceNotFound(sessionId);
|
|
7384
7565
|
}
|
|
7385
|
-
|
|
7566
|
+
// Upstream #1508: the CLI's own reason (bad proxy URL, wrong org sign-in, …) as an ACP
|
|
7567
|
+
// error, instead of "Claude Code process exited with code 1".
|
|
7568
|
+
throw startupFailureError(error) ?? error;
|
|
7386
7569
|
}
|
|
7387
7570
|
if (shouldHideClaudeAuth() &&
|
|
7388
7571
|
initializationResult.account.subscriptionType &&
|
|
@@ -8019,6 +8202,21 @@ planUpdates) {
|
|
|
8019
8202
|
}),
|
|
8020
8203
|
},
|
|
8021
8204
|
];
|
|
8205
|
+
// Upstream #1463: ACP requires `currentValue` to be one of `options`
|
|
8206
|
+
// (agent-client-protocol#2557). A session can run a model the picker does not list --
|
|
8207
|
+
// resumed onto a model the `availableModels` allowlist excludes, or switched by a refusal
|
|
8208
|
+
// fallback -- so list it as its own entry. It is not added to `availableModels`, so the
|
|
8209
|
+
// next rebuild after switching to a listed model drops it.
|
|
8210
|
+
const modelOption = options[1];
|
|
8211
|
+
const modelRows = modelOption.options;
|
|
8212
|
+
if (!modelRows.some((row) => row.value === models.currentModelId)) {
|
|
8213
|
+
const info = modelInfos.find((model) => model.value === models.currentModelId);
|
|
8214
|
+
modelRows.push({
|
|
8215
|
+
value: models.currentModelId,
|
|
8216
|
+
name: info?.displayName || models.currentModelId,
|
|
8217
|
+
...(info?.description ? { description: info.description } : {}),
|
|
8218
|
+
});
|
|
8219
|
+
}
|
|
8022
8220
|
// Add effort level option based on the currently selected model
|
|
8023
8221
|
const currentModelInfo = modelInfos.find((m) => m.value === models.currentModelId);
|
|
8024
8222
|
const supportedLevels = currentModelInfo?.supportsEffort
|
|
@@ -8626,17 +8824,26 @@ resumedSessionId) {
|
|
|
8626
8824
|
// not the raw catalog upstream maps here: the filter is visibility-only,
|
|
8627
8825
|
// so preference resolution and capability lookups elsewhere keep reading
|
|
8628
8826
|
// the unfiltered catalog (R4.3).
|
|
8629
|
-
availableModels: pickerModels.map((model) => ({
|
|
8827
|
+
availableModels: withDistinctModelNames(pickerModels.map((model) => ({
|
|
8630
8828
|
modelId: model.value,
|
|
8631
8829
|
name: model.displayName,
|
|
8632
8830
|
description: model.description,
|
|
8633
|
-
})),
|
|
8831
|
+
}))),
|
|
8634
8832
|
currentModelId: currentModel.value,
|
|
8635
8833
|
},
|
|
8636
8834
|
resumedContextWindow,
|
|
8637
8835
|
resumedContextUsedTokens,
|
|
8638
8836
|
};
|
|
8639
8837
|
}
|
|
8838
|
+
/** Upstream #1286 (issue #1246): an allowlist can list an alias and pinned ids that inherit the
|
|
8839
|
+
* same SDK label, so a name-only picker shows identical rows. A name that occurs more than once
|
|
8840
|
+
* gets its model id appended; every row stays, and distinct names are unchanged. */
|
|
8841
|
+
export function withDistinctModelNames(models) {
|
|
8842
|
+
const counts = new Map();
|
|
8843
|
+
for (const model of models)
|
|
8844
|
+
counts.set(model.name, (counts.get(model.name) ?? 0) + 1);
|
|
8845
|
+
return models.map((model) => counts.get(model.name) === 1 ? model : { ...model, name: `${model.name} (${model.modelId})` });
|
|
8846
|
+
}
|
|
8640
8847
|
function getAvailableSlashCommands(commands,
|
|
8641
8848
|
// Names the CLI tagged terminal-bound on `system`/init (their UX lives in
|
|
8642
8849
|
// the CLI's own terminal, which ACP clients aren't) — filtered alongside
|
|
@@ -8962,6 +9169,8 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
8962
9169
|
const taskState = options?.taskState ?? new Map();
|
|
8963
9170
|
const registerHooks = options?.registerHooks !== false;
|
|
8964
9171
|
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
9172
|
+
// §5.1: the task list as a keyed plan when the client applies them.
|
|
9173
|
+
const keyedPlans = supportsPlanOperations(options?.clientCapabilities);
|
|
8965
9174
|
if (typeof content === "string") {
|
|
8966
9175
|
if (content.length === 0 || containsFileChangeAuditMarker(content)) {
|
|
8967
9176
|
return [];
|
|
@@ -9063,10 +9272,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9063
9272
|
else if (chunk.name === "TodoWrite") {
|
|
9064
9273
|
// @ts-expect-error - sometimes input is empty object or undefined
|
|
9065
9274
|
if (Array.isArray(chunk.input?.todos)) {
|
|
9066
|
-
update =
|
|
9067
|
-
sessionUpdate: "plan",
|
|
9068
|
-
entries: planEntries(chunk.input),
|
|
9069
|
-
};
|
|
9275
|
+
update = planSessionUpdate(planEntries(chunk.input), keyedPlans);
|
|
9070
9276
|
}
|
|
9071
9277
|
}
|
|
9072
9278
|
else if (isTaskTool(chunk.name)) {
|
|
@@ -9144,6 +9350,26 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9144
9350
|
}
|
|
9145
9351
|
break;
|
|
9146
9352
|
}
|
|
9353
|
+
case "advisor_tool_result": {
|
|
9354
|
+
// Advisor results arrive in assistant content, including encrypted advice.
|
|
9355
|
+
// Resolve the server tool through the normal result path without rendering
|
|
9356
|
+
// opaque ciphertext. Server tools have no later PostToolUse hook to await.
|
|
9357
|
+
const result = chunk.content;
|
|
9358
|
+
unregisterHookCallback(chunk.tool_use_id);
|
|
9359
|
+
output.push(...toAcpNotifications([
|
|
9360
|
+
{
|
|
9361
|
+
type: "tool_result",
|
|
9362
|
+
tool_use_id: chunk.tool_use_id,
|
|
9363
|
+
is_error: result.type === "advisor_tool_result_error",
|
|
9364
|
+
content: result.type === "advisor_result"
|
|
9365
|
+
? result.text
|
|
9366
|
+
: result.type === "advisor_redacted_result"
|
|
9367
|
+
? "Advisor guidance applied server-side."
|
|
9368
|
+
: `Advisor error: ${result.error_code}`,
|
|
9369
|
+
},
|
|
9370
|
+
], role, sessionId, toolUseCache, client, logger, options));
|
|
9371
|
+
break;
|
|
9372
|
+
}
|
|
9147
9373
|
case "tool_result":
|
|
9148
9374
|
case "tool_search_tool_result":
|
|
9149
9375
|
case "web_fetch_tool_result":
|
|
@@ -9252,10 +9478,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9252
9478
|
}
|
|
9253
9479
|
}
|
|
9254
9480
|
if (shouldEmitTaskPlan) {
|
|
9255
|
-
update =
|
|
9256
|
-
sessionUpdate: "plan",
|
|
9257
|
-
entries: taskStateToPlanEntries(taskState),
|
|
9258
|
-
};
|
|
9481
|
+
update = planSessionUpdate(taskStateToPlanEntries(taskState), keyedPlans);
|
|
9259
9482
|
}
|
|
9260
9483
|
}
|
|
9261
9484
|
else if (toolUse.name !== "TodoWrite") {
|
|
@@ -9316,7 +9539,6 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
9316
9539
|
case "container_upload":
|
|
9317
9540
|
case "compaction":
|
|
9318
9541
|
case "compaction_delta":
|
|
9319
|
-
case "advisor_tool_result":
|
|
9320
9542
|
case "fallback":
|
|
9321
9543
|
case "mcp_tool_listing":
|
|
9322
9544
|
break;
|