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