billion-context 0.1.31 → 0.1.32

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/index.js CHANGED
@@ -46420,7 +46420,6 @@ function rejectLegacyRoute(key, value) {
46420
46420
  // src/server.ts
46421
46421
  import http from "http";
46422
46422
  import fs3 from "fs";
46423
- import { tmpdir as tmpdir2 } from "os";
46424
46423
 
46425
46424
  // src/registry.ts
46426
46425
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -47698,9 +47697,15 @@ async function flushAllSessions() {
47698
47697
  var COMPRESS_TOOL_NAME = "compress";
47699
47698
  var ACP_TEXT_OPEN = "<acp_compress>";
47700
47699
  var ACP_TEXT_CLOSE = "</acp_compress>";
47700
+ var ACP_STATUS_OPEN = "<acp_status>";
47701
+ var ACP_STATUS_CLOSE = "</acp_status>";
47702
+ var ACP_SEARCH_OPEN = "<acp_search>";
47703
+ var ACP_SEARCH_CLOSE = "</acp_search>";
47704
+ var ACP_DECOMPRESS_OPEN = "<acp_decompress>";
47705
+ var ACP_DECOMPRESS_CLOSE = "</acp_decompress>";
47701
47706
  var COMPRESS_TOOL = {
47702
47707
  name: COMPRESS_TOOL_NAME,
47703
- description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}].",
47708
+ description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}]. REQUIRED \u2014 compress without content is invalid.",
47704
47709
  input_schema: {
47705
47710
  type: "object",
47706
47711
  properties: {
@@ -47719,23 +47724,23 @@ var COMPRESS_TOOL = {
47719
47724
  required: ["startId", "endId", "summary"]
47720
47725
  }
47721
47726
  }
47722
- }
47727
+ },
47728
+ required: ["content"]
47723
47729
  }
47724
47730
  };
47725
- function parseCompressInput(input) {
47731
+ function parseCompressInput(input, callId) {
47726
47732
  if (!input || typeof input !== "object") {
47727
47733
  log("warn", `[acp-compress-input] rejected: not object (${typeof input})`);
47728
47734
  return [];
47729
47735
  }
47730
47736
  const obj = input;
47731
- if (Array.isArray(obj.content)) {
47732
- const out = obj.content.map((r) => toRange(r)).filter((r) => r !== null);
47733
- if (out.length === 0) log("warn", `[acp-compress-input] content array but 0 valid ranges. keys per item: ${obj.content.map((c) => Object.keys(c ?? {}).join(",")).join(" | ")}`);
47734
- return out;
47735
- }
47736
47737
  const single = toRange(obj);
47737
- if (!single) log("warn", `[acp-compress-input] no content array, single-parse failed. top keys: ${Object.keys(obj).join(",")}`);
47738
- return single ? [single] : [];
47738
+ const ranges = Array.isArray(obj.content) ? obj.content.map((r) => toRange(r)).filter((r) => r !== null) : single ? [single] : [];
47739
+ if (ranges.length === 0) {
47740
+ log("warn", `[acp-compress-input] parsed 0 valid ranges. top keys: ${Object.keys(obj).join(",")}`);
47741
+ }
47742
+ if (callId) for (const r of ranges) r.compressCallId = callId;
47743
+ return ranges;
47739
47744
  }
47740
47745
  function toRange(r) {
47741
47746
  const startRef = pick(r, "startId", "startRef");
@@ -47764,7 +47769,7 @@ var COMPRESS_TOOL_OPENAI = {
47764
47769
  topic: { type: "string", description: "Optional short title for the compressed range" },
47765
47770
  content: {
47766
47771
  type: "array",
47767
- description: "One or more ranges to compress into separate summary blocks",
47772
+ description: "One or more ranges to compress into separate summary blocks. REQUIRED \u2014 compress without content is invalid.",
47768
47773
  items: {
47769
47774
  type: "object",
47770
47775
  properties: {
@@ -47776,7 +47781,8 @@ var COMPRESS_TOOL_OPENAI = {
47776
47781
  required: ["startId", "endId", "summary"]
47777
47782
  }
47778
47783
  }
47779
- }
47784
+ },
47785
+ required: ["content"]
47780
47786
  }
47781
47787
  }
47782
47788
  };
@@ -47826,7 +47832,29 @@ Rules for the trigger:
47826
47832
  - JSON shape matches the compress tool: {"content":[{startId,endId,summary,topic?}]}. Batch multiple ranges in one trigger.
47827
47833
  - After emitting the marker, STOP your turn. Do not continue with other text \u2014 the proxy will execute the compression and return the result, then you continue fresh.
47828
47834
  - Do NOT wrap the marker in code fences, quotes, or commentary.
47829
- - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.`;
47835
+ - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.
47836
+
47837
+ ACP TOOLS (TEXT TRIGGERS)
47838
+
47839
+ Since host tools cannot coexist with a declared tools field, ALL ACP tools use text triggers. Emit the marker; the proxy intercepts and executes it; the marker is stripped from what the user sees.
47840
+
47841
+ 1. acp_status \u2014 view context usage, compression state, and compressible ranges:
47842
+ ${ACP_STATUS_OPEN}${ACP_STATUS_CLOSE}
47843
+ No payload needed. Use this FIRST when unsure about context state.
47844
+
47845
+ 2. search_context \u2014 search compressed block summaries by keyword:
47846
+ ${ACP_SEARCH_OPEN}{"query":"auth token refresh"}${ACP_SEARCH_CLOSE}
47847
+ Use when you need details that may have been compressed away.
47848
+
47849
+ 3. decompress \u2014 restore compressed content for exact details:
47850
+ ${ACP_DECOMPRESS_OPEN}{"blockId":"b5"}${ACP_DECOMPRESS_CLOSE}
47851
+ Optional: {"blockId":"b5","toFile":"/tmp/b5.txt"} to write to file instead.
47852
+ Optional: {"blockId":"b5","full":true} to restore all the way to original messages.
47853
+
47854
+ Rules for ALL triggers:
47855
+ - Output on its own, NO surrounding prose. Just the raw marker.
47856
+ - After emitting, STOP your turn. The proxy executes and returns the result.
47857
+ - Do NOT wrap in code fences, quotes, or commentary.`;
47830
47858
  }
47831
47859
  var DECOMPRESS_TOOL_NAME = "decompress";
47832
47860
  var DECOMPRESS_TOOL_OPENAI = {
@@ -47936,6 +47964,14 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
47936
47964
  SEARCH_CONTEXT_TOOL_NAME,
47937
47965
  ACP_STATUS_TOOL_NAME
47938
47966
  ]);
47967
+ var MUTATING_PROXY_TOOLS = /* @__PURE__ */ new Set([
47968
+ COMPRESS_TOOL_NAME,
47969
+ DECOMPRESS_TOOL_NAME
47970
+ ]);
47971
+ var READONLY_PROXY_TOOLS = /* @__PURE__ */ new Set([
47972
+ SEARCH_CONTEXT_TOOL_NAME,
47973
+ ACP_STATUS_TOOL_NAME
47974
+ ]);
47939
47975
 
47940
47976
  // src/decompress-shared.ts
47941
47977
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -48595,7 +48631,9 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
48595
48631
  }).filter((tc) => tc.name.length > 0);
48596
48632
  const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
48597
48633
  const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
48598
- const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
48634
+ const mutatingProxy = proxyCalls.filter((tc) => MUTATING_PROXY_TOOLS.has(tc.name));
48635
+ const readonlyProxy = proxyCalls.filter((tc) => READONLY_PROXY_TOOLS.has(tc.name));
48636
+ const hasMutatingOnly = mutatingProxy.length > 0 && realCalls.length === 0;
48599
48637
  if (usage) {
48600
48638
  const prompt = usage.prompt_tokens ?? usage.input_tokens;
48601
48639
  const det = usage.prompt_tokens_details ?? usage.prompt_cache_hit_tokens;
@@ -48611,7 +48649,27 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
48611
48649
  ctx.session.stats.cacheSamples += 1;
48612
48650
  }
48613
48651
  }
