@theokit/sdk 2.15.2 → 2.18.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/a2a/index.cjs +875 -198
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js +876 -199
  5. package/dist/a2a/index.js.map +1 -1
  6. package/dist/{cron-BxLSz1UH.d.cts → cron-Bbg0mBOv.d.ts} +33 -3
  7. package/dist/{cron-DcaoP7aW.d.ts → cron-ZLSKbDbB.d.cts} +33 -3
  8. package/dist/cron.cjs +840 -187
  9. package/dist/cron.cjs.map +1 -1
  10. package/dist/cron.d.cts +2 -2
  11. package/dist/cron.d.ts +2 -2
  12. package/dist/cron.js +840 -187
  13. package/dist/cron.js.map +1 -1
  14. package/dist/define-tool.d.ts +9 -2
  15. package/dist/{errors-Bart0ptP.d.cts → errors-1tVcX3Fq.d.cts} +1 -1
  16. package/dist/{errors-DJuuubJK.d.ts → errors-qyVYfk9H.d.ts} +1 -1
  17. package/dist/errors.d.cts +2 -2
  18. package/dist/eval.cjs +846 -189
  19. package/dist/eval.cjs.map +1 -1
  20. package/dist/eval.js +846 -189
  21. package/dist/eval.js.map +1 -1
  22. package/dist/event-bus.d.ts +3 -0
  23. package/dist/index.cjs +977 -215
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +122 -27
  26. package/dist/index.d.ts +122 -27
  27. package/dist/index.js +977 -217
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/agent-loop/tool-dispatch.d.ts +3 -1
  30. package/dist/internal/agent-loop/tool-result-guard.d.ts +24 -0
  31. package/dist/internal/agent-loop/tool-timeout.d.ts +23 -0
  32. package/dist/internal/llm/openai.d.ts +4 -0
  33. package/dist/internal/llm/sse.d.ts +13 -1
  34. package/dist/internal/mcp/client.d.ts +1 -1
  35. package/dist/internal/memory/active-memory.d.ts +1 -1
  36. package/dist/internal/persistence/conversation-storage-fs.d.cts +7 -1
  37. package/dist/internal/persistence/conversation-storage-fs.d.ts +7 -1
  38. package/dist/internal/persistence/conversation-storage-memory.d.cts +7 -1
  39. package/dist/internal/persistence/conversation-storage-memory.d.ts +7 -1
  40. package/dist/internal/persistence/pagination.d.cts +8 -0
  41. package/dist/internal/persistence/pagination.d.ts +8 -0
  42. package/dist/internal/plugins/index.cjs +135 -0
  43. package/dist/internal/plugins/index.cjs.map +1 -1
  44. package/dist/internal/plugins/index.js +135 -0
  45. package/dist/internal/plugins/index.js.map +1 -1
  46. package/dist/internal/plugins/manager.d.cts +21 -1
  47. package/dist/internal/plugins/manager.d.ts +21 -1
  48. package/dist/internal/plugins/types.d.cts +40 -0
  49. package/dist/internal/plugins/types.d.ts +40 -0
  50. package/dist/internal/{memory → resilience}/circuit-breaker.d.ts +5 -1
  51. package/dist/internal/runtime/hooks/hooks-frontmatter.d.ts +1 -1
  52. package/dist/internal/runtime/lifecycle/env-policy.d.ts +30 -0
  53. package/dist/internal/runtime/session/agent-session-store.d.ts +1 -0
  54. package/dist/internal/telemetry/span-names.d.ts +7 -1
  55. package/dist/job-queue.d.ts +29 -7
  56. package/dist/permission-engine.d.ts +32 -7
  57. package/dist/{run-DXy_MVwz.d.cts → run-pE-34AAo.d.cts} +64 -3
  58. package/dist/{run-DXy_MVwz.d.ts → run-pE-34AAo.d.ts} +64 -3
  59. package/dist/sandbox/index.cjs +53 -2
  60. package/dist/sandbox/index.cjs.map +1 -1
  61. package/dist/sandbox/index.js +53 -2
  62. package/dist/sandbox/index.js.map +1 -1
  63. package/dist/sandbox/local-sandbox.d.cts +11 -3
  64. package/dist/sandbox/local-sandbox.d.ts +11 -3
  65. package/dist/sandbox/types.d.cts +7 -0
  66. package/dist/sandbox/types.d.ts +7 -0
  67. package/dist/types/agent-prims.d.ts +6 -2
  68. package/dist/types/conversation-storage.d.ts +32 -2
  69. package/dist/types/mcp.d.ts +20 -0
  70. package/dist/types/run.d.ts +17 -0
  71. package/dist/workflow.cjs +6 -3
  72. package/dist/workflow.cjs.map +1 -1
  73. package/dist/workflow.js +6 -3
  74. package/dist/workflow.js.map +1 -1
  75. package/package.json +1 -1
package/dist/eval.cjs CHANGED
@@ -888,6 +888,16 @@ var init_credential_pool_types = __esm({
888
888
  });
889
889
 
890
890
  // src/internal/llm/retry.ts
