@demicodes/provider-claude-code 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/index.mjs +97 -11
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -27,6 +27,16 @@ See [docs/provider-global-credentials.md](../../docs/provider-global-credentials
|
|
|
27
27
|
|
|
28
28
|
See [docs/provider-quota.md](../../docs/provider-quota.md).
|
|
29
29
|
|
|
30
|
+
## Tool-call batches
|
|
31
|
+
|
|
32
|
+
Claude Code's SDK-MCP control channel can hold later `tools/call` callbacks until
|
|
33
|
+
the preceding callback receives a result, even when the model emitted several
|
|
34
|
+
`tool_use` blocks in one response. The provider preserves the model's original
|
|
35
|
+
batch and tool-use IDs for the host scheduler, then matches the completed
|
|
36
|
+
results to SDK-MCP callbacks as the CLI releases them.
|
|
37
|
+
|
|
38
|
+
See [docs/tool-call-concurrency.md](../../docs/tool-call-concurrency.md).
|
|
39
|
+
|
|
30
40
|
> Diagnostics: the transport writes a raw request/response wire log (including
|
|
31
41
|
> prompts) to `$TMPDIR/demi-claude-wire` by default. Disable with
|
|
32
42
|
> `DEMI_CLAUDE_WIRE_LOG=0`. See [SECURITY](../../SECURITY.md).
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
|
|
1
|
+
import { abortable, errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { applyModelPolicy, clampUsedPercent, createProviderQuota, defineProvider, numberHeader, severityFromUsedPercent, toolResultContentToText, unixSecondsToIso } from "@demicodes/provider";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -834,7 +834,7 @@ function mapClaudeStdoutMessage(message, options = {}) {
|
|
|
834
834
|
events,
|
|
835
835
|
terminal: false
|
|
836
836
|
};
|
|
837
|
-
if (message.type === "assistant" && isRecord(message.message)
|
|
837
|
+
if (message.type === "assistant" && isRecord(message.message)) events.push(...mapContentArray(message.message.content, options));
|
|
838
838
|
if (message.type === "stream_event" && isRecord(message.event)) events.push(...mapStreamEvent(message.event));
|
|
839
839
|
if (message.type === "control_request") {
|
|
840
840
|
const request = parseControlRequest(message);
|
|
@@ -892,11 +892,11 @@ function mapContentArray(content, options) {
|
|
|
892
892
|
const events = [];
|
|
893
893
|
for (const block of content) {
|
|
894
894
|
if (!isRecord(block)) continue;
|
|
895
|
-
if (block.type === "text") events.push({
|
|
895
|
+
if (block.type === "text" && !options.ignoreAssistantContent) events.push({
|
|
896
896
|
type: "text_delta",
|
|
897
897
|
text: String(block.text ?? "")
|
|
898
898
|
});
|
|
899
|
-
else if (block.type === "thinking") {
|
|
899
|
+
else if (block.type === "thinking" && !options.ignoreAssistantContent) {
|
|
900
900
|
events.push({ type: "thinking_start" });
|
|
901
901
|
events.push({
|
|
902
902
|
type: "thinking_delta",
|
|
@@ -906,7 +906,7 @@ function mapContentArray(content, options) {
|
|
|
906
906
|
type: "thinking_signature",
|
|
907
907
|
signature: block.signature
|
|
908
908
|
});
|
|
909
|
-
} else if (block.type === "redacted_thinking") events.push({
|
|
909
|
+
} else if (block.type === "redacted_thinking" && !options.ignoreAssistantContent) events.push({
|
|
910
910
|
type: "redacted_thinking",
|
|
911
911
|
data: String(block.data ?? "")
|
|
912
912
|
});
|
|
@@ -970,6 +970,7 @@ function parseControlRequest(message) {
|
|
|
970
970
|
outerRequestId: message.request_id,
|
|
971
971
|
serverName: request.server_name,
|
|
972
972
|
id,
|
|
973
|
+
toolUseId: sdkMcpToolUseId(inner.params),
|
|
973
974
|
method,
|
|
974
975
|
params: inner.params
|
|
975
976
|
};
|
|
@@ -984,6 +985,11 @@ function parseControlRequest(message) {
|
|
|
984
985
|
params: message.params
|
|
985
986
|
};
|
|
986
987
|
}
|
|
988
|
+
function sdkMcpToolUseId(params) {
|
|
989
|
+
if (!isRecord(params) || !isRecord(params._meta)) return void 0;
|
|
990
|
+
const value = params._meta["claudecode/toolUseId"];
|
|
991
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
992
|
+
}
|
|
987
993
|
/**
|
|
988
994
|
* Usage for the response event from a `result` message. The top-level
|
|
989
995
|
* `result.usage` sums every API call the CLI made within the turn (initial call
|
|
@@ -1434,21 +1440,50 @@ var ClaudeCodeProvider = class {
|
|
|
1434
1440
|
}
|
|
1435
1441
|
const raw = next.value;
|
|
1436
1442
|
this.observeQuotaFromMessage(raw);
|
|
1437
|
-
const mapped = mapClaudeStdoutMessage(raw, {
|
|
1438
|
-
ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant"),
|
|
1439
|
-
ignoreAssistantToolUse: active.sdkMcpEnabled
|
|
1440
|
-
});
|
|
1443
|
+
const mapped = mapClaudeStdoutMessage(raw, { ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant") });
|
|
1441
1444
|
if (isMessageType(raw, "stream_event")) active.hasStreamed = true;
|
|
1442
1445
|
if (mapped.controlRequest) {
|
|
1443
|
-
|
|
1446
|
+
const handled = await this.handleControlRequest(active, mapped.controlRequest, request);
|
|
1447
|
+
if (handled === "tool-call") {
|
|
1444
1448
|
const event = active.pendingControlRequest ? controlRequestToToolCall(active.pendingControlRequest) : null;
|
|
1445
1449
|
keepActiveForContinuation = true;
|
|
1446
1450
|
if (event) yield event;
|
|
1447
1451
|
return;
|
|
1448
1452
|
}
|
|
1453
|
+
if (handled === "sdk-tool-call") {
|
|
1454
|
+
const pending = active.pendingSdkControlRequests.get(mapped.controlRequest.toolUseId ?? "");
|
|
1455
|
+
const event = pending ? controlRequestToToolCall(pending) : null;
|
|
1456
|
+
if (event?.type === "tool_call_requested") {
|
|
1457
|
+
active.pendingSdkToolCalls = [event];
|
|
1458
|
+
keepActiveForContinuation = true;
|
|
1459
|
+
yield event;
|
|
1460
|
+
}
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1449
1463
|
continue;
|
|
1450
1464
|
}
|
|
1451
1465
|
const toolUseIds = mapped.events.filter(isToolCallRequested).map((event) => event.toolUseId);
|
|
1466
|
+
if (active.sdkMcpEnabled) {
|
|
1467
|
+
for (const event of mapped.events) {
|
|
1468
|
+
if (event.type === "tool_call_requested") {
|
|
1469
|
+
active.collectingSdkToolCalls.set(event.toolUseId, event);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
yield event;
|
|
1473
|
+
}
|
|
1474
|
+
if (isStreamMessageStop(raw) && active.collectingSdkToolCalls.size > 0) {
|
|
1475
|
+
active.pendingSdkToolCalls = [...active.collectingSdkToolCalls.values()];
|
|
1476
|
+
active.collectingSdkToolCalls.clear();
|
|
1477
|
+
keepActiveForContinuation = true;
|
|
1478
|
+
for (const event of active.pendingSdkToolCalls) yield event;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (mapped.terminal) {
|
|
1482
|
+
keepActiveForContinuation = true;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1452
1487
|
if (toolUseIds.length > 0) active.pendingToolUseIds = toolUseIds;
|
|
1453
1488
|
for (const event of mapped.events) {
|
|
1454
1489
|
if (event.type === "tool_call_requested") keepActiveForContinuation = true;
|
|
@@ -1494,7 +1529,7 @@ var ClaudeCodeProvider = class {
|
|
|
1494
1529
|
const credentialId = this.getActiveCredentialId ? await this.getActiveCredentialId() : null;
|
|
1495
1530
|
const existing = this.active;
|
|
1496
1531
|
if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request) && existing.credentialId === credentialId) {
|
|
1497
|
-
if (existing.pendingControlRequest !== null || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
|
|
1532
|
+
if (existing.pendingControlRequest !== null || existing.pendingSdkToolCalls.length > 0 || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
|
|
1498
1533
|
await this.sendContinuation(existing, request);
|
|
1499
1534
|
return existing;
|
|
1500
1535
|
}
|
|
@@ -1508,6 +1543,9 @@ var ClaudeCodeProvider = class {
|
|
|
1508
1543
|
transport,
|
|
1509
1544
|
iterator: transport.messages()[Symbol.asyncIterator](),
|
|
1510
1545
|
pendingControlRequest: null,
|
|
1546
|
+
pendingSdkControlRequests: /* @__PURE__ */ new Map(),
|
|
1547
|
+
pendingSdkToolCalls: [],
|
|
1548
|
+
collectingSdkToolCalls: /* @__PURE__ */ new Map(),
|
|
1511
1549
|
pendingToolUseIds: [],
|
|
1512
1550
|
bufferedMessages: [],
|
|
1513
1551
|
sdkMcpEnabled: request.tools.length > 0,
|
|
@@ -1532,6 +1570,10 @@ var ClaudeCodeProvider = class {
|
|
|
1532
1570
|
await this.writeToolResults(request, active.pendingControlRequest);
|
|
1533
1571
|
active.pendingControlRequest = null;
|
|
1534
1572
|
}
|
|
1573
|
+
if (active.pendingSdkToolCalls.length > 0) {
|
|
1574
|
+
await this.writeSdkMcpToolResults(active, request);
|
|
1575
|
+
active.pendingSdkToolCalls = [];
|
|
1576
|
+
}
|
|
1535
1577
|
if (active.pendingToolUseIds.length > 0) {
|
|
1536
1578
|
await this.writeToolResultMessages(request, active.pendingToolUseIds);
|
|
1537
1579
|
active.pendingToolUseIds = [];
|
|
@@ -1585,6 +1627,15 @@ var ClaudeCodeProvider = class {
|
|
|
1585
1627
|
await this.writeControlError(active, request, "Invalid tools/call request");
|
|
1586
1628
|
return "handled";
|
|
1587
1629
|
}
|
|
1630
|
+
if (request.protocol === "sdk-mcp") {
|
|
1631
|
+
request.toolUseId ??= `mcp-control-${randomUUID()}`;
|
|
1632
|
+
const normalized = {
|
|
1633
|
+
...request,
|
|
1634
|
+
toolUseId: request.toolUseId
|
|
1635
|
+
};
|
|
1636
|
+
active.pendingSdkControlRequests.set(normalized.toolUseId, normalized);
|
|
1637
|
+
return active.hasStreamed ? "handled" : "sdk-tool-call";
|
|
1638
|
+
}
|
|
1588
1639
|
active.pendingControlRequest = {
|
|
1589
1640
|
...request,
|
|
1590
1641
|
toolUseId: `mcp-control-${randomUUID()}`
|
|
@@ -1594,6 +1645,38 @@ var ClaudeCodeProvider = class {
|
|
|
1594
1645
|
await this.writeControlError(active, request, `Unsupported method: ${request.method}`);
|
|
1595
1646
|
return "handled";
|
|
1596
1647
|
}
|
|
1648
|
+
async writeSdkMcpToolResults(active, request) {
|
|
1649
|
+
const results = new Map(request.items.filter((item) => item.type === "tool_result").map((item) => [item.toolUseId, item]));
|
|
1650
|
+
const expected = active.pendingSdkToolCalls.map((event) => event.toolUseId);
|
|
1651
|
+
const missing = expected.filter((toolUseId) => !results.has(toolUseId));
|
|
1652
|
+
if (missing.length > 0) throw new Error(`Claude Code provider missing tool_result for SDK MCP tool_use ${missing.join(", ")}`);
|
|
1653
|
+
const remaining = new Set(expected);
|
|
1654
|
+
while (remaining.size > 0) {
|
|
1655
|
+
let responded = false;
|
|
1656
|
+
for (const toolUseId of remaining) {
|
|
1657
|
+
const controlRequest = active.pendingSdkControlRequests.get(toolUseId);
|
|
1658
|
+
if (!controlRequest) continue;
|
|
1659
|
+
await this.writeToolResults(request, controlRequest);
|
|
1660
|
+
active.pendingSdkControlRequests.delete(toolUseId);
|
|
1661
|
+
remaining.delete(toolUseId);
|
|
1662
|
+
responded = true;
|
|
1663
|
+
}
|
|
1664
|
+
if (remaining.size === 0) return;
|
|
1665
|
+
if (responded) continue;
|
|
1666
|
+
const next = await abortable(active.iterator.next(), request.cancel);
|
|
1667
|
+
if (next.done) throw new Error(`Claude Code exited before requesting SDK MCP tool result for ${[...remaining].join(", ")}`);
|
|
1668
|
+
this.observeQuotaFromMessage(next.value);
|
|
1669
|
+
const mapped = mapClaudeStdoutMessage(next.value, {
|
|
1670
|
+
ignoreAssistantContent: true,
|
|
1671
|
+
ignoreAssistantToolUse: true
|
|
1672
|
+
});
|
|
1673
|
+
if (mapped.controlRequest) {
|
|
1674
|
+
if (await this.handleControlRequest(active, mapped.controlRequest, request) === "tool-call") throw new Error("Claude Code emitted a legacy tool call while awaiting SDK MCP results");
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
active.bufferedMessages.push(next.value);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1597
1680
|
async writeToolResults(request, controlRequest) {
|
|
1598
1681
|
if (!this.active) throw new Error("No active Claude transport");
|
|
1599
1682
|
const expectedToolUseId = controlRequest.toolUseId ?? String(controlRequest.id);
|
|
@@ -1757,6 +1840,9 @@ function toolResultContentToMcp(output) {
|
|
|
1757
1840
|
function isMessageType(value, type) {
|
|
1758
1841
|
return isRecord(value) && value.type === type;
|
|
1759
1842
|
}
|
|
1843
|
+
function isStreamMessageStop(value) {
|
|
1844
|
+
return isRecord(value) && value.type === "stream_event" && isRecord(value.event) && value.event.type === "message_stop";
|
|
1845
|
+
}
|
|
1760
1846
|
function thinkingSignature(request) {
|
|
1761
1847
|
return JSON.stringify(request.thinking ?? null);
|
|
1762
1848
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@demicodes/provider-claude-code",
|
|
3
3
|
"description": "Claude Code provider adapter for Demi.",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@demicodes/core": "^0.10.
|
|
15
|
-
"@demicodes/provider": "^0.10.
|
|
16
|
-
"@demicodes/utils": "^0.10.
|
|
14
|
+
"@demicodes/core": "^0.10.1",
|
|
15
|
+
"@demicodes/provider": "^0.10.1",
|
|
16
|
+
"@demicodes/utils": "^0.10.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@demicodes/agent": "^0.10.
|
|
20
|
-
"@demicodes/shell": "^0.10.
|
|
19
|
+
"@demicodes/agent": "^0.10.1",
|
|
20
|
+
"@demicodes/shell": "^0.10.1"
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"main": "./dist/index.mjs",
|