@agentclientprotocol/claude-agent-acp 0.42.0 → 0.44.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 +20 -1
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +200 -8
- package/dist/elicitation.d.ts +80 -0
- package/dist/elicitation.d.ts.map +1 -0
- package/dist/elicitation.js +166 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +16 -0
- package/package.json +8 -8
package/dist/acp-agent.d.ts
CHANGED
|
@@ -227,7 +227,7 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
227
227
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
228
228
|
dispose(): Promise<void>;
|
|
229
229
|
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse>;
|
|
230
|
-
|
|
230
|
+
deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse>;
|
|
231
231
|
setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse>;
|
|
232
232
|
setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
|
|
233
233
|
private applySessionMode;
|
|
@@ -235,10 +235,29 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
235
235
|
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
|
236
236
|
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
|
|
237
237
|
canUseTool(sessionId: string): CanUseTool;
|
|
238
|
+
/**
|
|
239
|
+
* Handle elicitation requests that originate from MCP servers by forwarding
|
|
240
|
+
* them to the client over ACP. Modes the client did not advertise (or
|
|
241
|
+
* requests we can't represent) are declined.
|
|
242
|
+
*/
|
|
243
|
+
private handleMcpElicitation;
|
|
244
|
+
/**
|
|
245
|
+
* Present the built-in AskUserQuestion tool's questions as an ACP form
|
|
246
|
+
* elicitation and return the answers as the tool's `updatedInput`. Called from
|
|
247
|
+
* `canUseTool` since that is where the SDK routes the tool's permission check.
|
|
248
|
+
*/
|
|
249
|
+
private handleAskUserQuestion;
|
|
238
250
|
private sendAvailableCommandsUpdate;
|
|
239
251
|
private updateConfigOption;
|
|
240
252
|
private applyConfigOptionValue;
|
|
241
253
|
private getOrCreateSession;
|
|
254
|
+
/**
|
|
255
|
+
* Ensures the requested `cwd` is an absolute path that points at an existing
|
|
256
|
+
* directory before we create a session. Throws an `invalidParams` error with
|
|
257
|
+
* an actionable message so clients (e.g. Zed) can surface it to the user
|
|
258
|
+
* instead of failing later with an opaque SDK error.
|
|
259
|
+
*/
|
|
260
|
+
private validateCwd;
|
|
242
261
|
private createSession;
|
|
243
262
|
}
|
|
244
263
|
export declare function promptToClaude(prompt: PromptRequest): SDKUserMessage;
|
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,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,UAAU,EAKV,SAAS,
|
|
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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
2
|
import { deleteSession, getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import packageJson from "../package.json" with { type: "json" };
|
|
8
|
+
import { applyAskElicitationResponse, askUserQuestionsToCreateRequest, createElicitationResponseToElicitResult, extractAskUserQuestions, mcpElicitationToCreateRequest, } from "./elicitation.js";
|
|
7
9
|
import { SettingsManager } from "./settings.js";
|
|
8
10
|
import { applyTaskCreate, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
9
11
|
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
@@ -499,6 +501,21 @@ export class ClaudeAcpAgent {
|
|
|
499
501
|
// `parent_tool_use_id === null` (subagent work is folded into tool-result
|
|
500
502
|
// messages, never surfaced as partial streams).
|
|
501
503
|
let currentStreamMessageId;
|
|
504
|
+
// Per-message-id record of which assistant content actually streamed live via
|
|
505
|
+
// `stream_event` deltas, split by block type. The `assistant` case below
|
|
506
|
+
// normally drops `text`/`thinking` blocks on the assumption they already
|
|
507
|
+
// reached the client as `agent_message_chunk`/`agent_thought_chunk`. That
|
|
508
|
+
// breaks behind Anthropic-protocol gateways that return a turn as a single
|
|
509
|
+
// non-streamed block (common with OpenAI-compatible proxies): no
|
|
510
|
+
// `content_block_delta` fires, so the assembled block is the only copy the
|
|
511
|
+
// client will ever see. We keep the filter per (id, block type) for content
|
|
512
|
+
// that did stream, and fall back to forwarding what did not. Split by type so
|
|
513
|
+
// a gateway that streams text but not thinking (or vice versa) doesn't lose
|
|
514
|
+
// the un-streamed block. Only top-level (`parent_tool_use_id === null`)
|
|
515
|
+
// streams are recorded — subagent text is never streamed and must stay
|
|
516
|
+
// filtered, as it is internal to the tool call.
|
|
517
|
+
const streamedTextIds = new Set();
|
|
518
|
+
const streamedThinkingIds = new Set();
|
|
502
519
|
const userMessage = promptToClaude(params);
|
|
503
520
|
const promptUuid = randomUUID();
|
|
504
521
|
userMessage.uuid = promptUuid;
|
|
@@ -755,11 +772,28 @@ export class ClaudeAcpAgent {
|
|
|
755
772
|
case "task_notification":
|
|
756
773
|
case "task_progress":
|
|
757
774
|
case "task_updated":
|
|
758
|
-
|
|
775
|
+
break;
|
|
776
|
+
case "elicitation_complete": {
|
|
777
|
+
// A url-mode MCP elicitation finished server-side. Let the client
|
|
778
|
+
// dismiss any UI it opened for it. Only meaningful when the
|
|
779
|
+
// client supports url elicitation; ignore failures otherwise.
|
|
780
|
+
if (this.clientCapabilities?.elicitation?.url) {
|
|
781
|
+
try {
|
|
782
|
+
await this.client.unstable_completeElicitation({
|
|
783
|
+
elicitationId: message.elicitation_id,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
this.logger.error(`Failed to complete elicitation: ${error}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
759
792
|
case "plugin_install":
|
|
760
793
|
case "notification":
|
|
761
794
|
case "api_retry":
|
|
762
795
|
case "thinking_tokens":
|
|
796
|
+
case "model_refusal_fallback":
|
|
763
797
|
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
764
798
|
break;
|
|
765
799
|
default:
|
|
@@ -894,6 +928,21 @@ export class ClaudeAcpAgent {
|
|
|
894
928
|
if (message.event.type === "message_start") {
|
|
895
929
|
currentStreamMessageId = message.event.message.id || undefined;
|
|
896
930
|
}
|
|
931
|
+
// Record that this top-level message id actually streamed text/
|
|
932
|
+
// thinking, so the `assistant` case below knows its assembled blocks
|
|
933
|
+
// are duplicates (filter them) rather than the only copy (forward
|
|
934
|
+
// them). Gated on `parent_tool_use_id === null` so a subagent stream
|
|
935
|
+
// can't attribute its content to the top-level message id.
|
|
936
|
+
if (currentStreamMessageId &&
|
|
937
|
+
message.parent_tool_use_id === null &&
|
|
938
|
+
message.event.type === "content_block_delta") {
|
|
939
|
+
if (message.event.delta.type === "text_delta") {
|
|
940
|
+
streamedTextIds.add(currentStreamMessageId);
|
|
941
|
+
}
|
|
942
|
+
else if (message.event.delta.type === "thinking_delta") {
|
|
943
|
+
streamedThinkingIds.add(currentStreamMessageId);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
897
946
|
if (message.parent_tool_use_id === null &&
|
|
898
947
|
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
899
948
|
if (message.event.type === "message_start") {
|
|
@@ -1049,10 +1098,46 @@ export class ClaudeAcpAgent {
|
|
|
1049
1098
|
message.message.content[0].text.includes("Please run /login")) {
|
|
1050
1099
|
throw RequestError.authRequired();
|
|
1051
1100
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1101
|
+
let content;
|
|
1102
|
+
if (message.type === "assistant" && message.parent_tool_use_id === null) {
|
|
1103
|
+
// Top-level assistant message: drop text/thinking blocks already
|
|
1104
|
+
// streamed live as chunks, and forward (as a fallback) any that were
|
|
1105
|
+
// not, so non-streaming gateways still deliver the final answer.
|
|
1106
|
+
const id = messageIdForGrouping(message);
|
|
1107
|
+
content = message.message.content.filter((item) => {
|
|
1108
|
+
// Non-text blocks (tool_use, etc.) always pass through; their own
|
|
1109
|
+
// dedupe (`toolUseCache`) collapses the streamed/assembled pair.
|
|
1110
|
+
if (item.type !== "text" && item.type !== "thinking") {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
// Already delivered live as a chunk of this exact type — drop the
|
|
1114
|
+
// duplicate. Checked per type so streaming one (e.g. text) doesn't
|
|
1115
|
+
// suppress an un-streamed block of the other (e.g. thinking).
|
|
1116
|
+
const streamedLive = id !== undefined &&
|
|
1117
|
+
(item.type === "text" ? streamedTextIds : streamedThinkingIds).has(id);
|
|
1118
|
+
if (streamedLive) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
// Empty assembled blocks carry nothing (some gateways emit an empty
|
|
1122
|
+
// `thinking` block before the real text) — don't forward stray
|
|
1123
|
+
// empty chunks.
|
|
1124
|
+
const text = item.type === "text" ? item.text : item.thinking;
|
|
1125
|
+
if (text.length === 0) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
return true;
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
else if (message.type === "assistant") {
|
|
1132
|
+
// Subagent assistant message (`parent_tool_use_id !== null`). It is
|
|
1133
|
+
// never streamed live and its text/thinking is internal to the tool
|
|
1134
|
+
// call — keep dropping it so subagent prose doesn't leak into the
|
|
1135
|
+
// top-level feed.
|
|
1136
|
+
content = message.message.content.filter((item) => item.type !== "text" && item.type !== "thinking");
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
content = message.message.content;
|
|
1140
|
+
}
|
|
1056
1141
|
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, session.toolUseCache, this.client, this.logger, {
|
|
1057
1142
|
clientCapabilities: this.clientCapabilities,
|
|
1058
1143
|
parentToolUseId: message.parent_tool_use_id,
|
|
@@ -1253,7 +1338,7 @@ export class ClaudeAcpAgent {
|
|
|
1253
1338
|
await this.teardownSession(params.sessionId);
|
|
1254
1339
|
return {};
|
|
1255
1340
|
}
|
|
1256
|
-
async
|
|
1341
|
+
async deleteSession(params) {
|
|
1257
1342
|
// Tear down any active in-memory state first so the on-disk file isn't
|
|
1258
1343
|
// recreated by an outstanding query writing to it.
|
|
1259
1344
|
if (this.sessions[params.sessionId]) {
|
|
@@ -1415,6 +1500,13 @@ export class ClaudeAcpAgent {
|
|
|
1415
1500
|
message: "Session not found",
|
|
1416
1501
|
};
|
|
1417
1502
|
}
|
|
1503
|
+
// AskUserQuestion is surfaced to us as a normal permission check (the SDK
|
|
1504
|
+
// routes it through canUseTool whenever a callback is registered, rather
|
|
1505
|
+
// than the interactive dialog). Present it as an ACP form elicitation and
|
|
1506
|
+
// feed the answers back as updatedInput for the tool's own call() to read.
|
|
1507
|
+
if (toolName === "AskUserQuestion" && this.clientCapabilities?.elicitation?.form) {
|
|
1508
|
+
return this.handleAskUserQuestion(sessionId, toolInput, toolUseID, signal);
|
|
1509
|
+
}
|
|
1418
1510
|
if (toolName === "ExitPlanMode") {
|
|
1419
1511
|
const optionsAll = [
|
|
1420
1512
|
{ kind: "allow_always", name: 'Yes, and use "auto" mode', optionId: "auto" },
|
|
@@ -1540,6 +1632,62 @@ export class ClaudeAcpAgent {
|
|
|
1540
1632
|
}
|
|
1541
1633
|
};
|
|
1542
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Handle elicitation requests that originate from MCP servers by forwarding
|
|
1637
|
+
* them to the client over ACP. Modes the client did not advertise (or
|
|
1638
|
+
* requests we can't represent) are declined.
|
|
1639
|
+
*/
|
|
1640
|
+
handleMcpElicitation(sessionId, support) {
|
|
1641
|
+
return async (request, { signal }) => {
|
|
1642
|
+
const isUrl = request.mode === "url";
|
|
1643
|
+
if ((isUrl && !support.url) || (!isUrl && !support.form)) {
|
|
1644
|
+
return { action: "decline" };
|
|
1645
|
+
}
|
|
1646
|
+
const createRequest = mcpElicitationToCreateRequest(request, sessionId);
|
|
1647
|
+
if (!createRequest) {
|
|
1648
|
+
return { action: "decline" };
|
|
1649
|
+
}
|
|
1650
|
+
try {
|
|
1651
|
+
const response = await this.client.unstable_createElicitation(createRequest);
|
|
1652
|
+
if (signal.aborted) {
|
|
1653
|
+
return { action: "cancel" };
|
|
1654
|
+
}
|
|
1655
|
+
return createElicitationResponseToElicitResult(response);
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
this.logger.error(`Failed to forward MCP elicitation: ${error}`);
|
|
1659
|
+
return { action: "decline" };
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Present the built-in AskUserQuestion tool's questions as an ACP form
|
|
1665
|
+
* elicitation and return the answers as the tool's `updatedInput`. Called from
|
|
1666
|
+
* `canUseTool` since that is where the SDK routes the tool's permission check.
|
|
1667
|
+
*/
|
|
1668
|
+
async handleAskUserQuestion(sessionId, toolInput, toolUseID, signal) {
|
|
1669
|
+
const questions = extractAskUserQuestions(toolInput);
|
|
1670
|
+
if (!questions) {
|
|
1671
|
+
return { behavior: "deny", message: "AskUserQuestion called with no valid questions." };
|
|
1672
|
+
}
|
|
1673
|
+
const createRequest = askUserQuestionsToCreateRequest(questions, sessionId, toolUseID);
|
|
1674
|
+
let response;
|
|
1675
|
+
try {
|
|
1676
|
+
response = await this.client.unstable_createElicitation(createRequest);
|
|
1677
|
+
}
|
|
1678
|
+
catch (error) {
|
|
1679
|
+
this.logger.error(`Failed to present AskUserQuestion elicitation: ${error}`);
|
|
1680
|
+
return { behavior: "deny", message: "Could not present the question to the user." };
|
|
1681
|
+
}
|
|
1682
|
+
if (signal.aborted) {
|
|
1683
|
+
throw new Error("Tool use aborted");
|
|
1684
|
+
}
|
|
1685
|
+
const outcome = applyAskElicitationResponse(response, toolInput, questions);
|
|
1686
|
+
if (outcome.action === "cancel") {
|
|
1687
|
+
throw new Error("Tool use aborted");
|
|
1688
|
+
}
|
|
1689
|
+
return { behavior: "allow", updatedInput: outcome.updatedInput };
|
|
1690
|
+
}
|
|
1543
1691
|
async sendAvailableCommandsUpdate(sessionId) {
|
|
1544
1692
|
const session = this.sessions[sessionId];
|
|
1545
1693
|
if (!session)
|
|
@@ -1676,7 +1824,34 @@ export class ClaudeAcpAgent {
|
|
|
1676
1824
|
configOptions: response.configOptions,
|
|
1677
1825
|
};
|
|
1678
1826
|
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Ensures the requested `cwd` is an absolute path that points at an existing
|
|
1829
|
+
* directory before we create a session. Throws an `invalidParams` error with
|
|
1830
|
+
* an actionable message so clients (e.g. Zed) can surface it to the user
|
|
1831
|
+
* instead of failing later with an opaque SDK error.
|
|
1832
|
+
*/
|
|
1833
|
+
async validateCwd(cwd) {
|
|
1834
|
+
if (!path.isAbsolute(cwd)) {
|
|
1835
|
+
throw RequestError.invalidParams({ cwd }, `\`cwd\` must be an absolute path, but received: ${cwd}`);
|
|
1836
|
+
}
|
|
1837
|
+
let stats;
|
|
1838
|
+
try {
|
|
1839
|
+
stats = await fs.stat(cwd);
|
|
1840
|
+
}
|
|
1841
|
+
catch {
|
|
1842
|
+
throw RequestError.invalidParams({ cwd }, `\`cwd\` does not exist on the machine running the agent: ${cwd}`);
|
|
1843
|
+
}
|
|
1844
|
+
if (!stats.isDirectory()) {
|
|
1845
|
+
throw RequestError.invalidParams({ cwd }, `\`cwd\` is not a directory: ${cwd}`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1679
1848
|
async createSession(params, creationOpts = {}) {
|
|
1849
|
+
// Validate `cwd` up front. The ACP spec requires an absolute path, and the
|
|
1850
|
+
// directory must actually exist on the machine running the agent. Without
|
|
1851
|
+
// this check a session is created against a missing directory and the
|
|
1852
|
+
// failure only surfaces later as a confusing "native binary failed to
|
|
1853
|
+
// launch" error from the SDK (see issue #749).
|
|
1854
|
+
await this.validateCwd(params.cwd);
|
|
1680
1855
|
// We want to create a new session id unless it is resume,
|
|
1681
1856
|
// but not resume + forkSession.
|
|
1682
1857
|
let sessionId;
|
|
@@ -1746,8 +1921,17 @@ export class ClaudeAcpAgent {
|
|
|
1746
1921
|
const thinking = resolveThinkingConfig(process.env.MAX_THINKING_TOKENS, this.logger);
|
|
1747
1922
|
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
1748
1923
|
const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
|
|
1749
|
-
//
|
|
1750
|
-
|
|
1924
|
+
// Elicitation modes the connected client advertised. We only forward
|
|
1925
|
+
// elicitations (and only re-enable AskUserQuestion) for modes the client
|
|
1926
|
+
// can actually render.
|
|
1927
|
+
const elicitationSupport = {
|
|
1928
|
+
form: !!this.clientCapabilities?.elicitation?.form,
|
|
1929
|
+
url: !!this.clientCapabilities?.elicitation?.url,
|
|
1930
|
+
};
|
|
1931
|
+
// AskUserQuestion surfaces as a `permission_ask_user_question` dialog that
|
|
1932
|
+
// we render as a form elicitation. Without form-elicitation support there
|
|
1933
|
+
// is no way to present it over ACP, so keep it disabled in that case.
|
|
1934
|
+
const disallowedTools = elicitationSupport.form ? [] : ["AskUserQuestion"];
|
|
1751
1935
|
// Resolve which built-in tools to expose.
|
|
1752
1936
|
// Explicit tools array from _meta.claudeCode.options takes precedence.
|
|
1753
1937
|
// disableBuiltInTools is a legacy shorthand for tools: [] — kept for
|
|
@@ -1791,6 +1975,13 @@ export class ClaudeAcpAgent {
|
|
|
1791
1975
|
allowDangerouslySkipPermissions: ALLOW_BYPASS,
|
|
1792
1976
|
permissionMode,
|
|
1793
1977
|
canUseTool: this.canUseTool(sessionId),
|
|
1978
|
+
// Forward MCP elicitation requests onto ACP elicitation. Only attached
|
|
1979
|
+
// when the client advertised support, so non-supporting clients keep the
|
|
1980
|
+
// SDK's default (auto-decline) behavior. (AskUserQuestion is handled in
|
|
1981
|
+
// canUseTool, not here.)
|
|
1982
|
+
...(elicitationSupport.form || elicitationSupport.url
|
|
1983
|
+
? { onElicitation: this.handleMcpElicitation(sessionId, elicitationSupport) }
|
|
1984
|
+
: {}),
|
|
1794
1985
|
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
|
|
1795
1986
|
extraArgs: {
|
|
1796
1987
|
...userProvidedOptions?.extraArgs,
|
|
@@ -2798,6 +2989,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
2798
2989
|
case "compaction_delta":
|
|
2799
2990
|
case "advisor_tool_result":
|
|
2800
2991
|
case "mid_conv_system":
|
|
2992
|
+
case "fallback":
|
|
2801
2993
|
break;
|
|
2802
2994
|
default:
|
|
2803
2995
|
unreachable(chunk, logger);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CreateElicitationRequest, CreateElicitationResponse } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { ElicitationRequest, ElicitationResult } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import type { AskUserQuestionInput } from "@anthropic-ai/claude-agent-sdk/sdk-tools.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bridges between the Claude Agent SDK's elicitation/dialog callbacks and ACP's
|
|
6
|
+
* (unstable) elicitation protocol.
|
|
7
|
+
*
|
|
8
|
+
* Two distinct SDK surfaces flow through here:
|
|
9
|
+
*
|
|
10
|
+
* 1. `onElicitation` — fired when an MCP server requests user input. These map
|
|
11
|
+
* directly onto ACP `session/create_elicitation` (form or url mode).
|
|
12
|
+
* 2. The built-in AskUserQuestion tool — when a `canUseTool` callback is
|
|
13
|
+
* registered the SDK routes its permission check through `canUseTool`
|
|
14
|
+
* (not the interactive `permission_ask_user_question` dialog). We render
|
|
15
|
+
* its questions as an ACP form elicitation and feed the user's selections
|
|
16
|
+
* back as the tool's `updatedInput`, which the tool's own `call()` reads.
|
|
17
|
+
*/
|
|
18
|
+
/** Modes the connected client advertised support for. */
|
|
19
|
+
export type ElicitationSupport = {
|
|
20
|
+
form: boolean;
|
|
21
|
+
url: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Convert an MCP elicitation request (from the SDK's `onElicitation` callback)
|
|
25
|
+
* into an ACP `CreateElicitationRequest`. Returns `null` when the request can't
|
|
26
|
+
* be represented (e.g. a url-mode request with no url).
|
|
27
|
+
*/
|
|
28
|
+
export declare function mcpElicitationToCreateRequest(request: ElicitationRequest, sessionId: string): CreateElicitationRequest | null;
|
|
29
|
+
/**
|
|
30
|
+
* Map an ACP elicitation response back to the MCP `ElicitResult` the SDK expects
|
|
31
|
+
* to hand back to the requesting server.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createElicitationResponseToElicitResult(response: CreateElicitationResponse): ElicitationResult;
|
|
34
|
+
/**
|
|
35
|
+
* A single question as supplied by the AskUserQuestion tool. Derived from the
|
|
36
|
+
* SDK's input type so the shape stays in sync; the SDK validates the model's
|
|
37
|
+
* tool call against this schema before it reaches us.
|
|
38
|
+
*/
|
|
39
|
+
export type AskUserQuestion = AskUserQuestionInput["questions"][number];
|
|
40
|
+
/**
|
|
41
|
+
* Pull the well-formed questions out of an AskUserQuestion tool input. Returns
|
|
42
|
+
* `null` when there are no usable questions — including the case where every
|
|
43
|
+
* entry is malformed and filtering leaves an empty list — so callers can treat
|
|
44
|
+
* "nothing to ask" uniformly.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractAskUserQuestions(input: Record<string, unknown>): AskUserQuestion[] | null;
|
|
47
|
+
/**
|
|
48
|
+
* Render the AskUserQuestion tool's questions as an ACP form elicitation.
|
|
49
|
+
*
|
|
50
|
+
* Fields are keyed by a short stable id (`question_<n>`) rather than the full
|
|
51
|
+
* question text, so the question text appears in exactly one place per field.
|
|
52
|
+
* Single-select questions use a titled `oneOf` enum; multi-select questions use
|
|
53
|
+
* an array with a titled `anyOf` item enum. The enum `const` is always the
|
|
54
|
+
* option label, since that is what the tool records as the answer.
|
|
55
|
+
*
|
|
56
|
+
* A trailing optional free-text field mirrors the CLI's custom-answer box: the
|
|
57
|
+
* user can type their own answer instead of (or as well as) picking an option.
|
|
58
|
+
* Nothing is marked required, so the user can also just skip — matching the
|
|
59
|
+
* built-in tool, which always offers Skip + a free-text box.
|
|
60
|
+
*/
|
|
61
|
+
export declare function askUserQuestionsToCreateRequest(questions: AskUserQuestion[], sessionId: string, toolCallId: string | undefined): CreateElicitationRequest;
|
|
62
|
+
/** Outcome of an AskUserQuestion elicitation, decoupled from any transport. */
|
|
63
|
+
export type AskUserQuestionOutcome = {
|
|
64
|
+
action: "answered";
|
|
65
|
+
updatedInput: Record<string, unknown>;
|
|
66
|
+
} | {
|
|
67
|
+
action: "cancel";
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Fold an ACP elicitation response into the AskUserQuestion tool's input.
|
|
71
|
+
*
|
|
72
|
+
* Selected labels are read back from the indexed form fields and written into
|
|
73
|
+
* `answers` as a `{ [questionText]: label }` map (comma-joining multi-selects)
|
|
74
|
+
* — the key shape the tool's own `call()` reads. Free text from the custom-
|
|
75
|
+
* answer field becomes the tool's top-level `response`. Decline yields empty
|
|
76
|
+
* answers (the model is told the user skipped rather than the turn aborting);
|
|
77
|
+
* cancel aborts the tool call.
|
|
78
|
+
*/
|
|
79
|
+
export declare function applyAskElicitationResponse(response: CreateElicitationResponse, toolInput: Record<string, unknown>, questions: AskUserQuestion[]): AskUserQuestionOutcome;
|
|
80
|
+
//# sourceMappingURL=elicitation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elicitation.d.ts","sourceRoot":"","sources":["../src/elicitation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,wBAAwB,EACxB,yBAAyB,EAI1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,6CAA6C,CAAC;AAErD;;;;;;;;;;;;;GAaG;AAEH,yDAAyD;AACzD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,kBAAkB,EAC3B,SAAS,EAAE,MAAM,GAChB,wBAAwB,GAAG,IAAI,CAyBjC;AAED;;;GAGG;AACH,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,yBAAyB,GAClC,iBAAiB,CAUnB;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,EAAE,GAAG,IAAI,CAUhG;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,eAAe,EAAE,EAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,wBAAwB,CAyC1B;AAED,+EAA+E;AAC/E,MAAM,MAAM,sBAAsB,GAC9B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC7D;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,SAAS,EAAE,eAAe,EAAE,GAC3B,sBAAsB,CAiCxB"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Convert an MCP elicitation request (from the SDK's `onElicitation` callback)
|
|
4
|
+
* into an ACP `CreateElicitationRequest`. Returns `null` when the request can't
|
|
5
|
+
* be represented (e.g. a url-mode request with no url).
|
|
6
|
+
*/
|
|
7
|
+
export function mcpElicitationToCreateRequest(request, sessionId) {
|
|
8
|
+
if (request.mode === "url") {
|
|
9
|
+
if (!request.url) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
mode: "url",
|
|
14
|
+
sessionId,
|
|
15
|
+
message: request.message,
|
|
16
|
+
url: request.url,
|
|
17
|
+
// URL elicitations need a stable id so the client can correlate the
|
|
18
|
+
// later `session/complete_elicitation` notification. MCP servers usually
|
|
19
|
+
// provide one; fall back to a generated id if not.
|
|
20
|
+
elicitationId: request.elicitationId ?? randomUUID(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Form mode (the default). The MCP `requestedSchema` is already a JSON Schema
|
|
24
|
+
// with primitive-typed properties, which is structurally what ACP expects.
|
|
25
|
+
return {
|
|
26
|
+
mode: "form",
|
|
27
|
+
sessionId,
|
|
28
|
+
message: request.message,
|
|
29
|
+
requestedSchema: normalizeElicitationSchema(request.requestedSchema),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Map an ACP elicitation response back to the MCP `ElicitResult` the SDK expects
|
|
34
|
+
* to hand back to the requesting server.
|
|
35
|
+
*/
|
|
36
|
+
export function createElicitationResponseToElicitResult(response) {
|
|
37
|
+
switch (response.action) {
|
|
38
|
+
case "accept":
|
|
39
|
+
return { action: "accept", content: response.content ?? {} };
|
|
40
|
+
case "decline":
|
|
41
|
+
return { action: "decline" };
|
|
42
|
+
case "cancel":
|
|
43
|
+
default:
|
|
44
|
+
return { action: "cancel" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Pull the well-formed questions out of an AskUserQuestion tool input. Returns
|
|
49
|
+
* `null` when there are no usable questions — including the case where every
|
|
50
|
+
* entry is malformed and filtering leaves an empty list — so callers can treat
|
|
51
|
+
* "nothing to ask" uniformly.
|
|
52
|
+
*/
|
|
53
|
+
export function extractAskUserQuestions(input) {
|
|
54
|
+
const questions = input.questions;
|
|
55
|
+
if (!Array.isArray(questions)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const valid = questions.filter((q) => !!q && typeof q.question === "string" && Array.isArray(q.options) && q.options.length > 0);
|
|
59
|
+
return valid.length > 0 ? valid : null;
|
|
60
|
+
}
|
|
61
|
+
/** Stable form-field key for the question at the given index. */
|
|
62
|
+
function questionFieldKey(index) {
|
|
63
|
+
return `question_${index}`;
|
|
64
|
+
}
|
|
65
|
+
/** Form-field key for the optional free-text "custom answer" field. */
|
|
66
|
+
const CUSTOM_ANSWER_FIELD = "customAnswer";
|
|
67
|
+
/**
|
|
68
|
+
* Render the AskUserQuestion tool's questions as an ACP form elicitation.
|
|
69
|
+
*
|
|
70
|
+
* Fields are keyed by a short stable id (`question_<n>`) rather than the full
|
|
71
|
+
* question text, so the question text appears in exactly one place per field.
|
|
72
|
+
* Single-select questions use a titled `oneOf` enum; multi-select questions use
|
|
73
|
+
* an array with a titled `anyOf` item enum. The enum `const` is always the
|
|
74
|
+
* option label, since that is what the tool records as the answer.
|
|
75
|
+
*
|
|
76
|
+
* A trailing optional free-text field mirrors the CLI's custom-answer box: the
|
|
77
|
+
* user can type their own answer instead of (or as well as) picking an option.
|
|
78
|
+
* Nothing is marked required, so the user can also just skip — matching the
|
|
79
|
+
* built-in tool, which always offers Skip + a free-text box.
|
|
80
|
+
*/
|
|
81
|
+
export function askUserQuestionsToCreateRequest(questions, sessionId, toolCallId) {
|
|
82
|
+
const single = questions.length === 1;
|
|
83
|
+
const properties = {};
|
|
84
|
+
questions.forEach((question, index) => {
|
|
85
|
+
const options = question.options.map((option) => ({
|
|
86
|
+
const: option.label,
|
|
87
|
+
title: option.description ? `${option.label} — ${option.description}` : option.label,
|
|
88
|
+
}));
|
|
89
|
+
// For a single question the prompt is carried by `message`, so we don't
|
|
90
|
+
// repeat it in the field description. With multiple questions each field
|
|
91
|
+
// needs its own question text.
|
|
92
|
+
const description = single ? undefined : question.question;
|
|
93
|
+
const title = question.header || undefined;
|
|
94
|
+
properties[questionFieldKey(index)] = question.multiSelect
|
|
95
|
+
? { type: "array", title, description, items: { anyOf: options } }
|
|
96
|
+
: { type: "string", title, description, oneOf: options };
|
|
97
|
+
});
|
|
98
|
+
properties[CUSTOM_ANSWER_FIELD] = {
|
|
99
|
+
type: "string",
|
|
100
|
+
title: "Other",
|
|
101
|
+
description: "Type your own answer instead of choosing an option above (optional).",
|
|
102
|
+
};
|
|
103
|
+
const requestedSchema = {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties,
|
|
106
|
+
};
|
|
107
|
+
const message = single ? questions[0].question : "Please answer the following questions.";
|
|
108
|
+
return {
|
|
109
|
+
mode: "form",
|
|
110
|
+
sessionId,
|
|
111
|
+
...(toolCallId ? { toolCallId } : {}),
|
|
112
|
+
message,
|
|
113
|
+
requestedSchema,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fold an ACP elicitation response into the AskUserQuestion tool's input.
|
|
118
|
+
*
|
|
119
|
+
* Selected labels are read back from the indexed form fields and written into
|
|
120
|
+
* `answers` as a `{ [questionText]: label }` map (comma-joining multi-selects)
|
|
121
|
+
* — the key shape the tool's own `call()` reads. Free text from the custom-
|
|
122
|
+
* answer field becomes the tool's top-level `response`. Decline yields empty
|
|
123
|
+
* answers (the model is told the user skipped rather than the turn aborting);
|
|
124
|
+
* cancel aborts the tool call.
|
|
125
|
+
*/
|
|
126
|
+
export function applyAskElicitationResponse(response, toolInput, questions) {
|
|
127
|
+
if (response.action === "cancel") {
|
|
128
|
+
return { action: "cancel" };
|
|
129
|
+
}
|
|
130
|
+
if (response.action === "decline") {
|
|
131
|
+
return { action: "answered", updatedInput: { ...toolInput, answers: {} } };
|
|
132
|
+
}
|
|
133
|
+
const content = response.content ?? {};
|
|
134
|
+
// Typed against the tool's own output schema so the answer/response shapes
|
|
135
|
+
// stay in sync with what the built-in tool's call() expects to read back.
|
|
136
|
+
const answers = {};
|
|
137
|
+
questions.forEach((question, index) => {
|
|
138
|
+
const value = content[questionFieldKey(index)];
|
|
139
|
+
if (value === undefined || value === null) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const text = Array.isArray(value) ? value.join(", ") : String(value);
|
|
143
|
+
if (text === "") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
answers[question.question] = text;
|
|
147
|
+
});
|
|
148
|
+
const updatedInput = { ...toolInput, answers };
|
|
149
|
+
const custom = content[CUSTOM_ANSWER_FIELD];
|
|
150
|
+
if (typeof custom === "string" && custom.trim() !== "") {
|
|
151
|
+
const response = custom;
|
|
152
|
+
updatedInput.response = response;
|
|
153
|
+
}
|
|
154
|
+
return { action: "answered", updatedInput };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Coerce an arbitrary MCP `requestedSchema` into an ACP `ElicitationSchema`.
|
|
158
|
+
* The two are structurally compatible JSON Schemas; we just guarantee the
|
|
159
|
+
* `type: "object"` discriminator is present.
|
|
160
|
+
*/
|
|
161
|
+
function normalizeElicitationSchema(schema) {
|
|
162
|
+
if (!schema || typeof schema !== "object") {
|
|
163
|
+
return { type: "object", properties: {} };
|
|
164
|
+
}
|
|
165
|
+
return { ...schema, type: "object" };
|
|
166
|
+
}
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACT,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -274,6 +274,22 @@ export function toolInfoFromToolUse(toolUse, supportsTerminalOutput = false, cwd
|
|
|
274
274
|
: [],
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
|
+
case "AskUserQuestion": {
|
|
278
|
+
const input = toolUse.input;
|
|
279
|
+
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
280
|
+
return {
|
|
281
|
+
title: questions.length === 1 && questions[0]?.question
|
|
282
|
+
? questions[0].question
|
|
283
|
+
: "Asking for your input",
|
|
284
|
+
kind: "other",
|
|
285
|
+
content: questions
|
|
286
|
+
.filter((q) => typeof q?.question === "string")
|
|
287
|
+
.map((q) => ({
|
|
288
|
+
type: "content",
|
|
289
|
+
content: { type: "text", text: q.question },
|
|
290
|
+
})),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
277
293
|
case "Other": {
|
|
278
294
|
const input = toolUse.input;
|
|
279
295
|
let output;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.44.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",
|
|
@@ -60,20 +60,20 @@
|
|
|
60
60
|
"author": "Zed Industries",
|
|
61
61
|
"license": "Apache-2.0",
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@agentclientprotocol/sdk": "0.
|
|
64
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
63
|
+
"@agentclientprotocol/sdk": "0.25.0",
|
|
64
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.170",
|
|
65
65
|
"zod": "^3.25.0 || ^4.0.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@anthropic-ai/sdk": "0.
|
|
68
|
+
"@anthropic-ai/sdk": "0.103.0",
|
|
69
69
|
"@eslint/js": "10.0.1",
|
|
70
|
-
"@types/node": "25.9.
|
|
71
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
72
|
-
"@typescript-eslint/parser": "8.
|
|
70
|
+
"@types/node": "25.9.2",
|
|
71
|
+
"@typescript-eslint/eslint-plugin": "8.61.0",
|
|
72
|
+
"@typescript-eslint/parser": "8.61.0",
|
|
73
73
|
"eslint": "10.4.1",
|
|
74
74
|
"eslint-config-prettier": "10.1.8",
|
|
75
75
|
"globals": "17.6.0",
|
|
76
|
-
"prettier": "3.8.
|
|
76
|
+
"prettier": "3.8.4",
|
|
77
77
|
"ts-node": "10.9.2",
|
|
78
78
|
"typescript": "6.0.3",
|
|
79
79
|
"vitest": "4.1.8"
|