891
+ function computeBackoffMs(opts) {
892
+ const base = opts.baseMs ?? DEFAULT_BASE_MS;
893
+ const cap = opts.capMs ?? DEFAULT_CAP_MS;
894
+ const rng = opts.rng ?? Math.random;
895
+ if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
896
+ return Math.max(base, Math.min(cap, opts.retryAfterMs));
897
+ }
898
+ const ceiling = Math.min(cap, base * 2 ** opts.attempt);
899
+ return Math.floor(rng() * ceiling);
900
+ }
891
901
  function sleepWithAbort(ms, signal) {
892
902
  if (ms <= 0 || signal.aborted) return Promise.resolve();
893
903
  return new Promise((resolve3) => {
@@ -902,8 +912,11 @@ function sleepWithAbort(ms, signal) {
902
912
  signal.addEventListener("abort", onAbort, { once: true });
903
913
  });
904
914
  }
915
+ var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
905
916
  var init_retry = __esm({
906
917
  "src/internal/llm/retry.ts"() {
918
+ DEFAULT_BASE_MS = 500;
919
+ DEFAULT_CAP_MS = 32e3;
907
920
  }
908
921
  });
909
922
 
@@ -4564,13 +4577,15 @@ function buildGitInfo() {
4564
4577
  init_errors();
4565
4578
 
4566
4579
  // src/internal/llm/sse.ts
4567
- async function* parseSseStream(body, signal) {
4580
+ init_errors();
4581
+ var DEFAULT_SSE_IDLE_MS = 6e4;
4582
+ async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
4568
4583
  if (body === null) return;
4569
4584
  const reader = body.getReader();
4570
4585
  const decoder = new TextDecoder("utf-8");
4571
4586
  const state2 = { buffer: "", event: "message", data: "" };
4572
4587
  try {
4573
- for await (const chunk of readChunks(reader, signal)) {
4588
+ for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
4574
4589
  state2.buffer += decoder.decode(chunk, { stream: true });
4575
4590
  for (const record of drainCompleteRecords(state2)) yield record;
4576
4591
  }
@@ -4580,14 +4595,38 @@ async function* parseSseStream(body, signal) {
4580
4595
  releaseReader(reader);
4581
4596
  }
4582
4597
  }
4583
- async function* readChunks(reader, signal) {
4598
+ async function* readChunks(reader, signal, idleTimeoutMs) {
4584
4599
  while (true) {
4585
4600
  if (signal.aborted) return;
4586
- const { value, done } = await reader.read();
4601
+ const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
4587
4602
  if (done) return;
4588
4603
  if (value !== void 0) yield value;
4589
4604
  }
4590
4605
  }
4606
+ function readWithIdleTimeout(reader, idleTimeoutMs) {
4607
+ if (idleTimeoutMs <= 0) return reader.read();
4608
+ return new Promise(
4609
+ (resolve3, reject) => {
4610
+ const timer = setTimeout(() => {
4611
+ reject(
4612
+ new NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
4613
+ code: "stream_idle_timeout"
4614
+ })
4615
+ );
4616
+ }, idleTimeoutMs);
4617
+ reader.read().then(
4618
+ (result) => {
4619
+ clearTimeout(timer);
4620
+ resolve3(result);
4621
+ },
4622
+ (err) => {
4623
+ clearTimeout(timer);
4624
+ reject(err);
4625
+ }
4626
+ );
4627
+ }
4628
+ );
4629
+ }
4591
4630
  async function cancelReaderQuietly(reader) {
4592
4631
  try {
4593
4632
  await reader.cancel();
@@ -5281,6 +5320,9 @@ var PersonalityStore = class {
5281
5320
  }
5282
5321
  };
5283
5322
 
5323
+ // src/internal/plugins/manager.ts
5324
+ init_errors();
5325
+
5284
5326
  // src/internal/plugins/context.ts
5285
5327
  function createPluginContext() {
5286
5328
  const registrations = {
@@ -5342,6 +5384,9 @@ var PluginManager = class {
5342
5384
  memoryProviders: []
5343
5385
  };
5344
5386
  #initialized = false;
5387
+ // #68 — registrations of plugins added post-init via `register()`, keyed by
5388
+ // plugin name so a re-register REPLACES (not appends) the prior hooks.
5389
+ #byName = /* @__PURE__ */ new Map();
5345
5390
  async initialize(plugins) {
5346
5391
  if (this.#initialized) {
5347
5392
  throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
@@ -5359,6 +5404,36 @@ var PluginManager = class {
5359
5404
  await this.#dispatchPlugin(plugin);
5360
5405
  }
5361
5406
  }
5407
+ /**
5408
+ * #68 — register a single `general` plugin AFTER `initialize()` has run.
5409
+ *
5410
+ * The bulk `initialize()` is single-shot (one call per process); late
5411
+ * registration is a distinct, named operation used by adapters that install
5412
+ * a plugin per-session/per-request (e.g. the ACP permission veto, which is
5413
+ * installed once the permission mode + connection are known — after the
5414
+ * agent's own plugins were already initialized).
5415
+ *
5416
+ * Idempotent by plugin NAME: re-registering a plugin with the same name
5417
+ * REPLACES its prior hooks/tools instead of appending duplicates (the ACP
5418
+ * permission plugin is re-installed on every prompt).
5419
+ *
5420
+ * Only `general` plugins may be registered late — model-provider / memory
5421
+ * plugins are resolved during the bulk init and cannot be added afterwards.
5422
+ */
5423
+ async register(plugin) {
5424
+ if (plugin.kind !== "general") {
5425
+ throw new ConfigurationError(
5426
+ `late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
5427
+ { code: "plugin_late_register_kind" }
5428
+ );
5429
+ }
5430
+ const prior = this.#byName.get(plugin.name);
5431
+ if (prior !== void 0) this.#unmerge(prior);
5432
+ const { ctx, registrations } = createPluginContext();
5433
+ await plugin.register(ctx);
5434
+ this.#byName.set(plugin.name, registrations);
5435
+ this.#merge(registrations);
5436
+ }
5362
5437
  get aggregated() {
5363
5438
  return this.#aggregated;
5364
5439
  }
@@ -5439,6 +5514,64 @@ var PluginManager = class {
5439
5514
  }
5440
5515
  }
5441
5516
  }
5517
+ // #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
5518
+ // in order (per-handler errors logged, never thrown); transform hooks fold
5519
+ // over the payload (a handler returning a value replaces it).
5520
+ /** @internal */
5521
+ async #runFireAndForget(name, ctx) {
5522
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
5523
+ try {
5524
+ await h(ctx);
5525
+ } catch (err) {
5526
+ process.stderr.write(
5527
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
5528
+ `
5529
+ );
5530
+ }
5531
+ }
5532
+ }
5533
+ /** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
5534
+ async #runTransform(name, payload, ctx) {
5535
+ let current = payload;
5536
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
5537
+ try {
5538
+ const out = await h(current, ctx);
5539
+ if (out !== void 0) current = out;
5540
+ } catch (err) {
5541
+ process.stderr.write(
5542
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
5543
+ `
5544
+ );
5545
+ }
5546
+ }
5547
+ return current;
5548
+ }
5549
+ /** #65 — fired after a tool call completes. @internal */
5550
+ runPostToolCallHooks(ctx) {
5551
+ return this.#runFireAndForget("post_tool_call", ctx);
5552
+ }
5553
+ /** #65 — fired before / after each LLM turn. @internal */
5554
+ runPreLlmCallHooks(ctx) {
5555
+ return this.#runFireAndForget("pre_llm_call", ctx);
5556
+ }
5557
+ runPostLlmCallHooks(ctx) {
5558
+ return this.#runFireAndForget("post_llm_call", ctx);
5559
+ }
5560
+ /** #65 — fired at run start / end. @internal */
5561
+ runOnSessionStartHooks(ctx) {
5562
+ return this.#runFireAndForget("on_session_start", ctx);
5563
+ }
5564
+ runOnSessionEndHooks(ctx) {
5565
+ return this.#runFireAndForget("on_session_end", ctx);
5566
+ }
5567
+ /** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
5568
+ runTransformToolResultHooks(results, ctx) {
5569
+ return this.#runTransform("transform_tool_result", results, ctx);
5570
+ }
5571
+ /** #65 — transform the LLM output text before it is consumed. @internal */
5572
+ runTransformLlmOutputHooks(output, ctx) {
5573
+ return this.#runTransform("transform_llm_output", output, ctx);
5574
+ }
5442
5575
  async #dispatchPlugin(plugin) {
5443
5576
  if (plugin.kind === "general") {
5444
5577
  const { ctx, registrations } = createPluginContext();
@@ -5466,7 +5599,29 @@ var PluginManager = class {
5466
5599
  }
5467
5600
  this.#aggregated.injected.push(...r.injected);
5468
5601
  }
5602
+ /**
5603
+ * #68 — inverse of #merge: remove a prior registration's contributions from
5604
+ * the aggregated view by object identity. Used by `register()` to replace a
5605
+ * same-named plugin's hooks/tools instead of accumulating duplicates.
5606
+ */
5607
+ #unmerge(r) {
5608
+ removeAll(this.#aggregated.tools, r.tools);
5609
+ removeAll(this.#aggregated.commands, r.commands);
5610
+ removeAll(this.#aggregated.injected, r.injected);
5611
+ for (const [hook, handlers] of r.hooks.entries()) {
5612
+ const existing = this.#aggregated.hooks.get(hook);
5613
+ if (existing === void 0) continue;
5614
+ removeAll(existing, handlers);
5615
+ if (existing.length === 0) this.#aggregated.hooks.delete(hook);
5616
+ }
5617
+ }
5469
5618
  };
5619
+ function removeAll(arr, toRemove) {
5620
+ for (const item of toRemove) {
5621
+ const idx = arr.indexOf(item);
5622
+ if (idx !== -1) arr.splice(idx, 1);
5623
+ }
5624
+ }
5470
5625
 
5471
5626
  // src/internal/telemetry/span-names.ts
5472
5627
  var SPAN_NAMES = {
@@ -5474,7 +5629,12 @@ var SPAN_NAMES = {
5474
5629
  AGENT_SEND: "agent.send",
5475
5630
  MEMORY_RECALL: "memory.recall"};
5476
5631
  var HISTOGRAM_NAMES = {
5477
- MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
5632
+ MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
5633
+ TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
5634
+ LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
5635
+ LLM_TOKENS: "theokit_llm_tokens",
5636
+ /** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
5637
+ LLM_USAGE_MISSING: "theokit_llm_usage_missing"
5478
5638
  };
5479
5639
  function safeRequire(moduleName) {
5480
5640
  try {
@@ -5809,7 +5969,25 @@ function createTelemetry(settings) {
5809
5969
  enabled: true,
5810
5970
  includeContent: settings.includeContent === true,
5811
5971
  startSpan: startNewSpan,
5812
- startChildSpan: (_parent, name, attrs) => startNewSpan(name, attrs),
5972
+ // M3 #64 actually nest the child under its parent instead of discarding it.
5973
+ // The parent's SpanContext is set on a fresh OTel context so the child links
5974
+ // to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
5975
+ // back to a root span when the parent has no valid span id (telemetry off /
5976
+ // NOOP), preserving the pre-M3 behavior for parentless callers.
5977
+ startChildSpan: (parent, name, attrs) => {
5978
+ const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
5979
+ const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
5980
+ const pctx = safe(() => parent?.spanContext(), void 0);
5981
+ const span = safe(() => {
5982
+ if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
5983
+ const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
5984
+ return tracer.startSpan(name, opts, childCtx);
5985
+ }
5986
+ return tracer.startSpan(name, opts);
5987
+ }, NOOP_SPAN);
5988
+ if (span !== NOOP_SPAN) openSpans.add(span);
5989
+ return wrapSpan(span, openSpans);
5990
+ },
5813
5991
  recordHistogram,
5814
5992
  endAll: () => {
5815
5993
  for (const span of openSpans) safe(() => span.end(), void 0);
@@ -5845,12 +6023,62 @@ function redactAttrs(attrs) {
5845
6023
  }
5846
6024
  return out;
5847
6025
  }
6026
+
6027
+ // src/internal/runtime/lifecycle/env-policy.ts
6028
+ var SECRET_PATTERNS = [
6029
+ /KEY/i,
6030
+ /SECRET/i,
6031
+ /TOKEN/i,
6032
+ /PASSWORD/i,
6033
+ /PASSWD/i,
6034
+ /PASSPHRASE/i,
6035
+ /[_-]PWD/i,
6036
+ /CREDENTIAL/i,
6037
+ /PRIVATE/i,
6038
+ /_AUTH/i
6039
+ ];
6040
+ var CORE_VARS = [
6041
+ "PATH",
6042
+ "HOME",
6043
+ "SHELL",
6044
+ "LANG",
6045
+ "LC_ALL",
6046
+ "LC_CTYPE",
6047
+ "TMPDIR",
6048
+ "TMP",
6049
+ "TEMP",
6050
+ "USER",
6051
+ "LOGNAME"
6052
+ ];
6053
+ function isSecretName(name) {
6054
+ return SECRET_PATTERNS.some((re) => re.test(name));
6055
+ }
6056
+ function inheritsUnderPolicy(name, policy) {
6057
+ if (policy === "all") return true;
6058
+ if (policy === "core") return CORE_VARS.includes(name);
6059
+ return !isSecretName(name);
6060
+ }
6061
+ function resolveChildEnv(options = {}) {
6062
+ const parent = options.parent ?? process.env;
6063
+ const policy = options.policy ?? "inherit-scrubbed";
6064
+ const base = {};
6065
+ for (const [name, value] of Object.entries(parent)) {
6066
+ if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
6067
+ }
6068
+ for (const [name, value] of Object.entries(options.overrides ?? {})) {
6069
+ base[name] = value;
6070
+ }
6071
+ return base;
6072
+ }
6073
+
6074
+ // src/internal/runtime/lifecycle/spawn-collect.ts
5848
6075
  function spawnAndCollect(options) {
5849
6076
  return new Promise((resolve3) => {
5850
6077
  const timeoutMs = options.timeoutMs ?? 3e4;
5851
6078
  const spawnOptions = {
5852
6079
  cwd: options.cwd,
5853
- env: { ...process.env, ...options.env ?? {} }
6080
+ // #54 — scrub secret-like parent env by default; `options.env` still wins.
6081
+ env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
5854
6082
  };
5855
6083
  const child = child_process.spawn(options.command, options.args ?? [], spawnOptions);
5856
6084
  let stdout = "";
@@ -6068,15 +6296,24 @@ function warnMalformed(agentId, line) {
6068
6296
  `
6069
6297
  );
6070
6298
  }
6299
+ function hydrateSessionLine(parsed) {
6300
+ if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
6301
+ if (parsed.role === "user" || parsed.role === "assistant") {
6302
+ return { role: parsed.role, text: parsed.text };
6303
+ }
6304
+ if (parsed.role === "tool_call" || parsed.role === "tool_result") {
6305
+ const label = parsed.role === "tool_call" ? "tool call" : "tool result";
6306
+ return { role: "assistant", text: `[${label}] ${parsed.text}` };
6307
+ }
6308
+ return void 0;
6309
+ }
6071
6310
  async function readSessionFile(cwd, agentId) {
6072
6311
  const lines = await readJsonlLines(cwd, agentId);
6073
6312
  const messages = [];
6074
6313
  for (const line of lines) {
6075
6314
  try {
6076
- const parsed = JSON.parse(line);
6077
- if ((parsed.role === "user" || parsed.role === "assistant") && typeof parsed.text === "string") {
6078
- messages.push({ role: parsed.role, text: parsed.text });
6079
- }
6315
+ const msg = hydrateSessionLine(JSON.parse(line));
6316
+ if (msg !== void 0) messages.push(msg);
6080
6317
  } catch {
6081
6318
  warnMalformed(agentId, line);
6082
6319
  }
@@ -6103,24 +6340,72 @@ async function readAllPersistedMessages(cwd, agentId) {
6103
6340
  return messages;
6104
6341
  }
6105
6342
  async function appendAnyPersistedMessage(cwd, agentId, record) {
6343
+ await appendPersistedMessages(cwd, agentId, [record]);
6344
+ }
6345
+ async function appendPersistedMessages(cwd, agentId, records) {
6346
+ if (records.length === 0) return;
6106
6347
  const path$1 = sessionFilePath(cwd, agentId);
6107
- await promises.mkdir(path.dirname(path$1), { recursive: true });
6108
- await promises.appendFile(path$1, `${redactSecrets(JSON.stringify(record))}
6109
- `, "utf8");
6348
+ const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
6349
+ `).join("");
6350
+ const dir = path.dirname(path$1);
6351
+ let written = false;
6352
+ const attempt = async () => {
6353
+ await promises.mkdir(dir, { recursive: true });
6354
+ await withFileLock(path$1, async () => {
6355
+ await promises.appendFile(path$1, payload, "utf8");
6356
+ written = true;
6357
+ });
6358
+ };
6359
+ try {
6360
+ await attempt();
6361
+ } catch (cause) {
6362
+ if (written || cause.code !== "ENOENT") throw cause;
6363
+ await attempt();
6364
+ }
6365
+ }
6366
+ async function rewriteLockedSession(path, transform) {
6367
+ await withFileLock(path, async () => {
6368
+ let raw;
6369
+ try {
6370
+ raw = await promises.readFile(path, "utf8");
6371
+ } catch {
6372
+ return;
6373
+ }
6374
+ const lines = raw.split("\n").filter((line) => line.length > 0);
6375
+ const next = transform(lines);
6376
+ if (next === void 0) return;
6377
+ await replaceFileAtomic(path, next);
6378
+ });
6110
6379
  }
6111
6380
  async function compactSessionFile(cwd, agentId, maxTurns) {
6112
6381
  const path = sessionFilePath(cwd, agentId);
6113
- let raw;
6114
- try {
6115
- raw = await promises.readFile(path, "utf8");
6116
- } catch {
6117
- return;
6118
- }
6119
- const lines = raw.split("\n").filter((line) => line.length > 0);
6120
- if (lines.length <= maxTurns * 2) return;
6121
- const trimmed = `${lines.slice(-maxTurns).join("\n")}
6382
+ if (!fs.existsSync(path)) return;
6383
+ await rewriteLockedSession(
6384
+ path,
6385
+ (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
6386
+ `
6387
+ );
6388
+ }
6389
+ async function truncateSessionTo(cwd, agentId, keepCount) {
6390
+ const path = sessionFilePath(cwd, agentId);
6391
+ if (!fs.existsSync(path)) return 0;
6392
+ let kept = 0;
6393
+ await rewriteLockedSession(path, (lines) => {
6394
+ const keep = Math.max(0, Math.min(keepCount, lines.length));
6395
+ kept = keep;
6396
+ if (keep === lines.length) return void 0;
6397
+ return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
6122
6398
  `;
6123
- await replaceFileAtomic(path, trimmed);
6399
+ });
6400
+ return kept;
6401
+ }
6402
+
6403
+ // src/internal/persistence/pagination.ts
6404
+ function paginate(items, opts) {
6405
+ if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
6406
+ const start = Math.max(0, opts.offset ?? 0);
6407
+ const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
6408
+ return items.slice(start, end);
6124
6409
  }
6125
6410
 
6126
6411
  // src/internal/persistence/conversation-storage-fs.ts
@@ -6133,23 +6418,31 @@ var FileSystemConversationStorage = class {
6133
6418
  get root() {
6134
6419
  return this.#root;
6135
6420
  }
6136
- async getMessages(conversationId) {
6421
+ async getMessages(conversationId, opts) {
6137
6422
  const records = await readAllPersistedMessages(this.#root, conversationId);
6138
- return records.map(toStoredMessage);
6423
+ const all = records.map(toStoredMessage);
6424
+ return paginate(all, opts);
6139
6425
  }
6140
6426
  async appendMessage(conversationId, message) {
6141
- const record = {
6142
- role: message.role,
6143
- text: message.content,
6144
- at: message.at ?? Date.now()
6145
- };
6146
- await appendAnyPersistedMessage(this.#root, conversationId, record);
6427
+ await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
6428
+ }
6429
+ async appendMessages(conversationId, messages) {
6430
+ await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
6431
+ }
6432
+ async truncateConversation(conversationId, keepCount) {
6433
+ return truncateSessionTo(this.#root, conversationId, keepCount);
6147
6434
  }
6148
6435
  async deleteConversation(conversationId) {
6149
6436
  const safe3 = sanitizeIdentifier(conversationId, { maxLen: 128 });
6150
6437
  const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe3);
6151
6438
  await promises.rm(dirPath, { recursive: true, force: true });
6152
6439
  }
6440
+ async deleteScope(prefix) {
6441
+ const ids = await this.listConversationIds();
6442
+ const matching = ids.filter((id) => id.startsWith(prefix));
6443
+ for (const id of matching) await this.deleteConversation(id);
6444
+ return matching.length;
6445
+ }
6153
6446
  async listConversationIds(opts = {}) {
6154
6447
  const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
6155
6448
  let entries;
@@ -6175,6 +6468,9 @@ function toStoredMessage(record) {
6175
6468
  at: record.at
6176
6469
  };
6177
6470
  }
6471
+ function toRecord(message) {
6472
+ return { role: message.role, text: message.content, at: message.at ?? Date.now() };
6473
+ }
6178
6474
 
6179
6475
  // src/internal/runtime/session/agent-session.ts
6180
6476
  var DEFAULT_MAX_TURNS = 200;
@@ -6260,12 +6556,19 @@ async function readPersistedForCache(adapter, agentId) {
6260
6556
  const records = await adapter.getMessages(agentId);
6261
6557
  const out = [];
6262
6558
  for (const r of records) {
6263
- if (r.role === "user" || r.role === "assistant") {
6264
- out.push({ role: r.role, text: r.content });
6265
- }
6559
+ const folded = foldStoredToSession(r);
6560
+ if (folded !== void 0) out.push(folded);
6266
6561
  }
6267
6562
  return out;
6268
6563
  }
6564
+ function foldStoredToSession(r) {
6565
+ if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
6566
+ if (r.role === "tool_call" || r.role === "tool_result") {
6567
+ const label = r.role === "tool_call" ? "tool call" : "tool result";
6568
+ return { role: "assistant", text: `[${label}] ${r.content}` };
6569
+ }
6570
+ return void 0;
6571
+ }
6269
6572
  async function flushSessionWrites() {
6270
6573
  while (pendingAppends.size > 0) {
6271
6574
  const all = Array.from(pendingAppends.values());
@@ -8615,11 +8918,32 @@ function reasoningEffortFromParams(params) {
8615
8918
  const thinking = params?.find((p) => p.id === "thinking");
8616
8919
  return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
8617
8920
  }
8921
+ function emitLlmMetrics(inputs, result, startAt) {
8922
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
8923
+ provider: inputs.llm.name
8924
+ });
8925
+ if (result.inputTokens === void 0 && result.outputTokens === void 0) {
8926
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
8927
+ provider: inputs.llm.name
8928
+ });
8929
+ process.stderr.write(
8930
+ `[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
8931
+ `
8932
+ );
8933
+ return;
8934
+ }
8935
+ inputs.telemetry?.recordHistogram(
8936
+ HISTOGRAM_NAMES.LLM_TOKENS,
8937
+ (result.inputTokens ?? 0) + (result.outputTokens ?? 0),
8938
+ { provider: inputs.llm.name }
8939
+ );
8940
+ }
8618
8941
  async function streamLlmTurn(inputs, ctx) {
8619
- const llmSpan = inputs.telemetry?.startSpan("llm.call", {
8942
+ const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
8620
8943
  "model.id": inputs.model.id ?? "auto",
8621
8944
  provider: inputs.llm.name
8622
8945
  });
8946
+ const startAt = Date.now();
8623
8947
  const signal = inputs.signal ?? new AbortController().signal;
8624
8948
  const generator = inputs.llm.stream(
8625
8949
  {
@@ -8662,6 +8986,7 @@ async function streamLlmTurn(inputs, ctx) {
8662
8986
  inputTokens: result.inputTokens ?? 0,
8663
8987
  outputTokens: result.outputTokens ?? 0
8664
8988
  });
8989
+ emitLlmMetrics(inputs, result, startAt);
8665
8990
  llmSpan?.end();
8666
8991
  const stripped = stripThinkBlocks(collected.accumulatedText);
8667
8992
  return {
@@ -8891,21 +9216,21 @@ async function executeTool(inputs, resolved, call) {
8891
9216
  }
8892
9217
  if (resolved.origin === "shell") return runShellTool(inputs, call);
8893
9218
  if (resolved.origin === "memory") return runMemoryTool(resolved, call);
8894
- if (resolved.origin === "custom") return runCustomTool(resolved, call);
9219
+ if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
8895
9220
  return runMcpTool(inputs, resolved, call);
8896
9221
  }
8897
9222
  async function runMemoryTool(resolved, call) {
8898
9223
  return runHandlerTool("memory", resolved.memoryHandler, call);
8899
9224
  }
8900
- async function runCustomTool(resolved, call) {
8901
- return runHandlerTool("custom", resolved.customHandler, call);
9225
+ async function runCustomTool(resolved, call, signal) {
9226
+ return runHandlerTool("custom", resolved.customHandler, call, signal);
8902
9227
  }
8903
- async function runHandlerTool(kind, handler, call) {
9228
+ async function runHandlerTool(kind, handler, call, signal) {
8904
9229
  if (handler === void 0) {
8905
9230
  return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
8906
9231
  }
8907
9232
  try {
8908
- const stdout = await handler(call.input);
9233
+ const stdout = await handler(call.input, { signal });
8909
9234
  return { stdout, stderr: "", exitCode: 0 };
8910
9235
  } catch (cause) {
8911
9236
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -8951,22 +9276,66 @@ ${result.stderr}`.trim();
8951
9276
  return result.stdout.trim();
8952
9277
  }
8953
9278
 
9279
+ // src/internal/agent-loop/tool-timeout.ts
9280
+ var TOOL_ABORTED_EXIT = 124;
9281
+ function abortedResult(signal) {
9282
+ const reason = signal.reason;
9283
+ const timedOut = reason?.name === "TimeoutError";
9284
+ return {
9285
+ stdout: "",
9286
+ stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
9287
+ exitCode: TOOL_ABORTED_EXIT
9288
+ };
9289
+ }
9290
+ function raceToolExecution(exec, opts) {
9291
+ const { signal, timeoutMs } = opts;
9292
+ if (signal === void 0 && timeoutMs === void 0) return exec;
9293
+ const signals = [];
9294
+ if (signal !== void 0) signals.push(signal);
9295
+ if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
9296
+ const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
9297
+ if (merged.aborted) return Promise.resolve(abortedResult(merged));
9298
+ return new Promise((resolve3, reject) => {
9299
+ let settled = false;
9300
+ const onAbort = () => {
9301
+ if (settled) return;
9302
+ settled = true;
9303
+ resolve3(abortedResult(merged));
9304
+ };
9305
+ merged.addEventListener("abort", onAbort, { once: true });
9306
+ exec.then(
9307
+ (r) => {
9308
+ if (settled) return;
9309
+ settled = true;
9310
+ merged.removeEventListener("abort", onAbort);
9311
+ resolve3(r);
9312
+ },
9313
+ (e) => {
9314
+ if (settled) return;
9315
+ settled = true;
9316
+ merged.removeEventListener("abort", onAbort);
9317
+ reject(e);
9318
+ }
9319
+ );
9320
+ });
9321
+ }
9322
+
8954
9323
  // src/internal/agent-loop/tool-dispatch.ts
8955
- async function dispatchTools(inputs, tools, toolCalls, events) {
9324
+ async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
8956
9325
  const maxConcurrent = inputs.maxConcurrentTools ?? 4;
8957
9326
  return mapWithConcurrency(
8958
9327
  toolCalls,
8959
9328
  maxConcurrent,
8960
- (call) => dispatchSingleCall(inputs, tools, call, events)
9329
+ (call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
8961
9330
  );
8962
9331
  }
8963
- async function dispatchSingleCall(inputs, tools, call, events) {
9332
+ async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
8964
9333
  const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
8965
9334
  const callId = generateCallId();
8966
9335
  const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
8967
9336
  if (forkVeto !== void 0) return forkVeto;
8968
9337
  const resolved = tools.find((tool) => tool.name === workingCall.name);
8969
- const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
9338
+ const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
8970
9339
  events.push(buildToolUseRunning(inputs, callId, workingCall));
8971
9340
  const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
8972
9341
  if (pluginVeto !== void 0) {
@@ -8983,6 +9352,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
8983
9352
  return fileVeto;
8984
9353
  }
8985
9354
  const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
9355
+ await inputs.pluginManager?.runPostToolCallHooks({
9356
+ name: workingCall.name,
9357
+ args: workingCall.input,
9358
+ result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
9359
+ agentId: inputs.agentId,
9360
+ runId: inputs.runId
9361
+ });
8986
9362
  return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
8987
9363
  }
8988
9364
  function applyRepairAndExtractCall(tools, call) {
@@ -9015,8 +9391,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
9015
9391
  content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
9016
9392
  };
9017
9393
  }
9018
- function startToolCallSpan(inputs, call, resolved, callId, repairs) {
9019
- const toolSpan = inputs.telemetry?.startSpan("tool.call", {
9394
+ function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
9395
+ const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
9020
9396
  "tool.name": call.name,
9021
9397
  "tool.origin": resolved?.origin ?? "unknown",
9022
9398
  callId
@@ -9080,8 +9456,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
9080
9456
  conversationId: inputs.agentId,
9081
9457
  callId
9082
9458
  });
9083
- const result = await executeTool(inputs, resolved, call);
9459
+ const result = await raceToolExecution(executeTool(inputs, resolved, call), {
9460
+ signal: inputs.signal,
9461
+ timeoutMs: inputs.perToolTimeoutMs
9462
+ });
9084
9463
  const durationMs = Date.now() - startAt;
9464
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
9465
+ "tool.name": call.name
9466
+ });
9085
9467
  if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
9086
9468
  await safeEmitToolHook(inputs.onToolError, {
9087
9469
  toolName: call.name,
@@ -9180,6 +9562,35 @@ function buildToolUseCompleted(inputs, callId, call, result) {
9180
9562
  };
9181
9563
  }
9182
9564
 
9565
+ // src/internal/agent-loop/tool-result-guard.ts
9566
+ var OPEN = "<untrusted-tool-output>";
9567
+ var CLOSE = "</untrusted-tool-output>";
9568
+ var PII_PATTERNS = [
9569
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
9570
+ // email
9571
+ /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
9572
+ // phone
9573
+ ];
9574
+ function guardText(content, opts) {
9575
+ let out = content;
9576
+ if (opts.redactPii === true) {
9577
+ for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
9578
+ }
9579
+ if (opts.delimit === true) {
9580
+ const safe3 = out.split(CLOSE).join("</ untrusted-tool-output>");
9581
+ out = `${OPEN}
9582
+ ${safe3}
9583
+ ${CLOSE}`;
9584
+ }
9585
+ return out;
9586
+ }
9587
+ function applyToolResultGuard(parts, opts) {
9588
+ if (opts.delimit !== true && opts.redactPii !== true) return parts;
9589
+ return parts.map(
9590
+ (p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
9591
+ );
9592
+ }
9593
+
9183
9594
  // src/internal/budget/pricing-data.json
9184
9595
  var pricing_data_default = {
9185
9596
  _meta: {
@@ -9530,6 +9941,11 @@ async function runAgentLoop(inputs) {
9530
9941
  try {
9531
9942
  const ctx = await initLoopContext(inputs);
9532
9943
  ctxRef = ctx;
9944
+ ctx.sendSpan = sendSpan;
9945
+ await inputs.pluginManager?.runOnSessionStartHooks({
9946
+ agentId: inputs.agentId,
9947
+ runId: inputs.runId
9948
+ });
9533
9949
  const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
9534
9950
  let lastTurnDecision;
9535
9951
  while (budget.shouldContinue()) {
@@ -9557,6 +9973,7 @@ async function runAgentLoop(inputs) {
9557
9973
  }
9558
9974
  budget.consume();
9559
9975
  inputs.budgetTracker?.nextIteration?.();
9976
+ if (inputs.signal?.aborted === true) break;
9560
9977
  }
9561
9978
  if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
9562
9979
  ctx.stoppedAtIterationLimit = true;
@@ -9596,6 +10013,10 @@ async function runAgentLoop(inputs) {
9596
10013
  ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
9597
10014
  };
9598
10015
  } finally {
10016
+ await inputs.pluginManager?.runOnSessionEndHooks({
10017
+ agentId: inputs.agentId,
10018
+ runId: inputs.runId
10019
+ });
9599
10020
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
9600
10021
  try {
9601
10022
  await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
@@ -9699,7 +10120,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
9699
10120
  return "done";
9700
10121
  }
9701
10122
  async function runIteration(inputs, ctx) {
10123
+ const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
10124
+ await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
9702
10125
  const llmOutput = await streamLlmTurn(inputs, ctx);
10126
+ await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
9703
10127
  accumulateUsage(ctx.usage, llmOutput);
9704
10128
  if (inputs.budgetTracker !== void 0) {
9705
10129
  const modelId = inputs.model.id ?? "auto";
@@ -9725,6 +10149,13 @@ async function runIteration(inputs, ctx) {
9725
10149
  }
9726
10150
  return continueOrTerminate(inputs, ctx, llmOutput);
9727
10151
  }
10152
+ async function transformLlmOutputText(inputs, text, ctx) {
10153
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
10154
+ }
10155
+ async function guardAndTransformToolResults(inputs, raw, ctx) {
10156
+ const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
10157
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
10158
+ }
9728
10159
  async function continueOrTerminate(inputs, ctx, llmOutput) {
9729
10160
  if (llmOutput.errored) return "error";
9730
10161
  if (llmOutput.text.length > 0) {
@@ -9733,8 +10164,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
9733
10164
  if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
9734
10165
  return finishOrReflect(inputs, ctx, llmOutput);
9735
10166
  }
9736
- ctx.messages.push(buildAssistantTurn(llmOutput.text, llmOutput.toolCalls));
9737
- const toolResults = await dispatchTools(inputs, ctx.tools, llmOutput.toolCalls, ctx.events);
10167
+ const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
10168
+ const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
10169
+ ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
10170
+ const rawResults = await dispatchTools(
10171
+ inputs,
10172
+ ctx.tools,
10173
+ llmOutput.toolCalls,
10174
+ ctx.events,
10175
+ ctx.sendSpan
10176
+ // M3 #64 — nest tool.call spans under agent.send
10177
+ );
10178
+ const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
9738
10179
  ctx.messages.push({ role: "user", content: toolResults });
9739
10180
  if (inputs.onStep !== void 0) {
9740
10181
  const cb = inputs.onStep;
@@ -10164,6 +10605,56 @@ function mapAnthropicStatusToCode(status, body) {
10164
10605
  function formatMessage(status, code) {
10165
10606
  return `Anthropic API error: ${code} (HTTP ${status})`;
10166
10607
  }
10608
+ var cachedJsonrepair;
10609
+ function loadJsonrepair() {
10610
+ if (cachedJsonrepair === void 0) {
10611
+ const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('eval.cjs', document.baseURI).href)));
10612
+ cachedJsonrepair = req("jsonrepair").jsonrepair;
10613
+ }
10614
+ return cachedJsonrepair;
10615
+ }
10616
+ function isPlainObject(v) {
10617
+ return v !== null && typeof v === "object" && !Array.isArray(v);
10618
+ }
10619
+ function toFiniteNumber(raw) {
10620
+ if (raw === "") return void 0;
10621
+ const n = Number(raw);
10622
+ return Number.isFinite(n) && String(n) === raw ? n : void 0;
10623
+ }
10624
+ function tryJson(raw, repair) {
10625
+ const t = raw.trimStart();
10626
+ if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
10627
+ try {
10628
+ return JSON.parse(repair ? loadJsonrepair()(t) : t);
10629
+ } catch {
10630
+ return void 0;
10631
+ }
10632
+ }
10633
+ function heuristicCoerce(raw, repairJson) {
10634
+ if (raw === "true") return true;
10635
+ if (raw === "false") return false;
10636
+ if (raw === "null") return null;
10637
+ const n = toFiniteNumber(raw);
10638
+ if (n !== void 0) return n;
10639
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
10640
+ return json === void 0 ? raw : json;
10641
+ }
10642
+ function coerceCandidates(raw, repairJson) {
10643
+ const out = [];
10644
+ if (raw === "true") out.push(true);
10645
+ else if (raw === "false") out.push(false);
10646
+ else if (raw === "null") out.push(null);
10647
+ const n = toFiniteNumber(raw);
10648
+ if (n !== void 0) out.push(n);
10649
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
10650
+ if (json !== void 0) out.push(json);
10651
+ out.push(raw);
10652
+ return out;
10653
+ }
10654
+ function objectShape(schema) {
10655
+ const shape = schema?.shape;
10656
+ return shape !== null && typeof shape === "object" ? shape : void 0;
10657
+ }
10167
10658
 
10168
10659
  // src/internal/llm/finish.ts
10169
10660
  function collapseSystemText(system) {
@@ -10176,9 +10667,21 @@ function parseToolArguments(buffered) {
10176
10667
  try {
10177
10668
  return JSON.parse(buffered);
10178
10669
  } catch {
10670
+ const repaired = tryJson(buffered, true);
10671
+ if (isPlainObject(repaired)) return repaired;
10179
10672
  return { raw: buffered };
10180
10673
  }
10181
10674
  }
10675
+ function mapOpenAIFinish(reason) {
10676
+ switch (reason) {
10677
+ case "tool_calls":
10678
+ return "tool_use";
10679
+ case "length":
10680
+ return "max_tokens";
10681
+ default:
10682
+ return "end_turn";
10683
+ }
10684
+ }
10182
10685
  function makeLlmFinish(state2) {
10183
10686
  const finish = {
10184
10687
  stopReason: state2.stopReason,
@@ -11060,56 +11563,9 @@ function toOllamaTools(tools) {
11060
11563
  }
11061
11564
  }));
11062
11565
  }
11063
- var cachedJsonrepair;
11064
- function loadJsonrepair() {
11065
- if (cachedJsonrepair === void 0) {
11066
- const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('eval.cjs', document.baseURI).href)));
11067
- cachedJsonrepair = req("jsonrepair").jsonrepair;
11068
- }
11069
- return cachedJsonrepair;
11070
- }
11071
- function isPlainObject(v) {
11072
- return v !== null && typeof v === "object" && !Array.isArray(v);
11073
- }
11074
- function toFiniteNumber(raw) {
11075
- if (raw === "") return void 0;
11076
- const n = Number(raw);
11077
- return Number.isFinite(n) && String(n) === raw ? n : void 0;
11078
- }
11079
- function tryJson(raw, repair) {
11080
- const t = raw.trimStart();
11081
- if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
11082
- try {
11083
- return JSON.parse(repair ? loadJsonrepair()(t) : t);
11084
- } catch {
11085
- return void 0;
11086
- }
11087
- }
11088
- function heuristicCoerce(raw, repairJson) {
11089
- if (raw === "true") return true;
11090
- if (raw === "false") return false;
11091
- if (raw === "null") return null;
11092
- const n = toFiniteNumber(raw);
11093
- if (n !== void 0) return n;
11094
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
11095
- return json === void 0 ? raw : json;
11096
- }
11097
- function coerceCandidates(raw, repairJson) {
11098
- const out = [];
11099
- if (raw === "true") out.push(true);
11100
- else if (raw === "false") out.push(false);
11101
- else if (raw === "null") out.push(null);
11102
- const n = toFiniteNumber(raw);
11103
- if (n !== void 0) out.push(n);
11104
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
11105
- if (json !== void 0) out.push(json);
11106
- out.push(raw);
11107
- return out;
11108
- }
11109
- function objectShape(schema) {
11110
- const shape = schema?.shape;
11111
- return shape !== null && typeof shape === "object" ? shape : void 0;
11112
- }
11566
+
11567
+ // src/internal/llm/openai.ts
11568
+ init_errors();
11113
11569
 
11114
11570
  // src/sanitize/sanitize-tool-input.ts
11115
11571
  function applyTrim(key, value, ctx) {
@@ -11354,8 +11810,12 @@ var OpenAIClient = class {
11354
11810
  // model was actually given. Empty set (no tools) recovers nothing.
11355
11811
  new Set(request.tools?.map((tool) => tool.name) ?? [])
11356
11812
  );
11813
+ let sawDone = false;
11357
11814
  for await (const record of parseSseStream(response.body, signal)) {
11358
- if (record.data === "[DONE]") break;
11815
+ if (record.data === "[DONE]") {
11816
+ sawDone = true;
11817
+ break;
11818
+ }
11359
11819
  let chunk;
11360
11820
  try {
11361
11821
  chunk = JSON.parse(record.data);
@@ -11375,6 +11835,11 @@ var OpenAIClient = class {
11375
11835
  const events = accumulator.consume(chunk);
11376
11836
  for (const event of events) yield event;
11377
11837
  }
11838
+ if (!sawDone && !accumulator.finishReasonSeen) {
11839
+ throw new NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
11840
+ code: "stream_truncated"
11841
+ });
11842
+ }
11378
11843
  const drainEvent = accumulator.finalizeHeldText();
11379
11844
  if (drainEvent !== void 0) yield drainEvent;
11380
11845
  return accumulator.finish();
@@ -11474,8 +11939,15 @@ var OpenAIStreamAccumulator = class {
11474
11939
  this.toolCalls.set(call.index, existing);
11475
11940
  }
11476
11941
  }
11942
+ /** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
11943
+ * stream ending without `[DONE]` is a truncation, not a clean end). */
11944
+ sawFinishReason = false;
11945
+ get finishReasonSeen() {
11946
+ return this.sawFinishReason;
11947
+ }
11477
11948
  applyFinishReason(reason) {
11478
11949
  if (reason === void 0 || reason === null) return;
11950
+ this.sawFinishReason = true;
11479
11951
  this.stopReason = mapOpenAIFinish(reason);
11480
11952
  }
11481
11953
  finish() {
@@ -11520,18 +11992,6 @@ var OpenAIStreamAccumulator = class {
11520
11992
  });
11521
11993
  }
11522
11994
  };
11523
- function mapOpenAIFinish(reason) {
11524
- switch (reason) {
11525
- case "tool_calls":
11526
- return "tool_use";
11527
- case "length":
11528
- return "max_tokens";
11529
- case "stop":
11530
- return "end_turn";
11531
- default:
11532
- return "end_turn";
11533
- }
11534
- }
11535
11995
  function applyReasoningRequest(body, effort, providerName) {
11536
11996
  if (providerName === "openai") {
11537
11997
  body.reasoning_effort = effort;
@@ -11629,19 +12089,76 @@ function assistantMessage(message) {
11629
12089
 
11630
12090
  // src/internal/llm/pool-aware-client.ts
11631
12091
  init_errors();
12092
+
12093
+ // src/internal/resilience/circuit-breaker.ts
12094
+ var DEFAULT_MAX_TIMEOUTS = 3;
12095
+ var DEFAULT_COOLDOWN_MS2 = 6e4;
12096
+ var CircuitBreaker = class {
12097
+ constructor(opts = {}) {
12098
+ this.opts = opts;
12099
+ }
12100
+ opts;
12101
+ states = /* @__PURE__ */ new Map();
12102
+ /** @returns true when the breaker is open and the call should be skipped. */
12103
+ shouldSkip(key) {
12104
+ const state2 = this.states.get(key);
12105
+ if (state2 === void 0) return false;
12106
+ if (state2.cooldownUntilMs === 0) return false;
12107
+ if (this.now() < state2.cooldownUntilMs) return true;
12108
+ state2.cooldownUntilMs = 0;
12109
+ state2.consecutiveTimeouts = 0;
12110
+ return false;
12111
+ }
12112
+ recordSuccess(key) {
12113
+ const state2 = this.states.get(key);
12114
+ if (state2 === void 0) return;
12115
+ state2.consecutiveTimeouts = 0;
12116
+ state2.cooldownUntilMs = 0;
12117
+ }
12118
+ recordTimeout(key) {
12119
+ const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12120
+ state2.consecutiveTimeouts += 1;
12121
+ if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
12122
+ state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
12123
+ }
12124
+ this.states.set(key, state2);
12125
+ }
12126
+ /** @internal — tests inspect counter state. */
12127
+ inspect(key) {
12128
+ return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12129
+ }
12130
+ now() {
12131
+ return this.opts.now?.() ?? Date.now();
12132
+ }
12133
+ };
12134
+
12135
+ // src/internal/llm/pool-aware-client.ts
12136
+ init_retry();
11632
12137
  var PoolAwareLlmClient = class {
11633
- constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
12138
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
11634
12139
  this.pool = pool;
11635
12140
  this.buildClient = buildClient2;
11636
12141
  this.waitForAvailableMs = waitForAvailableMs;
11637
12142
  this.name = `pool-aware:${pool.provider}`;
12143
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
12144
+ this.backoffBaseMs = resilience.backoffBaseMs;
12145
+ this.rng = resilience.rng;
11638
12146
  }
11639
12147
  pool;
11640
12148
  buildClient;
11641
12149
  waitForAvailableMs;
11642
12150
  name;
12151
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
12152
+ breaker;
12153
+ backoffBaseMs;
12154
+ rng;
11643
12155
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: stream() must serialize pool-select → build client → first-event probe → classify → retry/rotate/propagate. Extracting helpers fragments the linear narrative; the comments above each branch keep it readable.
11644
12156
  async *stream(request, signal) {
12157
+ if (this.breaker.shouldSkip(this.pool.provider)) {
12158
+ throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
12159
+ code: "circuit_open"
12160
+ });
12161
+ }
11645
12162
  let hasRetried429 = false;
11646
12163
  while (true) {
11647
12164
  if (signal.aborted) throw abortError2(signal);
@@ -11656,6 +12173,7 @@ var PoolAwareLlmClient = class {
11656
12173
  }
11657
12174
  }
11658
12175
  if (entry === null) {
12176
+ this.breaker.recordTimeout(this.pool.provider);
11659
12177
  throw new CredentialPoolExhaustedError(
11660
12178
  `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
11661
12179
  { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
@@ -11665,10 +12183,19 @@ var PoolAwareLlmClient = class {
11665
12183
  const realClient = this.buildClient(entry.accessToken);
11666
12184
  const attempt = await tryFirstEvent(realClient, request, signal);
11667
12185
  if (attempt.kind === "ok") {
12186
+ this.breaker.recordSuccess(this.pool.provider);
11668
12187
  return yield* relayStream(attempt.generator, attempt.firstResult);
11669
12188
  }
11670
12189
  const decision = classifyAndDecide(attempt.error, hasRetried429);
11671
12190
  if (decision === "retry") {
12191
+ await sleepWithAbort(
12192
+ computeBackoffMs({
12193
+ attempt: 0,
12194
+ ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
12195
+ ...this.rng !== void 0 ? { rng: this.rng } : {}
12196
+ }),
12197
+ signal
12198
+ );
11672
12199
  hasRetried429 = true;
11673
12200
  continue;
11674
12201
  }
@@ -11688,6 +12215,7 @@ var PoolAwareLlmClient = class {
11688
12215
  hasRetried429 = false;
11689
12216
  continue;
11690
12217
  }
12218
+ this.breaker.recordTimeout(this.pool.provider);
11691
12219
  throw attempt.error;
11692
12220
  }
11693
12221
  }
@@ -12122,9 +12650,28 @@ function selectTransport(profile, apiKey) {
12122
12650
 
12123
12651
  // src/internal/mcp/client.ts
12124
12652
  init_errors();
12125
- function createMcpClient(name, config) {
12653
+ function createMcpClient(name, config, fetchImpl = fetch) {
12126
12654
  if (isStdio(config)) return new StdioMcpClient(name, config);
12127
- return new HttpMcpClient(name, config);
12655
+ return new HttpMcpClient(name, config, fetchImpl);
12656
+ }
12657
+ var DEFAULT_MCP_TIMEOUT_MS = 3e4;
12658
+ var MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
12659
+ var RECONNECT_BASE_MS = 250;
12660
+ var MAX_RECONNECT_ATTEMPTS = 2;
12661
+ function reconnectDelay(attempt) {
12662
+ const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
12663
+ const ms = Math.floor(Math.random() * ceiling);
12664
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
12665
+ }
12666
+ function mcpTimeoutError(name, timeoutMs) {
12667
+ return new NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
12668
+ code: "mcp_timeout"
12669
+ });
12670
+ }
12671
+ function isAbortLike(cause) {
12672
+ if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
12673
+ const name = cause.name;
12674
+ return name === "TimeoutError" || name === "AbortError";
12128
12675
  }
12129
12676
  async function rpcInitialize(request) {
12130
12677
  await request("initialize", {
@@ -12170,32 +12717,107 @@ var StdioMcpClient = class extends BaseMcpClient {
12170
12717
  name;
12171
12718
  child;
12172
12719
  nextId = 1;
12720
+ // #59 — pending requests carry a reject + timer so a silent server times out
12721
+ // (typed error), a late reply after timeout is a no-op, and close() settles them.
12173
12722
  pending = /* @__PURE__ */ new Map();
12174
12723
  buffer = "";
12175
- async initialize() {
12724
+ // M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
12725
+ // unexpectedly OR times out (not via close()); the next request re-spawns with
12726
+ // backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
12727
+ // concurrent request so parallel tool dispatch after a drop awaits one handshake
12728
+ // instead of racing (or spuriously failing with mcp_not_init).
12729
+ dropped = false;
12730
+ reconnectAttempts = 0;
12731
+ reconnectPromise;
12732
+ get timeoutMs() {
12733
+ return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
12734
+ }
12735
+ /** Spawn the server child and wire stdout/stderr/error/exit handlers.
12736
+ * Shared by `initialize()` and the M2 #59 reconnect path. */
12737
+ spawnChild() {
12176
12738
  const resolvedCwd = resolveMcpCwd(this.config.cwd);
12177
12739
  const child = child_process.spawn(this.config.command, this.config.args ?? [], {
12178
12740
  cwd: resolvedCwd,
12179
- env: { ...process.env, ...this.config.env ?? {} }
12741
+ // #54 (F-H1) a third-party MCP server binary must not inherit host
12742
+ // secrets. Scrub secret-like vars by default; `config.env` still wins.
12743
+ env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
12180
12744
  });
12181
12745
  this.child = child;
12182
12746
  child.stdout.on("data", (chunk) => this.consume(chunk));
12183
12747
  child.stderr.on("data", () => void 0);
12748
+ child.stdin.on("error", () => void 0);
12184
12749
  child.on("error", () => {
12185
- for (const resolver of this.pending.values()) {
12186
- resolver({ error: { message: "MCP process crashed" } });
12187
- }
12188
- this.pending.clear();
12750
+ this.rejectAllPending(
12751
+ new NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
12752
+ );
12753
+ });
12754
+ child.on("exit", () => {
12755
+ if (this.child !== child) return;
12756
+ this.child = void 0;
12757
+ this.dropped = true;
12758
+ this.rejectAllPending(
12759
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
12760
+ );
12189
12761
  });
12762
+ }
12763
+ async initialize() {
12764
+ this.spawnChild();
12190
12765
  await super.initialize();
12191
12766
  }
12767
+ /** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
12768
+ * full-jitter backoff) when the client was dropped; fail fast when never
12769
+ * initialized. Concurrent callers share ONE reconnect handshake. */
12770
+ ensureConnected() {
12771
+ if (this.child !== void 0) return Promise.resolve();
12772
+ if (!this.dropped) {
12773
+ return Promise.reject(
12774
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
12775
+ );
12776
+ }
12777
+ this.reconnectPromise ??= this.reconnect().finally(() => {
12778
+ this.reconnectPromise = void 0;
12779
+ });
12780
+ return this.reconnectPromise;
12781
+ }
12782
+ async reconnect() {
12783
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
12784
+ throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
12785
+ }
12786
+ await reconnectDelay(this.reconnectAttempts);
12787
+ this.reconnectAttempts += 1;
12788
+ this.spawnChild();
12789
+ await super.initialize();
12790
+ this.dropped = false;
12791
+ this.reconnectAttempts = 0;
12792
+ }
12192
12793
  async close() {
12193
- if (this.child === void 0) return;
12194
- this.child.kill("SIGTERM");
12794
+ this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
12795
+ const child = this.child;
12195
12796
  this.child = void 0;
12797
+ this.dropped = false;
12798
+ child?.kill("SIGTERM");
12799
+ }
12800
+ /** Reject + clear every pending request (crash / close). @internal */
12801
+ rejectAllPending(error) {
12802
+ for (const entry of this.pending.values()) {
12803
+ clearTimeout(entry.timer);
12804
+ entry.reject(error);
12805
+ }
12806
+ this.pending.clear();
12196
12807
  }
12197
12808
  consume(chunk) {
12198
12809
  this.buffer += chunk.toString("utf8");
12810
+ if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
12811
+ this.buffer = "";
12812
+ this.rejectAllPending(
12813
+ new NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
12814
+ code: "mcp_buffer_overflow"
12815
+ })
12816
+ );
12817
+ this.child?.kill("SIGKILL");
12818
+ this.child = void 0;
12819
+ return;
12820
+ }
12199
12821
  let newlineIndex = this.buffer.indexOf("\n");
12200
12822
  while (newlineIndex !== -1) {
12201
12823
  const line = this.buffer.slice(0, newlineIndex).trim();
@@ -12212,23 +12834,47 @@ var StdioMcpClient = class extends BaseMcpClient {
12212
12834
  return;
12213
12835
  }
12214
12836
  if (typeof message.id !== "number") return;
12215
- const resolver = this.pending.get(message.id);
12216
- if (resolver === void 0) return;
12837
+ const entry = this.pending.get(message.id);
12838
+ if (entry === void 0) return;
12217
12839
  this.pending.delete(message.id);
12218
- resolver(message);
12840
+ clearTimeout(entry.timer);
12841
+ entry.resolve(message);
12219
12842
  }
12220
12843
  request(method, params) {
12221
- if (this.child === void 0) {
12222
- return Promise.reject(
12223
- new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
12224
- );
12844
+ const child = this.child;
12845
+ if (child !== void 0) return this.send(child, method, params);
12846
+ if (this.dropped) return this.reconnectAndRequest(method, params);
12847
+ return Promise.reject(
12848
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
12849
+ );
12850
+ }
12851
+ /** M2 #59 — reconnect a dropped client, then send. Separate async path so the
12852
+ * happy path above never pays an extra microtask tick. */
12853
+ async reconnectAndRequest(method, params) {
12854
+ await this.ensureConnected();
12855
+ const child = this.child;
12856
+ if (child === void 0) {
12857
+ throw new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
12225
12858
  }
12859
+ return this.send(child, method, params);
12860
+ }
12861
+ send(child, method, params) {
12226
12862
  const id = this.nextId++;
12227
12863
  const payload = { jsonrpc: "2.0", id, method, params };
12228
- this.child.stdin.write(`${JSON.stringify(payload)}
12864
+ child.stdin.write(`${JSON.stringify(payload)}
12229
12865
  `);
12230
- return new Promise((resolve3) => {
12231
- this.pending.set(id, resolve3);
12866
+ return new Promise((resolve3, reject) => {
12867
+ const timer = setTimeout(() => {
12868
+ this.pending.delete(id);
12869
+ reject(mcpTimeoutError(this.name, this.timeoutMs));
12870
+ this.child?.kill("SIGKILL");
12871
+ this.child = void 0;
12872
+ this.dropped = true;
12873
+ this.rejectAllPending(
12874
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
12875
+ );
12876
+ }, this.timeoutMs);
12877
+ this.pending.set(id, { resolve: resolve3, reject, timer });
12232
12878
  });
12233
12879
  }
12234
12880
  };
@@ -12254,11 +12900,20 @@ var HttpMcpClient = class extends BaseMcpClient {
12254
12900
  accept: "application/json",
12255
12901
  ...this.config.headers ?? {}
12256
12902
  };
12257
- const response = await this.fetchImpl(this.config.url, {
12258
- method: "POST",
12259
- headers,
12260
- body: JSON.stringify(payload)
12261
- });
12903
+ const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
12904
+ let response;
12905
+ try {
12906
+ response = await this.fetchImpl(this.config.url, {
12907
+ method: "POST",
12908
+ headers,
12909
+ body: JSON.stringify(payload),
12910
+ // #59 — bound the request; a non-responding endpoint aborts here.
12911
+ signal: AbortSignal.timeout(timeoutMs)
12912
+ });
12913
+ } catch (cause) {
12914
+ if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
12915
+ throw cause;
12916
+ }
12262
12917
  if (!response.ok) {
12263
12918
  throw new NetworkError(`MCP ${this.name} returned ${response.status}`, {
12264
12919
  code: "mcp_http_error"
@@ -12373,11 +13028,33 @@ function resolveRunProvider(options) {
12373
13028
  );
12374
13029
  }
12375
13030
  const parsedModel = parseModelId(options.model?.id);
12376
- const inferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
12377
- const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? inferredProvider ?? detectPrimaryProvider();
12378
- const effectiveModelId = inferredProvider !== void 0 ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
13031
+ const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
13032
+ const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
13033
+ const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
13034
+ const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
12379
13035
  return { primary, effectiveModelId };
12380
13036
  }
13037
+ function inferProviderFromApiKey(apiKey) {
13038
+ if (apiKey === void 0 || apiKey.length === 0) return void 0;
13039
+ const byPrefix = [
13040
+ { provider: "openrouter", prefix: "sk-or-" },
13041
+ { provider: "anthropic", prefix: "sk-ant-" },
13042
+ { provider: "openai", prefix: "sk-" }
13043
+ ];
13044
+ for (const { provider, prefix } of byPrefix) {
13045
+ if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
13046
+ return provider;
13047
+ }
13048
+ }
13049
+ return void 0;
13050
+ }
13051
+ function mergeExplicitApiKey(pools, primary, apiKey) {
13052
+ if (apiKey === void 0 || apiKey.length === 0) return pools;
13053
+ if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
13054
+ const existing = pools?.[primary];
13055
+ if (existing !== void 0 && existing.length > 0) return pools;
13056
+ return { ...pools ?? {}, [primary]: [apiKey] };
13057
+ }
12381
13058
  function buildLoopInputs(options, runId, userText) {
12382
13059
  const maxIterations = options.sendOptions.maxIterations;
12383
13060
  if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
@@ -12388,7 +13065,11 @@ function buildLoopInputs(options, runId, userText) {
12388
13065
  }
12389
13066
  const { primary, effectiveModelId } = resolveRunProvider(options);
12390
13067
  const fallback = options.agentOptions.providers?.fallback;
12391
- const apiKeys = options.agentOptions.providers?.apiKeys;
13068
+ const apiKeys = mergeExplicitApiKey(
13069
+ options.agentOptions.providers?.apiKeys,
13070
+ primary,
13071
+ options.agentOptions.apiKey
13072
+ );
12392
13073
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
12393
13074
  const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
12394
13075
  const chain = resolveProviderChain({
@@ -12432,6 +13113,10 @@ function buildLoopInputs(options, runId, userText) {
12432
13113
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
12433
13114
  // can attach it to the LLM `fetch({ signal })` call.
12434
13115
  ...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
13116
+ // #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
13117
+ // can enable them via SendOptions (not only internal AgentLoopInputs).
13118
+ ...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
13119
+ ...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
12435
13120
  // M1-2: per-send iteration ceiling (validated above). The loop reads
12436
13121
  // inputs.maxIterations (default 8 when unset).
12437
13122
  ...maxIterations !== void 0 ? { maxIterations } : {},
@@ -12748,7 +13433,12 @@ async function runActiveMemory(args) {
12748
13433
  hits: []
12749
13434
  });
12750
13435
  }
12751
- const cached2 = args.cache?.get(args.userText, cfg.queryMode);
13436
+ const tenantCtx = {
13437
+ namespace: args.namespace,
13438
+ userId: args.userId,
13439
+ scope: args.scope
13440
+ };
13441
+ const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
12752
13442
  if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
12753
13443
  const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
12754
13444
  if (query.trim().length === 0) {
@@ -12833,7 +13523,12 @@ function notifyBreaker(breaker, key, status) {
12833
13523
  else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key);
12834
13524
  }
12835
13525
  async function finalize(args, queryMode, result) {
12836
- args.cache?.set(args.userText, queryMode, result);
13526
+ const tenantCtx = {
13527
+ namespace: args.namespace,
13528
+ userId: args.userId,
13529
+ scope: args.scope
13530
+ };
13531
+ args.cache?.set(args.userText, queryMode, result, tenantCtx);
12837
13532
  if (args.persistTranscripts === true && args.cwd !== void 0) {
12838
13533
  await persistActiveMemoryTranscript(args.cwd, {
12839
13534
  runId: args.runId ?? `run-${Date.now()}`,
@@ -13435,48 +14130,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
13435
14130
  gemini: geminiMemoryEmbeddingProviderAdapter
13436
14131
  };
13437
14132
 
13438
- // src/internal/memory/circuit-breaker.ts
13439
- var DEFAULT_MAX_TIMEOUTS = 3;
13440
- var DEFAULT_COOLDOWN_MS2 = 6e4;
13441
- var CircuitBreaker = class {
13442
- constructor(opts = {}) {
13443
- this.opts = opts;
13444
- }
13445
- opts;
13446
- states = /* @__PURE__ */ new Map();
13447
- /** @returns true when the breaker is open and the call should be skipped. */
13448
- shouldSkip(key) {
13449
- const state2 = this.states.get(key);
13450
- if (state2 === void 0) return false;
13451
- if (state2.cooldownUntilMs === 0) return false;
13452
- if (this.now() < state2.cooldownUntilMs) return true;
13453
- state2.cooldownUntilMs = 0;
13454
- state2.consecutiveTimeouts = 0;
13455
- return false;
13456
- }
13457
- recordSuccess(key) {
13458
- const state2 = this.states.get(key);
13459
- if (state2 === void 0) return;
13460
- state2.consecutiveTimeouts = 0;
13461
- state2.cooldownUntilMs = 0;
13462
- }
13463
- recordTimeout(key) {
13464
- const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
13465
- state2.consecutiveTimeouts += 1;
13466
- if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
13467
- state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
13468
- }
13469
- this.states.set(key, state2);
13470
- }
13471
- /** @internal — tests inspect counter state. */
13472
- inspect(key) {
13473
- return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
13474
- }
13475
- now() {
13476
- return this.opts.now?.() ?? Date.now();
13477
- }
13478
- };
13479
-
13480
14133
  // src/internal/persistence/fts5-sanitize.ts
13481
14134
  var PHRASE_OPEN = "";
13482
14135
  var PHRASE_CLOSE = "";
@@ -16710,7 +17363,9 @@ var SandboxBackend = class {
16710
17363
  this.config = {
16711
17364
  workDir: config.workDir ?? "/tmp",
16712
17365
  timeoutMs: config.timeoutMs ?? 3e4,
16713
- maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024
17366
+ maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024,
17367
+ // #54 — preserve the env policy so backends can scrub secrets.
17368
+ env: config.env ?? "inherit-scrubbed"
16714
17369
  };
16715
17370
  }
16716
17371
  async readFile(path) {
@@ -16780,7 +17435,9 @@ var LocalSandbox = class extends SandboxBackend {
16780
17435
  cwd: this.config.workDir,
16781
17436
  timeout,
16782
17437
  maxBuffer: max,
16783
- encoding: "utf-8"
17438
+ encoding: "utf-8",
17439
+ // #54 — scrub secret-like host env vars from the child by default.
17440
+ env: resolveChildEnv({ policy: this.config.env })
16784
17441
  },
16785
17442
  (error, stdout, stderr) => {
16786
17443
  resolve3(this.buildResult(error, stdout ?? "", stderr ?? ""));