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