@agentclientprotocol/claude-agent-acp 0.40.0 → 0.42.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 +58 -13
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +376 -60
- package/package.json +2 -2
package/dist/acp-agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse,
|
|
1
|
+
import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
|
|
2
2
|
import { CanUseTool, ModelInfo, Options, PermissionMode, PermissionUpdate, Query, SDKMessageOrigin, SDKPartialAssistantMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
|
4
4
|
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
|
@@ -52,6 +52,17 @@ type Session = {
|
|
|
52
52
|
}>;
|
|
53
53
|
nextPendingOrder: number;
|
|
54
54
|
abortController: AbortController;
|
|
55
|
+
/** Per-turn signal the active prompt loop races `query.next()` against.
|
|
56
|
+
* Aborted by cancel() (after a grace period) to force the loop to return
|
|
57
|
+
* "cancelled" when the SDK is wedged and `query.next()` never yields again
|
|
58
|
+
* (issue #680). Distinct from `abortController`: this only wakes the loop;
|
|
59
|
+
* it does NOT touch the SDK query/subprocess. Undefined when no prompt is
|
|
60
|
+
* actively consuming the query. */
|
|
61
|
+
cancelController?: AbortController;
|
|
62
|
+
/** Pending grace-period timer that aborts `cancelController`. Cleared when
|
|
63
|
+
* the loop returns normally so the backstop never fires after a clean
|
|
64
|
+
* cancel. */
|
|
65
|
+
forceCancelTimer?: ReturnType<typeof setTimeout>;
|
|
55
66
|
emitRawSDKMessages: boolean | SDKMessageFilter[];
|
|
56
67
|
/** Context window size of the last top-level assistant model, carried across
|
|
57
68
|
* prompts so mid-stream usage_update notifications report a correct `size`
|
|
@@ -62,14 +73,30 @@ type Session = {
|
|
|
62
73
|
/** Accumulated task list for the session, keyed by task ID. Task IDs are
|
|
63
74
|
* per-session, so this state must not be shared across sessions. */
|
|
64
75
|
taskState: TaskState;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
/** Caches `tool_use` blocks by id so the matching `tool_result` can recover
|
|
77
|
+
* the tool name/input when mapping it to a `tool_call_update`. Per-session
|
|
78
|
+
* (tool_use ids are only unique within a session) and pruned at
|
|
79
|
+
* `tool_result` time so a long-running session doesn't accumulate every
|
|
80
|
+
* tool call for its whole lifetime. */
|
|
81
|
+
toolUseCache: ToolUseCache;
|
|
82
|
+
/** Maps the ACP `messageId` we expose to clients (see `messageIdForGrouping`)
|
|
83
|
+
* to the SDK message uuid that the Agent SDK's rewind/resume APIs key on
|
|
84
|
+
* (`Query.rewindFiles` takes a user-message uuid; `resumeSessionAt` takes an
|
|
85
|
+
* `SDKAssistantMessage.uuid`). For assistant turns the two differ — the ACP
|
|
86
|
+
* id is the Anthropic API message id (`msg_…`), available at `message_start`
|
|
87
|
+
* so streamed chunks can carry it, while the uuid only arrives on the
|
|
88
|
+
* consolidated message — so a client can only ask to rewind/fork by the id it
|
|
89
|
+
* was given, and we need this table to translate it back.
|
|
90
|
+
*
|
|
91
|
+
* Populated as a byproduct of the message loop (the consolidated message
|
|
92
|
+
* carries both ids) and of `replaySessionHistory` on load, so no extra
|
|
93
|
+
* `getSessionMessages` read is needed at rewind time. Last-write-wins
|
|
94
|
+
* naturally yields the turn-boundary uuid when one `msg_…` spans several
|
|
95
|
+
* content-block messages.
|
|
96
|
+
*
|
|
97
|
+
* NOT READ YET — recorded now so the mapping exists if/when we wire up
|
|
98
|
+
* fork/rewind. */
|
|
99
|
+
messageIdToUuid: Map<string, string>;
|
|
73
100
|
};
|
|
74
101
|
export type SDKMessageFilter = {
|
|
75
102
|
type: string;
|
|
@@ -177,13 +204,13 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
177
204
|
[key: string]: Session;
|
|
178
205
|
};
|
|
179
206
|
client: AgentSideConnection;
|
|
180
|
-
toolUseCache: ToolUseCache;
|
|
181
|
-
backgroundTerminals: {
|
|
182
|
-
[key: string]: BackgroundTerminal;
|
|
183
|
-
};
|
|
184
207
|
clientCapabilities?: ClientCapabilities;
|
|
185
208
|
logger: Logger;
|
|
186
209
|
gatewayAuthRequest?: GatewayAuthRequest;
|
|
210
|
+
/** Grace period before a `session/cancel` forces a wedged prompt loop to
|
|
211
|
+
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
212
|
+
* tests can shrink it. */
|
|
213
|
+
forceCancelGraceMs: number;
|
|
187
214
|
constructor(client: AgentSideConnection, logger?: Logger);
|
|
188
215
|
initialize(request: InitializeRequest): Promise<InitializeResponse>;
|
|
189
216
|
newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
|
|
@@ -215,6 +242,22 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
215
242
|
private createSession;
|
|
216
243
|
}
|
|
217
244
|
export declare function promptToClaude(prompt: PromptRequest): SDKUserMessage;
|
|
245
|
+
/**
|
|
246
|
+
* Resolves the ACP `messageId` for a Claude SDK message (live) or a persisted
|
|
247
|
+
* transcript message (replay) so chunk grouping is identical in both views.
|
|
248
|
+
*
|
|
249
|
+
* Assistant turns are keyed by the Anthropic API message id (`message.id`),
|
|
250
|
+
* which is identical at `message_start`, on the consolidated assistant message,
|
|
251
|
+
* and in the persisted transcript — unlike the per-`stream_event` uuid, which is
|
|
252
|
+
* unique per event and never persisted. User messages have no API id, but they
|
|
253
|
+
* are never streamed, so their (stable) SDK uuid is used instead. ACP message
|
|
254
|
+
* ids are opaque strings, so no particular format is required.
|
|
255
|
+
*/
|
|
256
|
+
export declare function messageIdForGrouping(message: {
|
|
257
|
+
type?: string;
|
|
258
|
+
uuid?: string | null;
|
|
259
|
+
message?: unknown;
|
|
260
|
+
}): string | undefined;
|
|
218
261
|
/**
|
|
219
262
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
220
263
|
* Only handles text, image, and thinking chunks for now.
|
|
@@ -225,11 +268,13 @@ export declare function toAcpNotifications(content: string | ContentBlockParam[]
|
|
|
225
268
|
parentToolUseId?: string | null;
|
|
226
269
|
cwd?: string;
|
|
227
270
|
taskState?: TaskState;
|
|
271
|
+
messageId?: string;
|
|
228
272
|
}): SessionNotification[];
|
|
229
273
|
export declare function streamEventToAcpNotifications(message: SDKPartialAssistantMessage, sessionId: string, toolUseCache: ToolUseCache, client: AgentSideConnection, logger: Logger, options?: {
|
|
230
274
|
clientCapabilities?: ClientCapabilities;
|
|
231
275
|
cwd?: string;
|
|
232
276
|
taskState?: TaskState;
|
|
277
|
+
messageId?: string;
|
|
233
278
|
}): SessionNotification[];
|
|
234
279
|
export declare function runAcp(): {
|
|
235
280
|
connection: AgentSideConnection;
|
package/dist/acp-agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acp-agent.d.ts","sourceRoot":"","sources":["../src/acp-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,mBAAmB,EACnB,mBAAmB,EAGnB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EAEnB,iBAAiB,EACjB,kBAAkB,EAElB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,6BAA6B,EAC7B,8BAA8B,EAC9B,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,
|
|
1
|
+
{"version":3,"file":"acp-agent.d.ts","sourceRoot":"","sources":["../src/acp-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/acp-agent.js
CHANGED
|
@@ -27,6 +27,16 @@ const ZERO_USAGE = Object.freeze({
|
|
|
27
27
|
cache_creation_input_tokens: 0,
|
|
28
28
|
});
|
|
29
29
|
const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
30
|
+
/** Floor after `session/cancel` before the adapter forces the active prompt
|
|
31
|
+
* loop to return "cancelled". `query.interrupt()` normally makes the SDK
|
|
32
|
+
* yield a trailing idle within milliseconds, and the loop returns through its
|
|
33
|
+
* usual path — so this timer is armed and cleared, never fired, on healthy
|
|
34
|
+
* cancels. It only trips when the SDK is genuinely wedged (e.g. a
|
|
35
|
+
* `TaskOutput { block: true }` poll against a hung background task — issue
|
|
36
|
+
* #680) and never yields. The value is deliberately loose: it's an
|
|
37
|
+
* "obviously stuck" ceiling, not a guess at interrupt latency, so it can't
|
|
38
|
+
* pre-empt a slow-but-healthy interrupt. */
|
|
39
|
+
const DEFAULT_FORCE_CANCEL_GRACE_MS = 30_000;
|
|
30
40
|
/** Compute a stable fingerprint of the session-defining params so we can
|
|
31
41
|
* detect when a loadSession/resumeSession call requires tearing down and
|
|
32
42
|
* recreating the underlying Query process. MCP servers are sorted by name
|
|
@@ -233,10 +243,12 @@ export function describeAlwaysAllow(suggestions, toolName) {
|
|
|
233
243
|
// Implement the ACP Agent interface
|
|
234
244
|
export class ClaudeAcpAgent {
|
|
235
245
|
constructor(client, logger) {
|
|
236
|
-
|
|
246
|
+
/** Grace period before a `session/cancel` forces a wedged prompt loop to
|
|
247
|
+
* return "cancelled". See {@link DEFAULT_FORCE_CANCEL_GRACE_MS}. Mutable so
|
|
248
|
+
* tests can shrink it. */
|
|
249
|
+
this.forceCancelGraceMs = DEFAULT_FORCE_CANCEL_GRACE_MS;
|
|
237
250
|
this.sessions = {};
|
|
238
251
|
this.client = client;
|
|
239
|
-
this.toolUseCache = {};
|
|
240
252
|
this.logger = logger ?? console;
|
|
241
253
|
}
|
|
242
254
|
async initialize(request) {
|
|
@@ -463,6 +475,12 @@ export class ClaudeAcpAgent {
|
|
|
463
475
|
// forward it to clients as structured `data`, sparing them from
|
|
464
476
|
// pattern-matching on the human-readable message text.
|
|
465
477
|
let lastAssistantError;
|
|
478
|
+
// When a streaming classifier refuses a turn, the assistant message carries
|
|
479
|
+
// stop_reason "refusal" and structured stop_details. We capture the
|
|
480
|
+
// human-readable explanation here so the terminal `result` can surface it
|
|
481
|
+
// to the user (the refused assistant message itself usually has no content)
|
|
482
|
+
// and report ACP's dedicated `refusal` stop reason.
|
|
483
|
+
let lastRefusalExplanation = null;
|
|
466
484
|
// Tracks whether we're inside a compaction. The SDK emits the terminal
|
|
467
485
|
// `status` (compact_result success/failed) twice for a single failed
|
|
468
486
|
// compaction, and the two messages are indistinguishable — so we report the
|
|
@@ -470,6 +488,17 @@ export class ClaudeAcpAgent {
|
|
|
470
488
|
// `compacting` status sets it again, so every distinct compaction (e.g.
|
|
471
489
|
// repeated auto-compactions in a long turn) is still shown.
|
|
472
490
|
let compactionInProgress = false;
|
|
491
|
+
// Holds the Anthropic API message id of the assistant message currently
|
|
492
|
+
// being streamed, captured from `message_start` so every streamed chunk can
|
|
493
|
+
// be tagged with it. We use the API message id rather than the
|
|
494
|
+
// per-`stream_event` uuid because the same id is also present on the
|
|
495
|
+
// consolidated assistant message and in the persisted transcript — so a turn
|
|
496
|
+
// keeps the same ACP `messageId` whether it is streamed live or replayed
|
|
497
|
+
// from history. The per-event uuid is unique per event and never persisted.
|
|
498
|
+
// A single value suffices because every streaming partial arrives with
|
|
499
|
+
// `parent_tool_use_id === null` (subagent work is folded into tool-result
|
|
500
|
+
// messages, never surfaced as partial streams).
|
|
501
|
+
let currentStreamMessageId;
|
|
473
502
|
const userMessage = promptToClaude(params);
|
|
474
503
|
const promptUuid = randomUUID();
|
|
475
504
|
userMessage.uuid = promptUuid;
|
|
@@ -495,9 +524,26 @@ export class ClaudeAcpAgent {
|
|
|
495
524
|
let handedOff = false;
|
|
496
525
|
let errored = false;
|
|
497
526
|
let stopReason = "end_turn";
|
|
527
|
+
// Wake-up channel so cancel() can force this loop to return "cancelled"
|
|
528
|
+
// even when query.next() is wedged and never yields again (issue #680).
|
|
529
|
+
const cancelController = new AbortController();
|
|
530
|
+
session.cancelController = cancelController;
|
|
531
|
+
const cancelled = new Promise((resolve) => {
|
|
532
|
+
cancelController.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
533
|
+
});
|
|
498
534
|
try {
|
|
499
535
|
while (true) {
|
|
500
|
-
const
|
|
536
|
+
const nextMessage = session.query.next();
|
|
537
|
+
const next = await Promise.race([nextMessage, cancelled]);
|
|
538
|
+
if (cancelController.signal.aborted) {
|
|
539
|
+
// The SDK never yielded after interrupt() (e.g. a wedged TaskOutput
|
|
540
|
+
// block). Abandon the in-flight next() — swallowing any later
|
|
541
|
+
// rejection so it can't surface as an unhandled rejection — and
|
|
542
|
+
// honor the cancel per the ACP contract.
|
|
543
|
+
void nextMessage.catch(() => { });
|
|
544
|
+
return { stopReason: "cancelled" };
|
|
545
|
+
}
|
|
546
|
+
const { value: message, done } = next;
|
|
501
547
|
if (done || !message) {
|
|
502
548
|
if (session.cancelled) {
|
|
503
549
|
return { stopReason: "cancelled" };
|
|
@@ -554,28 +600,35 @@ export class ClaudeAcpAgent {
|
|
|
554
600
|
break;
|
|
555
601
|
}
|
|
556
602
|
case "compact_boundary": {
|
|
557
|
-
//
|
|
558
|
-
// the stale pre-compaction
|
|
603
|
+
// Refresh the displayed usage immediately so the client doesn't
|
|
604
|
+
// keep showing the stale pre-compaction size (e.g. "944k/1m")
|
|
605
|
+
// right after the user sees "Compacting completed", which is
|
|
606
|
+
// confusing and wrong.
|
|
607
|
+
//
|
|
608
|
+
// Prefer the SDK's authoritative post-compaction `used` via
|
|
609
|
+
// getContextUsage — it reflects the real retained context
|
|
610
|
+
// (system prompt + tools + surviving messages), which the
|
|
611
|
+
// per-message API usage numbers can't give us until the next
|
|
612
|
+
// turn's result. If the control request fails, fall back to the
|
|
613
|
+
// used:0 approximation: directionally correct (context just
|
|
614
|
+
// dropped dramatically) and replaced within seconds by the next
|
|
615
|
+
// result message.
|
|
559
616
|
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// just dropped dramatically — and the real value replaces it
|
|
564
|
-
// within seconds when the next result message arrives.
|
|
565
|
-
// The alternative (no update) leaves the client showing e.g.
|
|
566
|
-
// "944k/1m" right after the user sees "Compacting completed",
|
|
567
|
-
// which is confusing and wrong.
|
|
617
|
+
// `size` keeps coming from session.contextWindowSize (learned
|
|
618
|
+
// from modelUsage / the model heuristic) — getContextUsage's
|
|
619
|
+
// window field under-reports extended 1M windows.
|
|
568
620
|
//
|
|
569
621
|
// The "Compacting completed." text is emitted from the `status`
|
|
570
622
|
// handler (keyed on `compact_result`), not here, so the failure
|
|
571
623
|
// path gets a message too.
|
|
572
|
-
|
|
624
|
+
const usedTokens = await fetchContextUsedTokens(session.query, this.logger);
|
|
573
625
|
lastAssistantUsage = null;
|
|
626
|
+
lastAssistantTotalUsage = usedTokens ?? 0;
|
|
574
627
|
await this.client.sessionUpdate({
|
|
575
628
|
sessionId: message.session_id,
|
|
576
629
|
update: {
|
|
577
630
|
sessionUpdate: "usage_update",
|
|
578
|
-
used:
|
|
631
|
+
used: lastAssistantTotalUsage,
|
|
579
632
|
size: session.contextWindowSize,
|
|
580
633
|
},
|
|
581
634
|
});
|
|
@@ -637,6 +690,63 @@ export class ClaudeAcpAgent {
|
|
|
637
690
|
});
|
|
638
691
|
break;
|
|
639
692
|
}
|
|
693
|
+
case "commands_changed": {
|
|
694
|
+
// Push the full slash-command list after a mid-session change
|
|
695
|
+
// (e.g. skills discovered dynamically as the agent works in a
|
|
696
|
+
// subdirectory). The client should REPLACE its cached command
|
|
697
|
+
// list with this payload: supportedCommands() is captured once
|
|
698
|
+
// at initialize and never reflects mid-session changes, so we
|
|
699
|
+
// forward message.commands directly rather than re-querying.
|
|
700
|
+
await this.client.sessionUpdate({
|
|
701
|
+
sessionId: message.session_id,
|
|
702
|
+
update: {
|
|
703
|
+
sessionUpdate: "available_commands_update",
|
|
704
|
+
availableCommands: getAvailableSlashCommands(message.commands),
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
case "mirror_error": {
|
|
710
|
+
// The SDK failed to persist session history (SessionStore
|
|
711
|
+
// append rejected/timed out after retry) — potential data loss
|
|
712
|
+
// the user should know about rather than a silent gap on
|
|
713
|
+
// resume. Log it and surface a warning in the conversation.
|
|
714
|
+
this.logger.error(`Session ${message.session_id}: failed to persist history: ${message.error}`);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "permission_denied": {
|
|
718
|
+
// A tool call was auto-denied (by a rule, the classifier,
|
|
719
|
+
// dontAsk mode, etc.) before running. The tool_use block was
|
|
720
|
+
// already emitted as a `tool_call`, so mark it failed with the
|
|
721
|
+
// rejection reason — otherwise the client shows a tool call
|
|
722
|
+
// that silently never resolves.
|
|
723
|
+
const reason = message.decision_reason ?? message.message;
|
|
724
|
+
await this.client.sessionUpdate({
|
|
725
|
+
sessionId: message.session_id,
|
|
726
|
+
update: {
|
|
727
|
+
sessionUpdate: "tool_call_update",
|
|
728
|
+
toolCallId: message.tool_use_id,
|
|
729
|
+
status: "failed",
|
|
730
|
+
content: [
|
|
731
|
+
{
|
|
732
|
+
type: "content",
|
|
733
|
+
content: { type: "text", text: `Permission denied: ${reason}` },
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
_meta: {
|
|
737
|
+
claudeCode: {
|
|
738
|
+
toolName: message.tool_name,
|
|
739
|
+
toolResponse: {
|
|
740
|
+
decisionReasonType: message.decision_reason_type,
|
|
741
|
+
decisionReason: message.decision_reason,
|
|
742
|
+
message: message.message,
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
640
750
|
case "hook_started":
|
|
641
751
|
case "hook_progress":
|
|
642
752
|
case "hook_response":
|
|
@@ -649,8 +759,6 @@ export class ClaudeAcpAgent {
|
|
|
649
759
|
case "plugin_install":
|
|
650
760
|
case "notification":
|
|
651
761
|
case "api_retry":
|
|
652
|
-
case "mirror_error":
|
|
653
|
-
case "permission_denied":
|
|
654
762
|
case "thinking_tokens":
|
|
655
763
|
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
656
764
|
break;
|
|
@@ -704,6 +812,25 @@ export class ClaudeAcpAgent {
|
|
|
704
812
|
}
|
|
705
813
|
break;
|
|
706
814
|
}
|
|
815
|
+
// A refusal can arrive on any result subtype (and may even set
|
|
816
|
+
// is_error), so handle it before the subtype switch — otherwise the
|
|
817
|
+
// is_error throw below would surface it as an internal error. The
|
|
818
|
+
// refused assistant message carries no visible content, so surface
|
|
819
|
+
// the classifier's explanation (when available) and report ACP's
|
|
820
|
+
// dedicated `refusal` stop reason.
|
|
821
|
+
if (message.stop_reason === "refusal" && !isTaskNotification) {
|
|
822
|
+
if (lastRefusalExplanation) {
|
|
823
|
+
await this.client.sessionUpdate({
|
|
824
|
+
sessionId: params.sessionId,
|
|
825
|
+
update: {
|
|
826
|
+
sessionUpdate: "agent_message_chunk",
|
|
827
|
+
content: { type: "text", text: lastRefusalExplanation },
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
stopReason = "refusal";
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
707
834
|
switch (message.subtype) {
|
|
708
835
|
case "success": {
|
|
709
836
|
if (message.result.includes("Please run /login")) {
|
|
@@ -723,7 +850,7 @@ export class ClaudeAcpAgent {
|
|
|
723
850
|
// Task-notification followups never originate from a user
|
|
724
851
|
// slash command, so skip the forwarding for them.
|
|
725
852
|
if (isLocalOnlyCommand && !isTaskNotification) {
|
|
726
|
-
for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId,
|
|
853
|
+
for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, session.toolUseCache, this.client, this.logger)) {
|
|
727
854
|
await this.client.sessionUpdate(notification);
|
|
728
855
|
}
|
|
729
856
|
}
|
|
@@ -761,6 +888,12 @@ export class ClaudeAcpAgent {
|
|
|
761
888
|
break;
|
|
762
889
|
}
|
|
763
890
|
case "stream_event": {
|
|
891
|
+
// `message_start` carries the Anthropic API message id; capture it
|
|
892
|
+
// so the streamed chunks that follow (whose delta events don't carry
|
|
893
|
+
// it) can all be tagged with the same, replay-stable id.
|
|
894
|
+
if (message.event.type === "message_start") {
|
|
895
|
+
currentStreamMessageId = message.event.message.id || undefined;
|
|
896
|
+
}
|
|
764
897
|
if (message.parent_tool_use_id === null &&
|
|
765
898
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
766
899
|
if (message.event.type === "message_start") {
|
|
@@ -808,10 +941,11 @@ export class ClaudeAcpAgent {
|
|
|
808
941
|
});
|
|
809
942
|
}
|
|
810
943
|
}
|
|
811
|
-
for (const notification of streamEventToAcpNotifications(message, params.sessionId,
|
|
944
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
812
945
|
clientCapabilities: this.clientCapabilities,
|
|
813
946
|
cwd: session.cwd,
|
|
814
947
|
taskState: session.taskState,
|
|
948
|
+
messageId: currentStreamMessageId,
|
|
815
949
|
})) {
|
|
816
950
|
await this.client.sessionUpdate(notification);
|
|
817
951
|
}
|
|
@@ -822,6 +956,14 @@ export class ClaudeAcpAgent {
|
|
|
822
956
|
if (session.cancelled) {
|
|
823
957
|
break;
|
|
824
958
|
}
|
|
959
|
+
// Record the ACP messageId -> SDK uuid mapping for this message. The
|
|
960
|
+
// consolidated message carries both ids, so this is where we learn
|
|
961
|
+
// the uuid that the SDK's rewind/resume APIs key on for the id we
|
|
962
|
+
// hand clients. Not read yet (see Session.messageIdToUuid).
|
|
963
|
+
const mappedMessageId = messageIdForGrouping(message);
|
|
964
|
+
if (mappedMessageId && typeof message.uuid === "string" && message.uuid.length > 0) {
|
|
965
|
+
session.messageIdToUuid.set(mappedMessageId, message.uuid);
|
|
966
|
+
}
|
|
825
967
|
// Check for prompt replay
|
|
826
968
|
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
827
969
|
if (message.uuid === promptUuid) {
|
|
@@ -854,6 +996,9 @@ export class ClaudeAcpAgent {
|
|
|
854
996
|
if (message.error) {
|
|
855
997
|
lastAssistantError = message.error;
|
|
856
998
|
}
|
|
999
|
+
if (message.message.stop_reason === "refusal") {
|
|
1000
|
+
lastRefusalExplanation = message.message.stop_details?.explanation ?? null;
|
|
1001
|
+
}
|
|
857
1002
|
}
|
|
858
1003
|
// Strip <command-*>/<local-command-stdout> markers and render any
|
|
859
1004
|
// remaining prose. Skill bodies and built-in slash commands (e.g.
|
|
@@ -865,11 +1010,12 @@ export class ClaudeAcpAgent {
|
|
|
865
1010
|
message.message.content.includes("<local-command-stdout>")) {
|
|
866
1011
|
const stripped = stripLocalCommandMetadata(message.message.content);
|
|
867
1012
|
if (typeof stripped === "string") {
|
|
868
|
-
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId,
|
|
1013
|
+
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
869
1014
|
clientCapabilities: this.clientCapabilities,
|
|
870
1015
|
parentToolUseId: message.parent_tool_use_id,
|
|
871
1016
|
cwd: session.cwd,
|
|
872
1017
|
taskState: session.taskState,
|
|
1018
|
+
messageId: messageIdForGrouping(message),
|
|
873
1019
|
})) {
|
|
874
1020
|
await this.client.sessionUpdate(notification);
|
|
875
1021
|
}
|
|
@@ -907,21 +1053,51 @@ export class ClaudeAcpAgent {
|
|
|
907
1053
|
? // Handled by stream events above
|
|
908
1054
|
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
909
1055
|
: message.message.content;
|
|
910
|
-
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId,
|
|
1056
|
+
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
911
1057
|
clientCapabilities: this.clientCapabilities,
|
|
912
1058
|
parentToolUseId: message.parent_tool_use_id,
|
|
913
1059
|
cwd: session.cwd,
|
|
914
1060
|
taskState: session.taskState,
|
|
1061
|
+
messageId: messageIdForGrouping(message),
|
|
915
1062
|
})) {
|
|
916
1063
|
await this.client.sessionUpdate(notification);
|
|
917
1064
|
}
|
|
918
1065
|
break;
|
|
919
1066
|
}
|
|
920
|
-
case "tool_progress":
|
|
1067
|
+
case "tool_progress": {
|
|
1068
|
+
await this.client.sessionUpdate({
|
|
1069
|
+
sessionId: message.session_id,
|
|
1070
|
+
update: {
|
|
1071
|
+
sessionUpdate: "tool_call_update",
|
|
1072
|
+
toolCallId: message.tool_use_id,
|
|
1073
|
+
status: "in_progress",
|
|
1074
|
+
_meta: {
|
|
1075
|
+
claudeCode: {
|
|
1076
|
+
toolName: message.tool_name,
|
|
1077
|
+
toolResponse: { elapsedTimeSeconds: message.elapsed_time_seconds },
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case "rate_limit_event": {
|
|
1085
|
+
if (lastAssistantTotalUsage !== null) {
|
|
1086
|
+
await this.client.sessionUpdate({
|
|
1087
|
+
sessionId: message.session_id,
|
|
1088
|
+
update: {
|
|
1089
|
+
sessionUpdate: "usage_update",
|
|
1090
|
+
used: lastAssistantTotalUsage,
|
|
1091
|
+
size: session.contextWindowSize,
|
|
1092
|
+
_meta: { "_claude/rateLimit": message.rate_limit_info },
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
921
1098
|
case "tool_use_summary":
|
|
922
1099
|
case "auth_status":
|
|
923
1100
|
case "prompt_suggestion":
|
|
924
|
-
case "rate_limit_event":
|
|
925
1101
|
break;
|
|
926
1102
|
default:
|
|
927
1103
|
unreachable(message);
|
|
@@ -973,6 +1149,16 @@ export class ClaudeAcpAgent {
|
|
|
973
1149
|
throw error;
|
|
974
1150
|
}
|
|
975
1151
|
finally {
|
|
1152
|
+
// The loop is returning — interrupt() succeeded or the prompt finished
|
|
1153
|
+
// — so disarm the force-cancel backstop and release the wake-up channel
|
|
1154
|
+
// (only if we still own it; a handoff installs the next prompt's).
|
|
1155
|
+
if (session.forceCancelTimer) {
|
|
1156
|
+
clearTimeout(session.forceCancelTimer);
|
|
1157
|
+
session.forceCancelTimer = undefined;
|
|
1158
|
+
}
|
|
1159
|
+
if (session.cancelController === cancelController) {
|
|
1160
|
+
session.cancelController = undefined;
|
|
1161
|
+
}
|
|
976
1162
|
if (!handedOff) {
|
|
977
1163
|
session.promptRunning = false;
|
|
978
1164
|
if (errored) {
|
|
@@ -1008,6 +1194,27 @@ export class ClaudeAcpAgent {
|
|
|
1008
1194
|
pending.resolve(true);
|
|
1009
1195
|
}
|
|
1010
1196
|
session.pendingMessages.clear();
|
|
1197
|
+
// Arm a backstop before interrupting: if a prompt is actively consuming
|
|
1198
|
+
// the query and interrupt() doesn't make the SDK yield (e.g. a wedged
|
|
1199
|
+
// TaskOutput block — issue #680), force the loop to return "cancelled"
|
|
1200
|
+
// after the floor elapses so the pending session/prompt still resolves per
|
|
1201
|
+
// the ACP cancellation contract instead of hanging forever. The loop's
|
|
1202
|
+
// `finally` clears this timer when interrupt() works and it returns through
|
|
1203
|
+
// the normal idle path, so on healthy cancels it is armed but never fires.
|
|
1204
|
+
//
|
|
1205
|
+
// Arm at most once per turn: the floor is an absolute ceiling from the
|
|
1206
|
+
// first cancel, so a client that re-sends cancel (each call still retries
|
|
1207
|
+
// interrupt() below) can't keep pushing the deadline out.
|
|
1208
|
+
if (session.promptRunning &&
|
|
1209
|
+
session.cancelController &&
|
|
1210
|
+
!session.cancelController.signal.aborted &&
|
|
1211
|
+
!session.forceCancelTimer) {
|
|
1212
|
+
const cancelController = session.cancelController;
|
|
1213
|
+
session.forceCancelTimer = setTimeout(() => {
|
|
1214
|
+
this.logger.error(`Session ${params.sessionId}: cancel floor elapsed without the SDK yielding; forcing "cancelled". The underlying query may still be wedged — a new session may be required.`);
|
|
1215
|
+
cancelController.abort();
|
|
1216
|
+
}, this.forceCancelGraceMs);
|
|
1217
|
+
}
|
|
1011
1218
|
await session.query.interrupt();
|
|
1012
1219
|
}
|
|
1013
1220
|
/** Cleanly tear down a session: cancel in-flight work, dispose resources,
|
|
@@ -1018,6 +1225,18 @@ export class ClaudeAcpAgent {
|
|
|
1018
1225
|
return;
|
|
1019
1226
|
}
|
|
1020
1227
|
await this.cancel({ sessionId });
|
|
1228
|
+
// cancel() arms the force-cancel floor and interrupts gracefully, but a
|
|
1229
|
+
// wedged prompt loop only wakes when `cancelController` aborts — closing
|
|
1230
|
+
// the query/abortController below doesn't touch it. Since we're tearing the
|
|
1231
|
+
// session down anyway, wake the loop now so the in-flight prompt() resolves
|
|
1232
|
+
// immediately instead of after the floor, and clear the timer so it can't
|
|
1233
|
+
// outlive the deleted session (it isn't unref'd and would otherwise keep
|
|
1234
|
+
// the event loop alive until it fires).
|
|
1235
|
+
if (session.forceCancelTimer) {
|
|
1236
|
+
clearTimeout(session.forceCancelTimer);
|
|
1237
|
+
session.forceCancelTimer = undefined;
|
|
1238
|
+
}
|
|
1239
|
+
session.cancelController?.abort();
|
|
1021
1240
|
session.settingsManager.dispose();
|
|
1022
1241
|
session.abortController.abort();
|
|
1023
1242
|
session.query.close();
|
|
@@ -1145,6 +1364,15 @@ export class ClaudeAcpAgent {
|
|
|
1145
1364
|
const toolUseCache = {};
|
|
1146
1365
|
const messages = await getSessionMessages(sessionId);
|
|
1147
1366
|
for (const message of messages) {
|
|
1367
|
+
// Backfill the ACP messageId -> SDK uuid mapping for messages we didn't
|
|
1368
|
+
// observe live (resumed/loaded sessions), so rewind/resume can translate
|
|
1369
|
+
// a client-supplied id without an extra getSessionMessages read. Not read
|
|
1370
|
+
// yet (see Session.messageIdToUuid).
|
|
1371
|
+
const replayMessageId = messageIdForGrouping(message);
|
|
1372
|
+
const replaySession = this.sessions[sessionId];
|
|
1373
|
+
if (replaySession && replayMessageId && message.uuid) {
|
|
1374
|
+
replaySession.messageIdToUuid.set(replayMessageId, message.uuid);
|
|
1375
|
+
}
|
|
1148
1376
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
1149
1377
|
let content = message.message.content;
|
|
1150
1378
|
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
@@ -1162,6 +1390,7 @@ export class ClaudeAcpAgent {
|
|
|
1162
1390
|
clientCapabilities: this.clientCapabilities,
|
|
1163
1391
|
cwd: this.sessions[sessionId]?.cwd,
|
|
1164
1392
|
taskState: this.sessions[sessionId]?.taskState,
|
|
1393
|
+
messageId: replayMessageId,
|
|
1165
1394
|
})) {
|
|
1166
1395
|
await this.client.sessionUpdate(notification);
|
|
1167
1396
|
}
|
|
@@ -1513,10 +1742,8 @@ export class ClaudeAcpAgent {
|
|
|
1513
1742
|
// Extract options from _meta if provided
|
|
1514
1743
|
const sessionMeta = params._meta;
|
|
1515
1744
|
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
1516
|
-
// Configure thinking
|
|
1517
|
-
const
|
|
1518
|
-
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
1519
|
-
: undefined;
|
|
1745
|
+
// Configure thinking behavior from environment variable
|
|
1746
|
+
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
1520
1747
|
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
1521
1748
|
const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
|
|
1522
1749
|
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
@@ -1535,7 +1762,7 @@ export class ClaudeAcpAgent {
|
|
|
1535
1762
|
const options = {
|
|
1536
1763
|
systemPrompt,
|
|
1537
1764
|
settingSources: ["user", "project", "local"],
|
|
1538
|
-
...(
|
|
1765
|
+
...(thinking !== undefined && { thinking }),
|
|
1539
1766
|
...userProvidedOptions,
|
|
1540
1767
|
// CLAUDE_MODEL_CONFIG env var is a fallback for model
|
|
1541
1768
|
// configuration (e.g. Bedrock model ID overrides). When the caller
|
|
@@ -1752,6 +1979,8 @@ export class ClaudeAcpAgent {
|
|
|
1752
1979
|
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
|
1753
1980
|
contextWindowSize: inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW,
|
|
1754
1981
|
taskState,
|
|
1982
|
+
toolUseCache: {},
|
|
1983
|
+
messageIdToUuid: new Map(),
|
|
1755
1984
|
};
|
|
1756
1985
|
return {
|
|
1757
1986
|
sessionId,
|
|
@@ -1993,11 +2222,16 @@ function tokenizeModelPreference(model) {
|
|
|
1993
2222
|
function scoreModelMatch(model, tokens, contextHint) {
|
|
1994
2223
|
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
|
|
1995
2224
|
let score = 0;
|
|
2225
|
+
let nonHintMatched = false;
|
|
1996
2226
|
for (const token of tokens) {
|
|
1997
2227
|
if (haystack.includes(token)) {
|
|
2228
|
+
if (token !== contextHint)
|
|
2229
|
+
nonHintMatched = true;
|
|
1998
2230
|
score += token === contextHint ? 3 : 1;
|
|
1999
2231
|
}
|
|
2000
2232
|
}
|
|
2233
|
+
if (contextHint && !nonHintMatched)
|
|
2234
|
+
return 0;
|
|
2001
2235
|
return score;
|
|
2002
2236
|
}
|
|
2003
2237
|
function resolveModelPreference(models, preference) {
|
|
@@ -2265,6 +2499,42 @@ export function promptToClaude(prompt) {
|
|
|
2265
2499
|
parent_tool_use_id: null,
|
|
2266
2500
|
};
|
|
2267
2501
|
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Resolves the ACP `messageId` for a Claude SDK message (live) or a persisted
|
|
2504
|
+
* transcript message (replay) so chunk grouping is identical in both views.
|
|
2505
|
+
*
|
|
2506
|
+
* Assistant turns are keyed by the Anthropic API message id (`message.id`),
|
|
2507
|
+
* which is identical at `message_start`, on the consolidated assistant message,
|
|
2508
|
+
* and in the persisted transcript — unlike the per-`stream_event` uuid, which is
|
|
2509
|
+
* unique per event and never persisted. User messages have no API id, but they
|
|
2510
|
+
* are never streamed, so their (stable) SDK uuid is used instead. ACP message
|
|
2511
|
+
* ids are opaque strings, so no particular format is required.
|
|
2512
|
+
*/
|
|
2513
|
+
export function messageIdForGrouping(message) {
|
|
2514
|
+
if (message.type === "assistant") {
|
|
2515
|
+
const inner = message.message;
|
|
2516
|
+
const apiId = inner && typeof inner === "object" && "id" in inner
|
|
2517
|
+
? inner.id
|
|
2518
|
+
: undefined;
|
|
2519
|
+
if (typeof apiId === "string" && apiId.length > 0) {
|
|
2520
|
+
return apiId;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return typeof message.uuid === "string" && message.uuid.length > 0 ? message.uuid : undefined;
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Stamps an ACP `messageId` onto a session update, but only on the message/
|
|
2527
|
+
* thought chunk variants that carry one — tool_call/plan/etc. updates never do.
|
|
2528
|
+
* No-op when `messageId` is falsy, so callers can pass it through unconditionally.
|
|
2529
|
+
*/
|
|
2530
|
+
function applyMessageId(update, messageId) {
|
|
2531
|
+
if (messageId &&
|
|
2532
|
+
(update.sessionUpdate === "agent_message_chunk" ||
|
|
2533
|
+
update.sessionUpdate === "user_message_chunk" ||
|
|
2534
|
+
update.sessionUpdate === "agent_thought_chunk")) {
|
|
2535
|
+
update.messageId = messageId;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2268
2538
|
/**
|
|
2269
2539
|
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
2270
2540
|
* Only handles text, image, and thinking chunks for now.
|
|
@@ -2281,6 +2551,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
2281
2551
|
text: content,
|
|
2282
2552
|
},
|
|
2283
2553
|
};
|
|
2554
|
+
applyMessageId(update, options?.messageId);
|
|
2284
2555
|
if (options?.parentToolUseId) {
|
|
2285
2556
|
update._meta = {
|
|
2286
2557
|
...update._meta,
|
|
@@ -2353,40 +2624,40 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
2353
2624
|
else {
|
|
2354
2625
|
// Only register hooks on first encounter to avoid double-firing
|
|
2355
2626
|
if (registerHooks && !alreadyCached) {
|
|
2627
|
+
// Capture the tool name in the closure rather than re-reading the
|
|
2628
|
+
// cache when the hook fires. The cache entry is pruned at
|
|
2629
|
+
// tool_result time, and a PostToolUse hook can fire after that, so
|
|
2630
|
+
// closing over the name keeps the diff working without depending on
|
|
2631
|
+
// (or pinning) the cache entry's lifetime.
|
|
2632
|
+
const toolName = chunk.name;
|
|
2356
2633
|
registerHookCallback(chunk.id, {
|
|
2357
2634
|
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
toolResponse,
|
|
2375
|
-
toolName: toolUse.name,
|
|
2376
|
-
},
|
|
2635
|
+
// Both `Edit` and `Write` produce a structuredPatch in their
|
|
2636
|
+
// PostToolUse tool_response. For Edit the diff replaces the
|
|
2637
|
+
// optimistic content built at tool_use time. For Write the
|
|
2638
|
+
// optimistic content (built from `input.content` alone with
|
|
2639
|
+
// `oldText: null`) shows "creation" semantics regardless of
|
|
2640
|
+
// whether the file existed; the structuredPatch from the
|
|
2641
|
+
// hook lets us emit the real diff for `type: "update"`. The
|
|
2642
|
+
// helper returns `{}` if the response shape isn't usable.
|
|
2643
|
+
const editDiff = toolName === "Edit" || toolName === "Write"
|
|
2644
|
+
? toolUpdateFromDiffToolResponse(toolResponse)
|
|
2645
|
+
: {};
|
|
2646
|
+
const update = {
|
|
2647
|
+
_meta: {
|
|
2648
|
+
claudeCode: {
|
|
2649
|
+
toolResponse,
|
|
2650
|
+
toolName,
|
|
2377
2651
|
},
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
}
|
|
2387
|
-
else {
|
|
2388
|
-
logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
|
|
2389
|
-
}
|
|
2652
|
+
},
|
|
2653
|
+
toolCallId: toolUseId,
|
|
2654
|
+
sessionUpdate: "tool_call_update",
|
|
2655
|
+
...editDiff,
|
|
2656
|
+
};
|
|
2657
|
+
await client.sessionUpdate({
|
|
2658
|
+
sessionId,
|
|
2659
|
+
update,
|
|
2660
|
+
});
|
|
2390
2661
|
},
|
|
2391
2662
|
});
|
|
2392
2663
|
}
|
|
@@ -2509,6 +2780,11 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
2509
2780
|
...toolUpdate,
|
|
2510
2781
|
};
|
|
2511
2782
|
}
|
|
2783
|
+
// The tool_use is fully resolved now — drop it so a long session doesn't
|
|
2784
|
+
// retain every tool call. The PostToolUse hook (Edit/Write diffs) closes
|
|
2785
|
+
// over the tool name and no longer reads the cache, so pruning here is
|
|
2786
|
+
// safe regardless of hook/result ordering.
|
|
2787
|
+
delete toolUseCache[chunk.tool_use_id];
|
|
2512
2788
|
break;
|
|
2513
2789
|
}
|
|
2514
2790
|
case "document":
|
|
@@ -2537,6 +2813,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
2537
2813
|
},
|
|
2538
2814
|
};
|
|
2539
2815
|
}
|
|
2816
|
+
applyMessageId(update, options?.messageId);
|
|
2540
2817
|
output.push({ sessionId, update });
|
|
2541
2818
|
}
|
|
2542
2819
|
}
|
|
@@ -2551,6 +2828,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
2551
2828
|
parentToolUseId: message.parent_tool_use_id,
|
|
2552
2829
|
cwd: options?.cwd,
|
|
2553
2830
|
taskState: options?.taskState,
|
|
2831
|
+
messageId: options?.messageId,
|
|
2554
2832
|
});
|
|
2555
2833
|
case "content_block_delta":
|
|
2556
2834
|
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
|
|
@@ -2558,6 +2836,7 @@ export function streamEventToAcpNotifications(message, sessionId, toolUseCache,
|
|
|
2558
2836
|
parentToolUseId: message.parent_tool_use_id,
|
|
2559
2837
|
cwd: options?.cwd,
|
|
2560
2838
|
taskState: options?.taskState,
|
|
2839
|
+
messageId: options?.messageId,
|
|
2561
2840
|
});
|
|
2562
2841
|
// No content. `ping` is a Messages-API keep-alive event that the SDK's
|
|
2563
2842
|
// `BetaRawMessageStreamEvent` union doesn't include even though the
|
|
@@ -2593,7 +2872,8 @@ function commonPrefixLength(a, b) {
|
|
|
2593
2872
|
return i;
|
|
2594
2873
|
}
|
|
2595
2874
|
/** Best-effort first guess of a model's context window from its ID, used only
|
|
2596
|
-
*
|
|
2875
|
+
* as a fallback when the SDK's authoritative `getContextUsage` is unavailable
|
|
2876
|
+
* (and until a `result` message arrives with the `modelUsage` value).
|
|
2597
2877
|
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
|
|
2598
2878
|
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
|
|
2599
2879
|
* matching things like "10m" or embedded substrings. */
|
|
@@ -2602,6 +2882,42 @@ function inferContextWindowFromModel(model) {
|
|
|
2602
2882
|
return 1_000_000;
|
|
2603
2883
|
return null;
|
|
2604
2884
|
}
|
|
2885
|
+
/** Fetch the SDK's authoritative context-window occupancy via the
|
|
2886
|
+
* `getContextUsage` control request. Unlike the per-message API usage numbers
|
|
2887
|
+
* (which only count message tokens), this `totalTokens` includes the system
|
|
2888
|
+
* prompt, tool schemas, MCP tools, and memory-file overhead — the real
|
|
2889
|
+
* occupancy the user sees. Returns `null` on any control-request failure.
|
|
2890
|
+
*
|
|
2891
|
+
* Note: we deliberately do NOT use this response's window fields for `size`.
|
|
2892
|
+
* They have been observed to under-report extended (1M) context windows, so
|
|
2893
|
+
* the window keeps coming from `modelUsage` / `inferContextWindowFromModel`,
|
|
2894
|
+
* which handle the 1M variants correctly. */
|
|
2895
|
+
async function fetchContextUsedTokens(query, logger) {
|
|
2896
|
+
try {
|
|
2897
|
+
const usage = await query.getContextUsage();
|
|
2898
|
+
return usage.totalTokens;
|
|
2899
|
+
}
|
|
2900
|
+
catch (error) {
|
|
2901
|
+
logger.error("Failed to fetch context usage from SDK:", error);
|
|
2902
|
+
return null;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
/** Translate the legacy `MAX_THINKING_TOKENS` env var into the SDK's `thinking`
|
|
2906
|
+
* option. The `maxThinkingTokens` option it used to feed is deprecated and
|
|
2907
|
+
* reduced to on/off on current models, so map the value to explicit thinking
|
|
2908
|
+
* config instead: unset → `undefined` (SDK default, adaptive on models that
|
|
2909
|
+
* support it); `0` → disabled; a positive integer → a fixed token budget.
|
|
2910
|
+
* Anything else is ignored with a warning. */
|
|
2911
|
+
function resolveThinkingConfig(raw, logger) {
|
|
2912
|
+
if (raw === undefined)
|
|
2913
|
+
return undefined;
|
|
2914
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2915
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
2916
|
+
logger.error(`Ignoring MAX_THINKING_TOKENS: expected a non-negative integer, got '${raw}'.`);
|
|
2917
|
+
return undefined;
|
|
2918
|
+
}
|
|
2919
|
+
return parsed === 0 ? { type: "disabled" } : { type: "enabled", budgetTokens: parsed };
|
|
2920
|
+
}
|
|
2605
2921
|
function parseModelConfig(raw) {
|
|
2606
2922
|
if (!raw)
|
|
2607
2923
|
return undefined;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.42.0",
|
|
7
7
|
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
|
8
8
|
"main": "dist/lib.js",
|
|
9
9
|
"types": "dist/lib.d.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"license": "Apache-2.0",
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@agentclientprotocol/sdk": "0.24.0",
|
|
64
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
64
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.165",
|
|
65
65
|
"zod": "^3.25.0 || ^4.0.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|