48614
- if (!hasOnlyProxy) {
48652
+ if (!hasMutatingOnly) {
48653
+ for (const tc of readonlyProxy) {
48654
+ let args = {};
48655
+ try {
48656
+ args = JSON.parse(tc.arguments);
48657
+ } catch {
48658
+ args = {};
48659
+ }
48660
+ let result;
48661
+ try {
48662
+ result = executeProxyTool(tc.name, args, ctx);
48663
+ } catch (e) {
48664
+ result = `[${tc.name} FAILED: ${e instanceof Error ? e.message : String(e)}]`;
48665
+ }
48666
+ const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48667
+ ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48668
+ yield Buffer.from(
48669
+ buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
48670
+ "utf8"
48671
+ );
48672
+ }
48615
48673
  for (const tc of realCalls) {
48616
48674
  yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
48617
48675
  }
@@ -48867,8 +48925,23 @@ async function* compressLoopAnthropicStream(initialUpstream, ctx, requestBody, r
48867
48925
  }
48868
48926
  clientIndex = state.clientIndex;
48869
48927
  const proxyCalls = [...state.toolBlocks.values()].filter((b2) => PROXY_TOOL_NAMES.has(b2.name));
48870
- const hasOnlyProxy = proxyCalls.length > 0 && !hasRealToolUse;
48871
- if (!hasOnlyProxy) {
48928
+ const mutatingProxy = proxyCalls.filter((b2) => MUTATING_PROXY_TOOLS.has(b2.name));
48929
+ const readonlyProxy = proxyCalls.filter((b2) => READONLY_PROXY_TOOLS.has(b2.name));
48930
+ const hasMutatingOnly = mutatingProxy.length > 0 && !hasRealToolUse;
48931
+ if (!hasMutatingOnly) {
48932
+ for (const tc of readonlyProxy) {
48933
+ const args = safeParse2(tc.json);
48934
+ let result;
48935
+ try {
48936
+ result = executeProxyTool2(tc.name, args, ctx);
48937
+ } catch (e) {
48938
+ result = `[${tc.name} FAILED: ${e instanceof Error ? e.message : String(e)}]`;
48939
+ }
48940
+ const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48941
+ ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48942
+ yield Buffer.from(buildTextBlockSse(clientIndex, buildVisibilityMarker(tc.name, result)), "utf8");
48943
+ clientIndex++;
48944
+ }
48872
48945
  const stop = hasRealToolUse ? "tool_use" : roundStopReason ?? "end_turn";
48873
48946
  yield Buffer.from(buildTerminalSse(stop, totalOutputTokens, totalInputTokens, totalCachedTokens, messageId, model), "utf8");
48874
48947
  return;
@@ -49281,6 +49354,31 @@ function replaceResponsesJsonText(parts, text) {
49281
49354
  part.text = index === 0 ? text : "";
49282
49355
  });
49283
49356
  }
49357
+ function surfaceReadonlyJson(current, proxyCalls, ctx) {
49358
+ const markers = [];
49359
+ for (const call of proxyCalls) {
49360
+ if (MUTATING_PROXY_TOOLS.has(call.name)) continue;
49361
+ let args = {};
49362
+ try {
49363
+ args = JSON.parse(call.arguments);
49364
+ } catch {
49365
+ args = {};
49366
+ }
49367
+ let result;
49368
+ try {
49369
+ result = executeProxyTool3(call.name, args, ctx);
49370
+ ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) \u2192 ${result.slice(0, 120).replace(/\n/g, " ")}]`);
49371
+ } catch (e) {
49372
+ result = `\u274C [ACP] ${call.name} FAILED: ${String(e)}`;
49373
+ ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) FAILED: ${String(e)}]`);
49374
+ }
49375
+ markers.push(buildVisibilityMarker(call.name, result));
49376
+ }
49377
+ if (markers.length === 0) return current;
49378
+ const out = Array.isArray(current.output) ? [...current.output] : [];
49379
+ out.push({ type: "message", id: `msg_acp_ro_${Date.now()}_${markers.length}`, role: "assistant", content: [{ type: "output_text", text: markers.join("\n") }] });
49380
+ return { ...current, output: out };
49381
+ }
49284
49382
  async function compressLoopResponsesJson(initialResponse, ctx, requestBody, requestOptions) {
49285
49383
  let current = initialResponse;
49286
49384
  for (let loopCount = 1; loopCount <= 5; loopCount++) {
@@ -49289,8 +49387,12 @@ async function compressLoopResponsesJson(initialResponse, ctx, requestBody, requ
49289
49387
  const allCalls = [...output.calls, ...extracted.calls].filter((call) => call.name.length > 0);
49290
49388
  const proxyCalls = allCalls.filter((call) => PROXY_TOOL_NAMES.has(call.name));
49291
49389
  const realCalls = allCalls.filter((call) => !PROXY_TOOL_NAMES.has(call.name));
49292
- if (proxyCalls.length === 0 || realCalls.length > 0) {
49293
- if (proxyCalls.length > 0) replaceResponsesJsonText(output.textParts, extracted.clean);
49390
+ const mutatingProxy = proxyCalls.filter((call) => MUTATING_PROXY_TOOLS.has(call.name));
49391
+ if (mutatingProxy.length === 0 || realCalls.length > 0) {
49392
+ if (proxyCalls.length > 0) {
49393
+ replaceResponsesJsonText(output.textParts, extracted.clean);
49394
+ current = surfaceReadonlyJson(current, proxyCalls, ctx);
49395
+ }
49294
49396
  return current;
49295
49397
  }
49296
49398
  const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
@@ -49437,9 +49539,30 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
49437
49539
  const allCalls = [...fcByItemId.values()].filter((c) => c.name.length > 0);
49438
49540
  const proxyCalls = allCalls.filter((c) => PROXY_TOOL_NAMES.has(c.name));
49439
49541
  const realCalls = allCalls.filter((c) => !PROXY_TOOL_NAMES.has(c.name));
49542
+ const readonlyProxy = proxyCalls.filter((c) => READONLY_PROXY_TOOLS.has(c.name));
49440
49543
  log("debug", `[acp-diag] round ${loopCount} allCalls=[${allCalls.map((c) => c.name).join(",")}] realCalls=[${realCalls.map((c) => c.name).join(",")}] customToolCalls=${customToolCalls} text=${JSON.stringify(contentText.slice(0, 120))}`);
49441
- const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
49442
- if (!hasOnlyProxy) {
49544
+ const hasMutatingOnly = proxyCalls.some((c) => MUTATING_PROXY_TOOLS.has(c.name)) && realCalls.length === 0;
49545
+ if (!hasMutatingOnly) {
49546
+ for (const fc of readonlyProxy) {
49547
+ let args = {};
49548
+ try {
49549
+ args = JSON.parse(fc.arguments);
49550
+ } catch (e) {
49551
+ log("warn", `[acp-compress-args] ${fc.name} JSON.parse failed: ${String(e)}`);
49552
+ args = {};
49553
+ }
49554
+ let result;
49555
+ try {
49556
+ result = executeProxyTool3(fc.name, args, ctx);
49557
+ const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
49558
+ ctx.log(`[acp-proxy: responses ${fc.name} (read-only) (${fc.callId}) \u2192 ${preview.replace(/\n/g, " ")}]`);
49559
+ } catch (e) {
49560
+ result = `\u274C [ACP] ${fc.name} FAILED: ${String(e)}`;
49561
+ ctx.log(`[acp-proxy: responses ${fc.name} (read-only) (${fc.callId}) FAILED: ${String(e)}]`);
49562
+ }
49563
+ const markerItemId = `msg_acp_ro_${Date.now()}_${nextOutputIndex}`;
49564
+ yield Buffer.from(buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)), "utf8");
49565
+ }
49443
49566
  let oi2 = nextOutputIndex;
49444
49567
  for (const fc of realCalls) {
49445
49568
  yield Buffer.from(buildFunctionCallEvents(fc, oi2), "utf8");
@@ -49451,7 +49574,8 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
49451
49574
  return;
49452
49575
  }
49453
49576
  const hasUsage = !!responseObj?.usage;
49454
- if (contentText.length === 0 && realCalls.length === 0 && customToolCalls === 0 && !hasUsage) {
49577
+ const emittedReadonly = readonlyProxy.length > 0;
49578
+ if (contentText.length === 0 && realCalls.length === 0 && customToolCalls === 0 && !emittedReadonly && !hasUsage) {
49455
49579
  ctx.log("[acp-proxy: empty upstream response (no content/usage) \u2014 injecting response.failed for client retry]");
49456
49580
  yield Buffer.from(buildFailed(responseObj), "utf8");
49457
49581
  return;
@@ -49532,6 +49656,1070 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
49532
49656
  }
49533
49657
  }
49534
49658
 
49659
+ // src/loop/core.ts
49660
+ var MAX_LOOP_ROUNDS = 10;
49661
+ function executeProxyTool4(toolName, args, ctx, callId) {
49662
+ if (toolName === "compress") {
49663
+ return applyRanges(parseCompressInput(args, callId), ctx);
49664
+ }
49665
+ if (toolName === "decompress") {
49666
+ return resolveDecompress(args, ctx);
49667
+ }
49668
+ if (toolName === "search_context") {
49669
+ const query = typeof args.query === "string" ? args.query : "";
49670
+ if (query.length === 0) return "[search_context FAILED: query is required]";
49671
+ const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
49672
+ const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
49673
+ if (blocks.length === 0) return `[No blocks matched "${query}"]`;
49674
+ const lines = blocks.map((b2) => {
49675
+ const topic = b2.topic ?? "(no topic)";
49676
+ const preview = b2.summary.length > 200 ? b2.summary.slice(0, 200) + "..." : b2.summary;
49677
+ return `${b2.blockId} (T${b2.tier}) "${topic}"
49678
+ ${preview}`;
49679
+ });
49680
+ return `Found ${blocks.length} block(s) for "${query}":
49681
+
49682
+ ${lines.join("\n\n")}`;
49683
+ }
49684
+ if (toolName === "acp_status") {
49685
+ return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
49686
+ }
49687
+ return `[Unknown proxy tool: ${toolName}]`;
49688
+ }
49689
+ function recordUsage(ctx, usage, round) {
49690
+ const prompt = usage.inputTokens;
49691
+ const cached = usage.cachedTokens;
49692
+ const out = usage.outputTokens;
49693
+ if (typeof prompt === "number") ctx.session.stats.inputTokens += prompt;
49694
+ ctx.session.stats.lastInputTokens = (typeof prompt === "number" ? prompt : 0) + (typeof cached === "number" ? cached : 0);
49695
+ if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
49696
+ if (typeof out === "number") ctx.session.stats.outputTokens += out;
49697
+ ctx.session.stats.cacheSamples += 1;
49698
+ const hitPct = typeof prompt === "number" && typeof cached === "number" && prompt + cached > 0 ? Math.round(cached / (prompt + cached) * 100) : 0;
49699
+ ctx.log(
49700
+ `[acp-usage] round ${round} input=${ctx.session.stats.lastInputTokens} cached=${cached ?? 0} (cache hit ${hitPct}%)`
49701
+ );
49702
+ }
49703
+ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adapter, systemPrompt) {
49704
+ let activeClearTimer = null;
49705
+ let currentUpstream = upstream;
49706
+ const coreMessages = [...ctx.messages];
49707
+ try {
49708
+ for (let round = 1; round <= MAX_LOOP_ROUNDS; round++) {
49709
+ let assistantText = "";
49710
+ const calls = [];
49711
+ let usage = {};
49712
+ let finishReason;
49713
+ for await (const ev of adapter.parseStream(currentUpstream, round)) {
49714
+ if (ev.kind === "text") {
49715
+ assistantText += ev.delta;
49716
+ if (!ctx.textProtocol && round === 1 && ev.raw) {
49717
+ yield ev.raw;
49718
+ }
49719
+ } else if (ev.kind === "tool_call") {
49720
+ calls.push({ name: ev.name, callId: ev.callId, arguments: ev.arguments });
49721
+ } else if (ev.kind === "usage") {
49722
+ usage = {
49723
+ inputTokens: ev.inputTokens,
49724
+ outputTokens: ev.outputTokens,
49725
+ cachedTokens: ev.cachedTokens
49726
+ };
49727
+ } else if (ev.kind === "done") {
49728
+ finishReason = ev.finishReason;
49729
+ } else if (ev.kind === "meta") {
49730
+ if (round === 1 || !ev.firstRoundOnly) {
49731
+ yield ev.chunk;
49732
+ }
49733
+ }
49734
+ }
49735
+ if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0 || usage.cachedTokens !== void 0) {
49736
+ recordUsage(ctx, usage, round);
49737
+ }
49738
+ let resolvedText = assistantText;
49739
+ let allCalls = calls;
49740
+ if (ctx.textProtocol && assistantText.length > 0 && adapter.extractTextTriggers) {
49741
+ const extracted = adapter.extractTextTriggers(assistantText);
49742
+ resolvedText = extracted.clean;
49743
+ allCalls = [...calls, ...extracted.calls];
49744
+ }
49745
+ if (ctx.textProtocol && resolvedText.length > 0) {
49746
+ yield adapter.emitText(resolvedText);
49747
+ } else if (!ctx.textProtocol && round > 1 && resolvedText.length > 0) {
49748
+ yield adapter.emitText(resolvedText);
49749
+ }
49750
+ let realCalls = 0;
49751
+ const realToolCalls = [];
49752
+ const proxyResults = [];
49753
+ for (const call of allCalls) {
49754
+ if (PROXY_TOOL_NAMES.has(call.name)) {
49755
+ let parsedArgs;
49756
+ try {
49757
+ parsedArgs = call.arguments.length > 0 ? JSON.parse(call.arguments) : {};
49758
+ } catch {
49759
+ parsedArgs = {};
49760
+ }
49761
+ const result = executeProxyTool4(call.name, parsedArgs, ctx, call.callId);
49762
+ proxyResults.push({ name: call.name, callId: call.callId, result, arguments: call.arguments });
49763
+ yield adapter.emitMarker(call.name, result);
49764
+ } else {
49765
+ realToolCalls.push(call);
49766
+ realCalls += 1;
49767
+ }
49768
+ }
49769
+ if (ctx.debug) {
49770
+ const callSummary = allCalls.map((c) => {
49771
+ const argSnippet = c.arguments.length > 200 ? c.arguments.slice(0, 200) + "..." : c.arguments;
49772
+ return `${c.name}(${argSnippet})`;
49773
+ }).join(" | ");
49774
+ ctx.log(`[acp-loop] round ${round}: ${allCalls.length} call(s): ${callSummary || "(none)"}`);
49775
+ for (const pr2 of proxyResults) {
49776
+ const resSnippet = pr2.result.length > 300 ? pr2.result.slice(0, 300) + "..." : pr2.result;
49777
+ ctx.log(`[acp-loop] \u2192 ${pr2.name} result: ${resSnippet}`);
49778
+ }
49779
+ if (realCalls > 0) ctx.log(`[acp-loop] round ${round}: ${realCalls} real tool call(s) forwarded to client`);
49780
+ }
49781
+ if (proxyResults.length > 0) {
49782
+ if (resolvedText.length > 0) {
49783
+ coreMessages.push({
49784
+ id: `acp_loop_r${round}_asst`,
49785
+ role: "assistant",
49786
+ contentType: "text",
49787
+ text: resolvedText
49788
+ });
49789
+ }
49790
+ for (const pr2 of proxyResults) {
49791
+ if (ctx.textProtocol) {
49792
+ coreMessages.push({
49793
+ id: `acp_loop_r${round}_marker_${pr2.callId}`,
49794
+ role: "user",
49795
+ contentType: "text",
49796
+ text: buildVisibilityMarker(pr2.name, pr2.result)
49797
+ });
49798
+ } else {
49799
+ coreMessages.push({
49800
+ id: `acp_loop_r${round}_asst_tc_${pr2.callId}`,
49801
+ role: "assistant",
49802
+ contentType: "tool-call",
49803
+ toolName: pr2.name,
49804
+ toolCallId: pr2.callId,
49805
+ text: pr2.arguments
49806
+ });
49807
+ coreMessages.push({
49808
+ id: `acp_loop_r${round}_tool_${pr2.callId}`,
49809
+ role: "tool",
49810
+ contentType: "tool-result",
49811
+ toolCallId: pr2.callId,
49812
+ text: pr2.result
49813
+ });
49814
+ }
49815
+ }
49816
+ if (!ctx.textProtocol) {
49817
+ const hidden = hideConsumedCompressCalls(ctx.session.state, coreMessages);
49818
+ if (hidden.hidden > 0) {
49819
+ ctx.log(`[acp-loop] round ${round} hideConsumed hid ${hidden.hidden} compress record(s)`);
49820
+ coreMessages.length = 0;
49821
+ coreMessages.push(...hidden.messages);
49822
+ }
49823
+ }
49824
+ }
49825
+ for (const tc of realToolCalls) {
49826
+ yield adapter.emitToolCall(tc);
49827
+ }
49828
+ const reRequest = proxyResults.length > 0 && realCalls === 0;
49829
+ if (!reRequest) {
49830
+ yield adapter.emitCompletion({ finishReason, usage });
49831
+ return;
49832
+ }
49833
+ if (round >= MAX_LOOP_ROUNDS) {
49834
+ ctx.log(`[acp-loop] round ${round} hit MAX_LOOP_ROUNDS; completing gracefully`);
49835
+ log("warn", `[acp-loop] loop limit (${MAX_LOOP_ROUNDS}) reached; completing gracefully`);
49836
+ yield adapter.emitCompletion({ finishReason: "length", usage });
49837
+ return;
49838
+ }
49839
+ ctx.log(`[acp-loop] round ${round} saw mutating proxy tool; re-requesting`);
49840
+ const newBody = adapter.buildRequest(coreMessages, systemPrompt, requestBody);
49841
+ if (process.env.ACP_DUMP_REQ !== "0" && ctx.debug) {
49842
+ try {
49843
+ const fs5 = await import("fs");
49844
+ const dumpDir = process.env.ACP_DUMP_DIR || `${process.env.HOME}/.local/state/billion-context/dumps`;
49845
+ fs5.mkdirSync(dumpDir, { recursive: true });
49846
+ const sid = ctx.session.id ?? "unknown";
49847
+ fs5.writeFileSync(`${dumpDir}/req-${Date.now()}-${sid}-REREQUEST.json`, JSON.stringify(newBody, null, 2));
49848
+ } catch {
49849
+ }
49850
+ }
49851
+ const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
49852
+ method: "POST",
49853
+ headers: requestOptions.headers,
49854
+ body: JSON.stringify(newBody),
49855
+ ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
49856
+ });
49857
+ if (!resp.ok || !resp.body) {
49858
+ clearTimer();
49859
+ const errText = await resp.text().catch(() => "upstream error");
49860
+ ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
49861
+ log("error", `[acp-loop] upstream error ${resp.status}: ${errText.slice(0, 200)}`);
49862
+ yield adapter.emitError(`upstream error ${resp.status}: ${errText.slice(0, 200)}`);
49863
+ return;
49864
+ }
49865
+ currentUpstream = resp.body;
49866
+ if (activeClearTimer) activeClearTimer();
49867
+ activeClearTimer = clearTimer;
49868
+ }
49869
+ } finally {
49870
+ if (activeClearTimer) {
49871
+ activeClearTimer();
49872
+ activeClearTimer = null;
49873
+ }
49874
+ }
49875
+ }
49876
+
49877
+ // src/loop/adapter-responses.ts
49878
+ async function* iterSseEvents(stream2) {
49879
+ const reader = stream2.getReader();
49880
+ let buf = "";
49881
+ try {
49882
+ while (true) {
49883
+ let done;
49884
+ let value;
49885
+ try {
49886
+ ({ done, value } = await reader.read());
49887
+ } catch {
49888
+ break;
49889
+ }
49890
+ if (done) break;
49891
+ buf += new TextDecoder().decode(value, { stream: true });
49892
+ buf = buf.replace(/\r\n|\r/g, "\n");
49893
+ let idx;
49894
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
49895
+ const raw = buf.slice(0, idx);
49896
+ buf = buf.slice(idx + 2);
49897
+ if (raw.trim().length > 0) yield raw;
49898
+ }
49899
+ }
49900
+ if (buf.trim().length > 0) yield buf;
49901
+ } finally {
49902
+ reader.releaseLock();
49903
+ }
49904
+ }
49905
+ function extractEventType2(rawEvent) {
49906
+ for (const l of rawEvent.split("\n")) {
49907
+ if (l.startsWith("event:")) return l.slice(6).trim();
49908
+ }
49909
+ return null;
49910
+ }
49911
+ function extractDataLine2(rawEvent) {
49912
+ const parts = [];
49913
+ for (const l of rawEvent.split("\n")) {
49914
+ if (l.startsWith("data:")) {
49915
+ let v2 = l.slice(5);
49916
+ if (v2.startsWith(" ")) v2 = v2.slice(1);
49917
+ parts.push(v2);
49918
+ }
49919
+ }
49920
+ return parts.length ? parts.join("\n") : null;
49921
+ }
49922
+ function buildMessageItemSequence2(itemId, outputIndex, text) {
49923
+ const item = { type: "message", id: itemId, role: "assistant", content: [] };
49924
+ const part = { type: "output_text", text: "" };
49925
+ const doneItem = {
49926
+ type: "message",
49927
+ id: itemId,
49928
+ role: "assistant",
49929
+ content: [{ type: "output_text", text }]
49930
+ };
49931
+ return Buffer.from(
49932
+ [
49933
+ `event: response.output_item.added
49934
+ data: ${JSON.stringify({ type: "response.output_item.added", output_index: outputIndex, item })}
49935
+
49936
+ `,
49937
+ `event: response.content_part.added
49938
+ data: ${JSON.stringify({ type: "response.content_part.added", item_id: itemId, output_index: outputIndex, part })}
49939
+
49940
+ `,
49941
+ `event: response.output_text.delta
49942
+ data: ${JSON.stringify({ type: "response.output_text.delta", item_id: itemId, output_index: outputIndex, delta: text })}
49943
+
49944
+ `,
49945
+ `event: response.output_text.done
49946
+ data: ${JSON.stringify({ type: "response.output_text.done", item_id: itemId, output_index: outputIndex, text })}
49947
+
49948
+ `,
49949
+ `event: response.content_part.done
49950
+ data: ${JSON.stringify({ type: "response.content_part.done", item_id: itemId, output_index: outputIndex, part: { type: "output_text", text } })}
49951
+
49952
+ `,
49953
+ `event: response.output_item.done
49954
+ data: ${JSON.stringify({ type: "response.output_item.done", output_index: outputIndex, item: doneItem })}
49955
+
49956
+ `
49957
+ ].join(""),
49958
+ "utf8"
49959
+ );
49960
+ }
49961
+ function buildFunctionCallEvents2(fc, itemId, outputIndex) {
49962
+ return Buffer.from(
49963
+ [
49964
+ `event: response.output_item.added
49965
+ data: ${JSON.stringify({
49966
+ type: "response.output_item.added",
49967
+ output_index: outputIndex,
49968
+ item: { type: "function_call", id: itemId, call_id: fc.callId, name: fc.name, arguments: "" }
49969
+ })}
49970
+
49971
+ `,
49972
+ `event: response.function_call_arguments.delta
49973
+ data: ${JSON.stringify({
49974
+ type: "response.function_call_arguments.delta",
49975
+ item_id: itemId,
49976
+ delta: fc.arguments
49977
+ })}
49978
+
49979
+ `,
49980
+ `event: response.function_call_arguments.done
49981
+ data: ${JSON.stringify({
49982
+ type: "response.function_call_arguments.done",
49983
+ item_id: itemId,
49984
+ arguments: fc.arguments
49985
+ })}
49986
+
49987
+ `,
49988
+ `event: response.output_item.done
49989
+ data: ${JSON.stringify({
49990
+ type: "response.output_item.done",
49991
+ output_index: outputIndex,
49992
+ item: { type: "function_call", id: itemId, call_id: fc.callId, name: fc.name, arguments: fc.arguments }
49993
+ })}
49994
+
49995
+ `
49996
+ ].join(""),
49997
+ "utf8"
49998
+ );
49999
+ }
50000
+ function buildCompleted2(responseObj) {
50001
+ const resp = responseObj ?? { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
50002
+ return Buffer.from(
50003
+ `event: response.completed
50004
+ data: ${JSON.stringify({ type: "response.completed", response: resp })}
50005
+
50006
+ `,
50007
+ "utf8"
50008
+ );
50009
+ }
50010
+ function createResponsesAdapter(textProtocol, projection) {
50011
+ const suppressTextLifecycle = !!textProtocol;
50012
+ let outputIndex = 0;
50013
+ let responseObj = null;
50014
+ let terminalRaw = null;
50015
+ let terminalKind = null;
50016
+ return {
50017
+ buildRequest(coreMessages, systemPrompt, requestBody) {
50018
+ const customToolCallIds = /* @__PURE__ */ new Set();
50019
+ for (const m2 of coreMessages) {
50020
+ const bm = m2;
50021
+ const raw = bm?.rawResponsesItem;
50022
+ if (raw && (raw.type === "custom_tool_call" || raw.type === "custom_tool_call_output")) {
50023
+ const id = typeof raw.call_id === "string" ? raw.call_id : typeof raw.id === "string" ? raw.id : "";
50024
+ if (id) customToolCallIds.add(id);
50025
+ }
50026
+ }
50027
+ let inputItems;
50028
+ if (projection) {
50029
+ const rebuiltInput = patchResponsesInput(projection, coreMessages);
50030
+ inputItems = typeof rebuiltInput === "string" ? [{ type: "message", role: "user", content: rebuiltInput }] : rebuiltInput;
50031
+ } else {
50032
+ inputItems = coreToResponses(coreMessages, customToolCallIds);
50033
+ }
50034
+ const devParts = projection && projection.systemParts.length > 0 ? [...projection.systemParts, systemPrompt] : [systemPrompt];
50035
+ const withDev = injectResponsesDeveloperMessage(inputItems, devParts.join("\n\n---\n\n"));
50036
+ const rebuilt = { ...requestBody, input: withDev };
50037
+ delete rebuilt.previous_response_id;
50038
+ delete rebuilt.instructions;
50039
+ return rebuilt;
50040
+ },
50041
+ async *parseStream(upstream, round) {
50042
+ const pending = /* @__PURE__ */ new Map();
50043
+ for await (const eventStr of iterSseEvents(upstream)) {
50044
+ const type = extractEventType2(eventStr);
50045
+ const dataLine = extractDataLine2(eventStr);
50046
+ if (!type || !dataLine) continue;
50047
+ let obj;
50048
+ try {
50049
+ obj = JSON.parse(dataLine);
50050
+ } catch {
50051
+ continue;
50052
+ }
50053
+ const rawBuf = Buffer.from(eventStr + "\n\n", "utf8");
50054
+ if (round === 1 && typeof obj.output_index === "number") {
50055
+ outputIndex = Math.max(outputIndex, obj.output_index + 1);
50056
+ }
50057
+ if (type === "response.created" || type === "response.in_progress") {
50058
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50059
+ } else if (type === "response.output_item.added") {
50060
+ const item = obj.item;
50061
+ if (item?.type === "function_call") {
50062
+ const itemId = typeof item.id === "string" ? item.id : "";
50063
+ pending.set(itemId, {
50064
+ itemId,
50065
+ callId: typeof item.call_id === "string" ? item.call_id : "",
50066
+ name: typeof item.name === "string" ? item.name : "",
50067
+ arguments: ""
50068
+ });
50069
+ } else if (item?.type === "custom_tool_call") {
50070
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: false };
50071
+ } else if (!suppressTextLifecycle) {
50072
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50073
+ }
50074
+ } else if (type === "response.content_part.added" || type === "response.content_part.done" || type === "response.output_text.done") {
50075
+ if (!suppressTextLifecycle) {
50076
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50077
+ }
50078
+ } else if (type === "response.output_text.delta") {
50079
+ const delta = typeof obj.delta === "string" ? obj.delta : "";
50080
+ if (delta.length > 0) {
50081
+ yield { kind: "text", delta, raw: rawBuf };
50082
+ }
50083
+ } else if (type === "response.function_call_arguments.delta") {
50084
+ const itemId = typeof obj.item_id === "string" ? obj.item_id : "";
50085
+ const delta = typeof obj.delta === "string" ? obj.delta : "";
50086
+ const fc = pending.get(itemId);
50087
+ if (fc) fc.arguments += delta;
50088
+ } else if (type === "response.function_call_arguments.done") {
50089
+ const itemId = typeof obj.item_id === "string" ? obj.item_id : "";
50090
+ const args = typeof obj.arguments === "string" ? obj.arguments : "";
50091
+ const fc = pending.get(itemId);
50092
+ if (fc && args) fc.arguments = args;
50093
+ } else if (type === "response.output_item.done") {
50094
+ const item = obj.item;
50095
+ if (item?.type === "function_call") {
50096
+ const itemId = typeof item.id === "string" ? item.id : "";
50097
+ const fc = pending.get(itemId);
50098
+ if (fc) {
50099
+ if (typeof item.arguments === "string" && item.arguments) fc.arguments = item.arguments;
50100
+ pending.delete(itemId);
50101
+ yield {
50102
+ kind: "tool_call",
50103
+ name: fc.name,
50104
+ callId: fc.callId,
50105
+ arguments: fc.arguments
50106
+ };
50107
+ }
50108
+ } else if (item?.type === "custom_tool_call") {
50109
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: false };
50110
+ } else if (!suppressTextLifecycle) {
50111
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50112
+ }
50113
+ } else if (type === "response.completed") {
50114
+ responseObj = obj.response ?? null;
50115
+ terminalKind = "completed";
50116
+ terminalRaw = null;
50117
+ const respUsage = responseObj?.usage;
50118
+ const pd = respUsage?.input_tokens_details;
50119
+ yield {
50120
+ kind: "usage",
50121
+ inputTokens: typeof respUsage?.input_tokens === "number" ? respUsage.input_tokens : void 0,
50122
+ outputTokens: typeof respUsage?.output_tokens === "number" ? respUsage.output_tokens : void 0,
50123
+ cachedTokens: typeof pd?.cached_tokens === "number" ? pd.cached_tokens : void 0
50124
+ };
50125
+ yield { kind: "done", finishReason: "completed" };
50126
+ } else if (type === "response.incomplete") {
50127
+ terminalKind = "incomplete";
50128
+ terminalRaw = rawBuf;
50129
+ yield { kind: "done", finishReason: "incomplete" };
50130
+ } else if (type === "response.failed" || type === "response.error") {
50131
+ terminalKind = "failed";
50132
+ terminalRaw = rawBuf;
50133
+ yield { kind: "done", finishReason: "failed" };
50134
+ } else {
50135
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50136
+ }
50137
+ }
50138
+ if (!terminalKind) {
50139
+ yield { kind: "done", finishReason: "failed" };
50140
+ }
50141
+ },
50142
+ emitText(delta) {
50143
+ return buildMessageItemSequence2(`msg-proxy-${Date.now()}-${outputIndex}`, outputIndex++, delta);
50144
+ },
50145
+ emitToolCall(call) {
50146
+ const buf = buildFunctionCallEvents2(call, `fc-proxy-${Date.now()}-${outputIndex}`, outputIndex);
50147
+ outputIndex += 1;
50148
+ return buf;
50149
+ },
50150
+ emitMarker(toolName, result) {
50151
+ return buildMessageItemSequence2(
50152
+ `marker-${Date.now()}-${outputIndex}`,
50153
+ outputIndex++,
50154
+ buildVisibilityMarker(toolName, result)
50155
+ );
50156
+ },
50157
+ emitCompletion(opts) {
50158
+ if (terminalRaw && (terminalKind === "failed" || terminalKind === "incomplete")) {
50159
+ return terminalRaw;
50160
+ }
50161
+ if (!responseObj && opts?.finishReason === "failed") {
50162
+ const failed = {
50163
+ id: `resp-error-${Date.now()}`,
50164
+ status: "failed",
50165
+ error: { code: "server_error", message: "upstream returned no response" }
50166
+ };
50167
+ return Buffer.from(
50168
+ `event: response.failed
50169
+ data: ${JSON.stringify({ type: "response.failed", response: failed })}
50170
+
50171
+ `,
50172
+ "utf8"
50173
+ );
50174
+ }
50175
+ let resp = responseObj ? { ...responseObj } : { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
50176
+ if (opts?.usage) {
50177
+ const usage = {
50178
+ ...typeof resp.usage === "object" ? resp.usage : {}
50179
+ };
50180
+ if (typeof opts.usage.inputTokens === "number") usage.input_tokens = opts.usage.inputTokens;
50181
+ if (typeof opts.usage.outputTokens === "number") usage.output_tokens = opts.usage.outputTokens;
50182
+ if (typeof opts.usage.cachedTokens === "number") {
50183
+ usage.input_tokens_details = {
50184
+ ...usage.input_tokens_details ?? {},
50185
+ cached_tokens: opts.usage.cachedTokens
50186
+ };
50187
+ }
50188
+ resp = { ...resp, usage };
50189
+ }
50190
+ return buildCompleted2(resp);
50191
+ },
50192
+ emitError(message) {
50193
+ const resp = {
50194
+ id: `resp-error-${Date.now()}`,
50195
+ status: "failed",
50196
+ error: { code: "server_error", message }
50197
+ };
50198
+ return Buffer.from(
50199
+ `event: response.failed
50200
+ data: ${JSON.stringify({ type: "response.failed", response: resp })}
50201
+
50202
+ `,
50203
+ "utf8"
50204
+ );
50205
+ },
50206
+ extractTextTriggers(text) {
50207
+ const calls = [];
50208
+ let clean = text;
50209
+ let hadTrigger = false;
50210
+ const triggers = [
50211
+ { name: "compress", open: ACP_TEXT_OPEN, close: ACP_TEXT_CLOSE, requirePayload: true },
50212
+ { name: "acp_status", open: ACP_STATUS_OPEN, close: ACP_STATUS_CLOSE, requirePayload: false },
50213
+ { name: "search_context", open: ACP_SEARCH_OPEN, close: ACP_SEARCH_CLOSE, requirePayload: true },
50214
+ { name: "decompress", open: ACP_DECOMPRESS_OPEN, close: ACP_DECOMPRESS_CLOSE, requirePayload: true }
50215
+ ];
50216
+ for (const t of triggers) {
50217
+ let start = clean.indexOf(t.open);
50218
+ while (start >= 0) {
50219
+ const end = clean.indexOf(t.close, start + t.open.length);
50220
+ if (end < 0) break;
50221
+ hadTrigger = true;
50222
+ const payload = clean.slice(start + t.open.length, end).trim();
50223
+ if (payload.length > 0 || !t.requirePayload) {
50224
+ const stamp = `${Date.now()}-${calls.length}`;
50225
+ calls.push({
50226
+ name: t.name,
50227
+ callId: `call_text_${stamp}`,
50228
+ arguments: payload.length > 0 ? payload : "{}"
50229
+ });
50230
+ }
50231
+ clean = clean.slice(0, start) + clean.slice(end + t.close.length);
50232
+ start = clean.indexOf(t.open);
50233
+ }
50234
+ }
50235
+ return { clean: hadTrigger ? clean : text, calls };
50236
+ }
50237
+ };
50238
+ }
50239
+
50240
+ // src/loop/adapter-openai.ts
50241
+ async function* iterSseChunks(stream2) {
50242
+ const reader = stream2.getReader();
50243
+ let buf = "";
50244
+ try {
50245
+ while (true) {
50246
+ let done;
50247
+ let value;
50248
+ try {
50249
+ ({ done, value } = await reader.read());
50250
+ } catch {
50251
+ break;
50252
+ }
50253
+ if (done) break;
50254
+ buf += new TextDecoder().decode(value, { stream: true });
50255
+ buf = buf.replace(/\r\n|\r/g, "\n");
50256
+ let idx;
50257
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
50258
+ const raw = buf.slice(0, idx);
50259
+ buf = buf.slice(idx + 2);
50260
+ if (raw.trim().length > 0) yield raw;
50261
+ }
50262
+ }
50263
+ if (buf.trim().length > 0) yield buf;
50264
+ } finally {
50265
+ reader.releaseLock();
50266
+ }
50267
+ }
50268
+ function createOpenaiAdapter(requestBody) {
50269
+ const model = requestBody.model ?? "unknown";
50270
+ let responseId = `chatcmpl-proxy-${Date.now()}`;
50271
+ let toolIndex = 0;
50272
+ const makeBase = () => ({
50273
+ id: responseId,
50274
+ object: "chat.completion.chunk",
50275
+ created: Date.now(),
50276
+ model
50277
+ });
50278
+ const buildContent = (content) => Buffer.from(
50279
+ `data: ${JSON.stringify({
50280
+ id: responseId,
50281
+ object: "chat.completion.chunk",
50282
+ created: Date.now(),
50283
+ model,
50284
+ choices: [{ index: 0, delta: { content }, finish_reason: null }]
50285
+ })}
50286
+
50287
+ `,
50288
+ "utf8"
50289
+ );
50290
+ const buildToolCall = (call) => {
50291
+ const idx = toolIndex++;
50292
+ return Buffer.from(
50293
+ `data: ${JSON.stringify({
50294
+ ...makeBase(),
50295
+ choices: [{
50296
+ index: 0,
50297
+ delta: {
50298
+ tool_calls: [{
50299
+ index: idx,
50300
+ id: call.callId,
50301
+ type: "function",
50302
+ function: { name: call.name, arguments: call.arguments }
50303
+ }]
50304
+ },
50305
+ finish_reason: null
50306
+ }]
50307
+ })}
50308
+
50309
+ `,
50310
+ "utf8"
50311
+ );
50312
+ };
50313
+ const buildFinish = (finishReason, usage) => Buffer.from(
50314
+ `data: ${JSON.stringify({
50315
+ ...makeBase(),
50316
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
50317
+ ...usage ? { usage } : {}
50318
+ })}
50319
+
50320
+ `,
50321
+ "utf8"
50322
+ );
50323
+ return {
50324
+ buildRequest(coreMessages, systemPrompt, body) {
50325
+ const messages = coreToOpenai(coreMessages);
50326
+ const withSys = injectOpenaiSystem(messages, [systemPrompt]);
50327
+ return { ...body, messages: withSys };
50328
+ },
50329
+ async *parseStream(upstream, _round) {
50330
+ const pending = /* @__PURE__ */ new Map();
50331
+ for await (const eventStr of iterSseChunks(upstream)) {
50332
+ const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
50333
+ if (!dataLine) continue;
50334
+ const jsonStr = dataLine.slice(5).trim();
50335
+ if (jsonStr === "[DONE]") {
50336
+ for (const [, tc] of pending) {
50337
+ if (tc.name.length > 0 || tc.id.length > 0) {
50338
+ yield {
50339
+ kind: "tool_call",
50340
+ name: tc.name,
50341
+ callId: tc.id,
50342
+ arguments: tc.arguments
50343
+ };
50344
+ }
50345
+ }
50346
+ pending.clear();
50347
+ yield { kind: "done", finishReason: "stop" };
50348
+ continue;
50349
+ }
50350
+ let parsed;
50351
+ try {
50352
+ parsed = JSON.parse(jsonStr);
50353
+ } catch {
50354
+ continue;
50355
+ }
50356
+ const rawBuf = Buffer.from(eventStr + "\n\n", "utf8");
50357
+ const choices = parsed.choices;
50358
+ const choice = choices?.[0];
50359
+ if (!choice) {
50360
+ if (parsed.usage) {
50361
+ const u2 = parsed.usage;
50362
+ const pd = u2.prompt_tokens_details;
50363
+ yield {
50364
+ kind: "usage",
50365
+ inputTokens: typeof u2.prompt_tokens === "number" ? u2.prompt_tokens : void 0,
50366
+ outputTokens: typeof u2.completion_tokens === "number" ? u2.completion_tokens : void 0,
50367
+ cachedTokens: typeof pd?.cached_tokens === "number" ? pd.cached_tokens : void 0
50368
+ };
50369
+ }
50370
+ continue;
50371
+ }
50372
+ const delta = choice.delta;
50373
+ const finishReason = typeof choice.finish_reason === "string" ? choice.finish_reason : void 0;
50374
+ if (finishReason) {
50375
+ for (const [, tc] of pending) {
50376
+ if (tc.name.length > 0 || tc.id.length > 0) {
50377
+ yield {
50378
+ kind: "tool_call",
50379
+ name: tc.name,
50380
+ callId: tc.id,
50381
+ arguments: tc.arguments
50382
+ };
50383
+ }
50384
+ }
50385
+ pending.clear();
50386
+ const u2 = parsed.usage;
50387
+ const pd = u2?.prompt_tokens_details;
50388
+ yield {
50389
+ kind: "usage",
50390
+ inputTokens: typeof u2?.prompt_tokens === "number" ? u2.prompt_tokens : void 0,
50391
+ outputTokens: typeof u2?.completion_tokens === "number" ? u2.completion_tokens : void 0,
50392
+ cachedTokens: typeof pd?.cached_tokens === "number" ? pd.cached_tokens : void 0
50393
+ };
50394
+ yield { kind: "done", finishReason };
50395
+ }
50396
+ if (!delta) continue;
50397
+ if (delta.tool_calls) {
50398
+ const tcs = delta.tool_calls;
50399
+ for (const tc of tcs) {
50400
+ const idx = typeof tc.index === "number" ? tc.index : 0;
50401
+ const fn = tc.function;
50402
+ const name = typeof fn?.name === "string" ? fn.name : "";
50403
+ const id = typeof tc.id === "string" ? tc.id : "";
50404
+ const args = typeof fn?.arguments === "string" ? fn.arguments : "";
50405
+ let buf = pending.get(idx);
50406
+ if (!buf) {
50407
+ buf = { index: idx, id, name, arguments: args };
50408
+ pending.set(idx, buf);
50409
+ } else {
50410
+ if (id) buf.id = id;
50411
+ if (name) buf.name = name;
50412
+ buf.arguments += args;
50413
+ }
50414
+ }
50415
+ if (typeof delta.content === "string" && delta.content.length > 0) {
50416
+ yield { kind: "text", delta: delta.content };
50417
+ }
50418
+ continue;
50419
+ }
50420
+ if (typeof delta.content === "string" && delta.content.length > 0) {
50421
+ yield { kind: "text", delta: delta.content, raw: rawBuf };
50422
+ } else if (delta.role || Object.keys(delta).length === 0 && !finishReason) {
50423
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50424
+ }
50425
+ }
50426
+ },
50427
+ emitText(delta) {
50428
+ return buildContent(delta);
50429
+ },
50430
+ emitToolCall(call) {
50431
+ return buildToolCall(call);
50432
+ },
50433
+ emitMarker(toolName, result) {
50434
+ return buildContent(buildVisibilityMarker(toolName, result));
50435
+ },
50436
+ emitCompletion(opts) {
50437
+ const finishReason = opts?.finishReason ?? "stop";
50438
+ const usage = opts?.usage ? {
50439
+ prompt_tokens: opts.usage.inputTokens,
50440
+ completion_tokens: opts.usage.outputTokens,
50441
+ ...typeof opts.usage.cachedTokens === "number" ? { prompt_tokens_details: { cached_tokens: opts.usage.cachedTokens } } : {}
50442
+ } : null;
50443
+ return Buffer.concat([buildFinish(finishReason, usage), Buffer.from("data: [DONE]\n\n", "utf8")]);
50444
+ },
50445
+ emitError(message) {
50446
+ return Buffer.concat([
50447
+ buildContent(`
50448
+ [acp-proxy: ${message}]
50449
+ `),
50450
+ buildFinish("stop", null),
50451
+ Buffer.from("data: [DONE]\n\n", "utf8")
50452
+ ]);
50453
+ }
50454
+ };
50455
+ }
50456
+
50457
+ // src/loop/adapter-anthropic.ts
50458
+ async function* iterSseEvents2(stream2) {
50459
+ const reader = stream2.getReader();
50460
+ let buf = "";
50461
+ try {
50462
+ while (true) {
50463
+ let done;
50464
+ let value;
50465
+ try {
50466
+ ({ done, value } = await reader.read());
50467
+ } catch {
50468
+ break;
50469
+ }
50470
+ if (done) break;
50471
+ buf += new TextDecoder().decode(value, { stream: true });
50472
+ buf = buf.replace(/\r\n|\r/g, "\n");
50473
+ let idx;
50474
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
50475
+ const raw = buf.slice(0, idx);
50476
+ buf = buf.slice(idx + 2);
50477
+ if (raw.trim().length > 0) yield raw;
50478
+ }
50479
+ }
50480
+ if (buf.trim().length > 0) yield buf;
50481
+ } finally {
50482
+ reader.releaseLock();
50483
+ }
50484
+ }
50485
+ function parseAnthropicSse2(eventStr) {
50486
+ const lines = eventStr.split("\n");
50487
+ let type = "";
50488
+ const dataLines = [];
50489
+ for (const l of lines) {
50490
+ if (l.startsWith("event:")) type = l.slice(6).trim();
50491
+ else if (l.startsWith("data:")) dataLines.push(l.slice(5).replace(/^ /, ""));
50492
+ }
50493
+ if (!type) return null;
50494
+ const jsonStr = dataLines.join("\n").trim();
50495
+ if (!jsonStr) return { type, data: {} };
50496
+ try {
50497
+ return { type, data: JSON.parse(jsonStr) };
50498
+ } catch {
50499
+ return { type, data: {} };
50500
+ }
50501
+ }
50502
+ function remapIndexInEvent(eventStr, newIndex) {
50503
+ const lines = eventStr.split("\n");
50504
+ const rebuilt = [];
50505
+ let touched = false;
50506
+ for (const l of lines) {
50507
+ if (!touched && l.startsWith("data:")) {
50508
+ const jsonStr = l.slice(5).replace(/^ /, "");
50509
+ try {
50510
+ const obj = JSON.parse(jsonStr);
50511
+ if (typeof obj === "object" && obj !== null && typeof obj.index === "number") {
50512
+ obj.index = newIndex;
50513
+ rebuilt.push(`data: ${JSON.stringify(obj)}`);
50514
+ touched = true;
50515
+ continue;
50516
+ }
50517
+ } catch {
50518
+ }
50519
+ }
50520
+ rebuilt.push(l);
50521
+ }
50522
+ return Buffer.from(rebuilt.join("\n") + "\n\n", "utf8");
50523
+ }
50524
+ function createAnthropicAdapter(requestBody, originalSystem) {
50525
+ const model = requestBody.model ?? void 0;
50526
+ let messageId;
50527
+ let clientIndex = 0;
50528
+ const buildTextBlock = (index, text) => Buffer.from(
50529
+ `event: content_block_start
50530
+ data: ${JSON.stringify({ type: "content_block_start", index, content_block: { type: "text", text: "" } })}
50531
+
50532
+ event: content_block_delta
50533
+ data: ${JSON.stringify({ type: "content_block_delta", index, delta: { type: "text_delta", text } })}
50534
+
50535
+ event: content_block_stop
50536
+ data: ${JSON.stringify({ type: "content_block_stop", index })}
50537
+
50538
+ `,
50539
+ "utf8"
50540
+ );
50541
+ const buildToolUseBlock = (index, call) => Buffer.from(
50542
+ `event: content_block_start
50543
+ data: ${JSON.stringify({ type: "content_block_start", index, content_block: { type: "tool_use", id: call.callId, name: call.name, input: {} } })}
50544
+
50545
+ event: content_block_delta
50546
+ data: ${JSON.stringify({ type: "content_block_delta", index, delta: { type: "input_json_delta", partial_json: call.arguments } })}
50547
+
50548
+ event: content_block_stop
50549
+ data: ${JSON.stringify({ type: "content_block_stop", index })}
50550
+
50551
+ `,
50552
+ "utf8"
50553
+ );
50554
+ const buildTerminal = (stopReason, outputTokens, inputTokens, cachedTokens) => {
50555
+ const usage = {
50556
+ input_tokens: inputTokens,
50557
+ output_tokens: outputTokens,
50558
+ cache_read_input_tokens: cachedTokens
50559
+ };
50560
+ const extra = {};
50561
+ if (messageId) extra.id = messageId;
50562
+ if (model) extra.model = model;
50563
+ return Buffer.from(
50564
+ `event: message_delta
50565
+ data: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage, ...extra })}
50566
+
50567
+ event: message_stop
50568
+ data: ${JSON.stringify({ type: "message_stop" })}
50569
+
50570
+ `,
50571
+ "utf8"
50572
+ );
50573
+ };
50574
+ return {
50575
+ buildRequest(coreMessages, systemPrompt, body) {
50576
+ const messages = coreToAnthropic(coreMessages);
50577
+ const baseText = originalSystem !== void 0 ? extractSystem(originalSystem) : "";
50578
+ const full = baseText ? `${baseText}
50579
+
50580
+ ---
50581
+
50582
+ ${systemPrompt}` : systemPrompt;
50583
+ const system = originalSystem !== void 0 ? buildSystem(full, originalSystem) : full;
50584
+ return { ...body, system, messages };
50585
+ },
50586
+ async *parseStream(upstream, round) {
50587
+ const pending = /* @__PURE__ */ new Map();
50588
+ let roundInput;
50589
+ let roundCached;
50590
+ let roundOutput;
50591
+ let stopReason;
50592
+ let usageYielded = false;
50593
+ const indexMap = /* @__PURE__ */ new Map();
50594
+ for await (const eventStr of iterSseEvents2(upstream)) {
50595
+ const parsed = parseAnthropicSse2(eventStr);
50596
+ if (!parsed) continue;
50597
+ const { type, data } = parsed;
50598
+ const rawBuf = Buffer.from(eventStr + "\n\n", "utf8");
50599
+ if (type === "message_start") {
50600
+ const msg2 = data.message ?? {};
50601
+ if (typeof msg2.id === "string" && !messageId) messageId = msg2.id;
50602
+ const u2 = msg2.usage ?? {};
50603
+ if (typeof u2.input_tokens === "number") roundInput = u2.input_tokens;
50604
+ if (typeof u2.cache_read_input_tokens === "number") roundCached = u2.cache_read_input_tokens;
50605
+ if (round === 1) {
50606
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50607
+ }
50608
+ } else if (type === "ping") {
50609
+ yield { kind: "meta", chunk: rawBuf };
50610
+ } else if (type === "content_block_start") {
50611
+ const upstreamIndex = data.index ?? 0;
50612
+ const block = data.content_block ?? {};
50613
+ if (block.type === "tool_use") {
50614
+ const name = typeof block.name === "string" ? block.name : "";
50615
+ const id = typeof block.id === "string" ? block.id : `toolu_${upstreamIndex}`;
50616
+ pending.set(upstreamIndex, { id, name, json: "" });
50617
+ } else if (round === 1) {
50618
+ const ci2 = clientIndex++;
50619
+ indexMap.set(upstreamIndex, ci2);
50620
+ yield { kind: "meta", chunk: remapIndexInEvent(eventStr, ci2), firstRoundOnly: true };
50621
+ }
50622
+ } else if (type === "content_block_delta") {
50623
+ const upstreamIndex = data.index ?? 0;
50624
+ const delta = data.delta ?? {};
50625
+ if (pending.has(upstreamIndex)) {
50626
+ if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
50627
+ pending.get(upstreamIndex).json += delta.partial_json;
50628
+ }
50629
+ } else if (delta.type === "text_delta" && typeof delta.text === "string") {
50630
+ if (round === 1) {
50631
+ const ci2 = indexMap.get(upstreamIndex) ?? upstreamIndex;
50632
+ yield { kind: "text", delta: delta.text, raw: remapIndexInEvent(eventStr, ci2) };
50633
+ } else {
50634
+ yield { kind: "text", delta: delta.text };
50635
+ }
50636
+ } else if (round === 1) {
50637
+ const ci2 = indexMap.get(upstreamIndex) ?? upstreamIndex;
50638
+ yield { kind: "meta", chunk: remapIndexInEvent(eventStr, ci2), firstRoundOnly: true };
50639
+ }
50640
+ } else if (type === "content_block_stop") {
50641
+ const upstreamIndex = data.index ?? 0;
50642
+ const tb = pending.get(upstreamIndex);
50643
+ if (tb) {
50644
+ pending.delete(upstreamIndex);
50645
+ yield {
50646
+ kind: "tool_call",
50647
+ name: tb.name,
50648
+ callId: tb.id,
50649
+ arguments: tb.json
50650
+ };
50651
+ } else if (round === 1) {
50652
+ const ci2 = indexMap.get(upstreamIndex) ?? upstreamIndex;
50653
+ yield { kind: "meta", chunk: remapIndexInEvent(eventStr, ci2), firstRoundOnly: true };
50654
+ }
50655
+ } else if (type === "message_delta") {
50656
+ const u2 = data.usage ?? {};
50657
+ if (typeof u2.output_tokens === "number") roundOutput = u2.output_tokens;
50658
+ if (typeof u2.input_tokens === "number") roundInput = u2.input_tokens;
50659
+ if (typeof u2.cache_read_input_tokens === "number") roundCached = u2.cache_read_input_tokens;
50660
+ const d = data.delta ?? {};
50661
+ if (typeof d.stop_reason === "string") stopReason = d.stop_reason;
50662
+ if (!usageYielded) {
50663
+ usageYielded = true;
50664
+ yield {
50665
+ kind: "usage",
50666
+ inputTokens: roundInput,
50667
+ outputTokens: roundOutput,
50668
+ cachedTokens: roundCached
50669
+ };
50670
+ }
50671
+ yield { kind: "done", finishReason: stopReason };
50672
+ } else if (type === "message_stop") {
50673
+ if (!usageYielded) {
50674
+ usageYielded = true;
50675
+ yield {
50676
+ kind: "usage",
50677
+ inputTokens: roundInput,
50678
+ outputTokens: roundOutput,
50679
+ cachedTokens: roundCached
50680
+ };
50681
+ }
50682
+ yield { kind: "done", finishReason: stopReason ?? "end_turn" };
50683
+ } else if (round === 1) {
50684
+ yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
50685
+ }
50686
+ }
50687
+ },
50688
+ emitText(delta) {
50689
+ return buildTextBlock(clientIndex++, delta);
50690
+ },
50691
+ emitToolCall(call) {
50692
+ return buildToolUseBlock(clientIndex++, call);
50693
+ },
50694
+ emitMarker(toolName, result) {
50695
+ return buildTextBlock(clientIndex++, buildVisibilityMarker(toolName, result));
50696
+ },
50697
+ emitCompletion(opts) {
50698
+ const stopReason = opts?.finishReason ?? "end_turn";
50699
+ return buildTerminal(
50700
+ stopReason,
50701
+ opts?.usage?.outputTokens ?? 0,
50702
+ opts?.usage?.inputTokens ?? 0,
50703
+ opts?.usage?.cachedTokens ?? 0
50704
+ );
50705
+ },
50706
+ emitError(message) {
50707
+ const errBlock = buildTextBlock(clientIndex++, `
50708
+ [acp-proxy: ${message}]
50709
+ `);
50710
+ return Buffer.concat([errBlock, buildTerminal("end_turn", 0, 0, 0)]);
50711
+ }
50712
+ };
50713
+ }
50714
+
50715
+ // src/loop/index.ts
50716
+ function pickAdapter(protocol, requestBody, textProtocol, responsesProjection, anthropicSystem) {
50717
+ if (protocol === "responses") return createResponsesAdapter(textProtocol, responsesProjection);
50718
+ if (protocol === "openai") return createOpenaiAdapter(requestBody);
50719
+ if (protocol === "anthropic") return createAnthropicAdapter(requestBody, anthropicSystem);
50720
+ throw new Error(`[acp-loop] unknown protocol: ${protocol}`);
50721
+ }
50722
+
49535
50723
  // src/stream-openai.ts
