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