@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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.mjs +97 -11
  3. 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) && !options.ignoreAssistantContent) events.push(...mapContentArray(message.message.content, options));
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
- if (await this.handleControlRequest(active, mapped.controlRequest, request) === "tool-call") {
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.0",
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.0",
15
- "@demicodes/provider": "^0.10.0",
16
- "@demicodes/utils": "^0.10.0"
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.0",
20
- "@demicodes/shell": "^0.10.0"
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",