49536
50724
  function rewriteOpenaiJsonResponse(body, ctx) {
49537
50725
  if (!body || typeof body !== "object") return body;
@@ -49668,7 +50856,7 @@ function extractKey(headers) {
49668
50856
  return "(no-key)";
49669
50857
  }
49670
50858
  function clientConversationHeader(headers) {
49671
- const names = ["x-session-affinity", "x-acp-session", "x-session-id", "x-opencode-session", "session-id", "session_id"];
50859
+ const names = ["x-claude-code-session-id", "x-session-affinity", "x-acp-session", "x-session-id", "x-opencode-session", "session-id", "session_id"];
49672
50860
  for (const name of names) {
49673
50861
  const v2 = headers[name];
49674
50862
  if (typeof v2 === "string" && v2.trim().length > 0) return v2.trim();
@@ -50985,7 +52173,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
50985
52173
  }
50986
52174
  const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
50987
52175
  markDirty(session);
50988
- return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool };
52176
+ return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, anthropicSystem: parsed.system, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool };
50989
52177
  }
50990
52178
  function prepareOpenai(parsed, req, opts, core, config, log2, session) {
50991
52179
  const sessionId = session.id;
@@ -51049,12 +52237,14 @@ function prepareResponses(parsed, req, opts, core, config, log2, session, identi
51049
52237
  }
51050
52238
  let processedMessages = [];
51051
52239
  let originalMessages = [];
52240
+ let responsesProjection;
51052
52241
  let rebuiltInput = parsed.input;
51053
52242
  let toolsOut = parsed.tools;
51054
52243
  const shouldInject = opts.compress.injectTool;
51055
52244
  const responsesTextProtocol = FORCE_TEXT_PROTOCOL || isChatGptCodexUpstream(session.meta.upstreamOrigin) || isCodexResponsesLite(req.headers, parsed);
51056
52245
  try {
51057
52246
  const projection = responsesToCore(parsed);
52247
+ responsesProjection = projection;
51058
52248
  const { msgs } = projection;
51059
52249
  originalMessages = msgs;
51060
52250
  if (process.env.ACP_DEBUG) {
@@ -51120,6 +52310,7 @@ function prepareResponses(parsed, req, opts, core, config, log2, session, identi
51120
52310
  session,
51121
52311
  processedMessages,
51122
52312
  originalMessages,
52313
+ responsesProjection,
51123
52314
  protocol: "responses",
51124
52315
  stream: stream2,
51125
52316
  compressInjected: shouldInject,
@@ -51258,9 +52449,20 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
51258
52449
  return fn?.name ?? t.name ?? "?";
51259
52450
  });
51260
52451
  log2("info", `[debug] tools=[${toolNames.join(",")}] msgs=${parsed.messages?.length ?? 0} stream=${parsed.stream ?? false} system_len=${JSON.stringify(parsed.messages?.find((m2) => m2.role === "system")?.content ?? "").length}`);
51261
- if (process.env.ACP_DUMP_REQ === "1") {
51262
- const out = `${tmpdir2()}/acp-proxy-debug-req-${Date.now()}.json`;
51263
- fs3.writeFileSync(out, body.slice(0, 5e4));
52452
+ if (process.env.ACP_DUMP_REQ !== "0") {
52453
+ const dumpDir = process.env.ACP_DUMP_DIR || `${stateDir()}/dumps`;
52454
+ try {
52455
+ fs3.mkdirSync(dumpDir, { recursive: true });
52456
+ } catch {
52457
+ }
52458
+ const sid = prepared?.session.id ?? "unknown";
52459
+ const out = `${dumpDir}/req-${Date.now()}-${sid}.json`;
52460
+ try {
52461
+ const pretty = JSON.stringify(JSON.parse(body), null, 2);
52462
+ fs3.writeFileSync(out, pretty);
52463
+ } catch {
52464
+ fs3.writeFileSync(out, body);
52465
+ }
51264
52466
  log2("info", `[debug] forwarded body written to ${out}`);
51265
52467
  }
51266
52468
  } catch {
@@ -51276,6 +52478,13 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
51276
52478
  headers["x-session-id"] = affinity;
51277
52479
  }
51278
52480
  const proxyUrl = resolveProxy(opts.routes, opts.proxy, route?.rewrittenUrl ?? upstreamUrl, opts.proxyFallback);
52481
+ if (opts.debug) {
52482
+ const hdrLog = {};
52483
+ for (const [hk, hv] of Object.entries(headers)) {
52484
+ if (typeof hv === "string") hdrLog[hk] = hv.length > 200 ? hv.slice(0, 200) + "..." : hv;
52485
+ }
52486
+ log2("info", `[${prepared?.session.id ?? "unknown"}] \u2192 upstream headers: ${JSON.stringify(hdrLog)}`);
52487
+ }
51279
52488
  const dispatcher = proxyDispatcher(proxyUrl);
51280
52489
  const init = {
51281
52490
  method: req.method ?? "GET",
@@ -51297,6 +52506,14 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
51297
52506
  if (UPSTREAM_HOP_HEADERS.has(k2.toLowerCase())) return;
51298
52507
  respHeaders[k2] = v2;
51299
52508
  });
52509
+ if (opts.debug) {
52510
+ const respLog = {};
52511
+ upstream.headers.forEach((v2, k2) => {
52512
+ if (UPSTREAM_HOP_HEADERS.has(k2.toLowerCase())) return;
52513
+ respLog[k2] = v2.length > 300 ? v2.slice(0, 300) + "..." : v2;
52514
+ });
52515
+ log2("info", `[${prepared?.session.id ?? "unknown"}] \u2190 upstream response headers: ${JSON.stringify(respLog)}`);
52516
+ }
51300
52517
  if (!upstream.ok) {
51301
52518
  res.writeHead(upstream.status, respHeaders);
51302
52519
  if (upstream.body) await pipeThrough(upstream.body, res);
@@ -51340,7 +52557,37 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
51340
52557
  dumpRaw = dumpStreamToFile(b2, opts.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
51341
52558
  }
51342
52559
  try {
51343
- if (prepared.protocol === "openai") {
52560
+ if (process.env.ACP_LOOP_V2 !== "0") {
52561
+ const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52562
+ const reqHeaders = {};
52563
+ for (const [k2, v2] of Object.entries(headers)) {
52564
+ if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
52565
+ reqHeaders[k2] = v2;
52566
+ }
52567
+ reqHeaders["content-type"] = "application/json";
52568
+ const textProtocol = prepared.protocol === "responses" && !!prepared.responsesTextProtocol;
52569
+ const systemPrompt = textProtocol ? buildCompressTextSystemPrompt() : buildCompressSystemPrompt();
52570
+ const adapter = pickAdapter(prepared.protocol, parsedReq, textProtocol, prepared.responsesProjection, prepared.anthropicSystem);
52571
+ const loop = runCompressLoop(
52572
+ streamToRead,
52573
+ { core, config, messages: prepared.processedMessages.length > 0 ? prepared.processedMessages : prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl, textProtocol, debug: opts.debug },
52574
+ parsedReq,
52575
+ { url: upstreamUrl, headers: reqHeaders },
52576
+ adapter,
52577
+ systemPrompt
52578
+ );
52579
+ for await (const chunk of loop) {
52580
+ {
52581
+ const s3 = chunk.toString("utf8");
52582
+ if (s3.includes("<acp ") || s3.includes("</acp")) {
52583
+ log2("warn", `[${prepared.session.id}] tag echo: ${prepared.protocol} response stream contains <acp tag`);
52584
+ }
52585
+ }
52586
+ res.write(chunk);
52587
+ if (res.writableNeedDrain) await new Promise((r) => res.once("drain", () => r()));
52588
+ }
52589
+ res.end();
52590
+ } else if (prepared.protocol === "openai") {
51344
52591
  const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
51345
52592
  const reqHeaders = {};
51346
52593
  for (const [k2, v2] of Object.entries(headers)) {