@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
@@ -2548,13 +2548,13 @@ var init_cloud_run = __esm({
2548
2548
  });
2549
2549
 
2550
2550
  // src/internal/llm/sse.ts
2551
- async function* parseSseStream(body, signal) {
2551
+ async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
2552
2552
  if (body === null) return;
2553
2553
  const reader = body.getReader();
2554
2554
  const decoder = new TextDecoder("utf-8");
2555
2555
  const state2 = { buffer: "", event: "message", data: "" };
2556
2556
  try {
2557
- for await (const chunk of readChunks(reader, signal)) {
2557
+ for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
2558
2558
  state2.buffer += decoder.decode(chunk, { stream: true });
2559
2559
  for (const record of drainCompleteRecords(state2)) yield record;
2560
2560
  }
@@ -2564,14 +2564,38 @@ async function* parseSseStream(body, signal) {
2564
2564
  releaseReader(reader);
2565
2565
  }
2566
2566
  }
2567
- async function* readChunks(reader, signal) {
2567
+ async function* readChunks(reader, signal, idleTimeoutMs) {
2568
2568
  while (true) {
2569
2569
  if (signal.aborted) return;
2570
- const { value, done } = await reader.read();
2570
+ const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
2571
2571
  if (done) return;
2572
2572
  if (value !== void 0) yield value;
2573
2573
  }
2574
2574
  }
2575
+ function readWithIdleTimeout(reader, idleTimeoutMs) {
2576
+ if (idleTimeoutMs <= 0) return reader.read();
2577
+ return new Promise(
2578
+ (resolve3, reject) => {
2579
+ const timer = setTimeout(() => {
2580
+ reject(
2581
+ new NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
2582
+ code: "stream_idle_timeout"
2583
+ })
2584
+ );
2585
+ }, idleTimeoutMs);
2586
+ reader.read().then(
2587
+ (result) => {
2588
+ clearTimeout(timer);
2589
+ resolve3(result);
2590
+ },
2591
+ (err) => {
2592
+ clearTimeout(timer);
2593
+ reject(err);
2594
+ }
2595
+ );
2596
+ }
2597
+ );
2598
+ }
2575
2599
  async function cancelReaderQuietly(reader) {
2576
2600
  try {
2577
2601
  await reader.cancel();
@@ -2622,8 +2646,11 @@ function releaseReader(reader) {
2622
2646
  } catch {
2623
2647
  }
2624
2648
  }
2649
+ var DEFAULT_SSE_IDLE_MS;
2625
2650
  var init_sse = __esm({
2626
2651
  "src/internal/llm/sse.ts"() {
2652
+ init_errors();
2653
+ DEFAULT_SSE_IDLE_MS = 6e4;
2627
2654
  }
2628
2655
  });
2629
2656
 
@@ -3572,9 +3599,16 @@ var init_context = __esm({
3572
3599
  });
3573
3600
 
3574
3601
  // src/internal/plugins/manager.ts
3602
+ function removeAll(arr, toRemove) {
3603
+ for (const item of toRemove) {
3604
+ const idx = arr.indexOf(item);
3605
+ if (idx !== -1) arr.splice(idx, 1);
3606
+ }
3607
+ }
3575
3608
  var PluginManager;
3576
3609
  var init_manager = __esm({
3577
3610
  "src/internal/plugins/manager.ts"() {
3611
+ init_errors();
3578
3612
  init_context();
3579
3613
  PluginManager = class {
3580
3614
  #aggregated = {
@@ -3586,6 +3620,9 @@ var init_manager = __esm({
3586
3620
  memoryProviders: []
3587
3621
  };
3588
3622
  #initialized = false;
3623
+ // #68 — registrations of plugins added post-init via `register()`, keyed by
3624
+ // plugin name so a re-register REPLACES (not appends) the prior hooks.
3625
+ #byName = /* @__PURE__ */ new Map();
3589
3626
  async initialize(plugins) {
3590
3627
  if (this.#initialized) {
3591
3628
  throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
@@ -3603,6 +3640,36 @@ var init_manager = __esm({
3603
3640
  await this.#dispatchPlugin(plugin);
3604
3641
  }
3605
3642
  }
3643
+ /**
3644
+ * #68 — register a single `general` plugin AFTER `initialize()` has run.
3645
+ *
3646
+ * The bulk `initialize()` is single-shot (one call per process); late
3647
+ * registration is a distinct, named operation used by adapters that install
3648
+ * a plugin per-session/per-request (e.g. the ACP permission veto, which is
3649
+ * installed once the permission mode + connection are known — after the
3650
+ * agent's own plugins were already initialized).
3651
+ *
3652
+ * Idempotent by plugin NAME: re-registering a plugin with the same name
3653
+ * REPLACES its prior hooks/tools instead of appending duplicates (the ACP
3654
+ * permission plugin is re-installed on every prompt).
3655
+ *
3656
+ * Only `general` plugins may be registered late — model-provider / memory
3657
+ * plugins are resolved during the bulk init and cannot be added afterwards.
3658
+ */
3659
+ async register(plugin) {
3660
+ if (plugin.kind !== "general") {
3661
+ throw new ConfigurationError(
3662
+ `late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
3663
+ { code: "plugin_late_register_kind" }
3664
+ );
3665
+ }
3666
+ const prior = this.#byName.get(plugin.name);
3667
+ if (prior !== void 0) this.#unmerge(prior);
3668
+ const { ctx, registrations } = createPluginContext();
3669
+ await plugin.register(ctx);
3670
+ this.#byName.set(plugin.name, registrations);
3671
+ this.#merge(registrations);
3672
+ }
3606
3673
  get aggregated() {
3607
3674
  return this.#aggregated;
3608
3675
  }
@@ -3683,6 +3750,64 @@ var init_manager = __esm({
3683
3750
  }
3684
3751
  }
3685
3752
  }
3753
+ // #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
3754
+ // in order (per-handler errors logged, never thrown); transform hooks fold
3755
+ // over the payload (a handler returning a value replaces it).
3756
+ /** @internal */
3757
+ async #runFireAndForget(name, ctx) {
3758
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
3759
+ try {
3760
+ await h(ctx);
3761
+ } catch (err) {
3762
+ process.stderr.write(
3763
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
3764
+ `
3765
+ );
3766
+ }
3767
+ }
3768
+ }
3769
+ /** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
3770
+ async #runTransform(name, payload, ctx) {
3771
+ let current = payload;
3772
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
3773
+ try {
3774
+ const out = await h(current, ctx);
3775
+ if (out !== void 0) current = out;
3776
+ } catch (err) {
3777
+ process.stderr.write(
3778
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
3779
+ `
3780
+ );
3781
+ }
3782
+ }
3783
+ return current;
3784
+ }
3785
+ /** #65 — fired after a tool call completes. @internal */
3786
+ runPostToolCallHooks(ctx) {
3787
+ return this.#runFireAndForget("post_tool_call", ctx);
3788
+ }
3789
+ /** #65 — fired before / after each LLM turn. @internal */
3790
+ runPreLlmCallHooks(ctx) {
3791
+ return this.#runFireAndForget("pre_llm_call", ctx);
3792
+ }
3793
+ runPostLlmCallHooks(ctx) {
3794
+ return this.#runFireAndForget("post_llm_call", ctx);
3795
+ }
3796
+ /** #65 — fired at run start / end. @internal */
3797
+ runOnSessionStartHooks(ctx) {
3798
+ return this.#runFireAndForget("on_session_start", ctx);
3799
+ }
3800
+ runOnSessionEndHooks(ctx) {
3801
+ return this.#runFireAndForget("on_session_end", ctx);
3802
+ }
3803
+ /** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
3804
+ runTransformToolResultHooks(results, ctx) {
3805
+ return this.#runTransform("transform_tool_result", results, ctx);
3806
+ }
3807
+ /** #65 — transform the LLM output text before it is consumed. @internal */
3808
+ runTransformLlmOutputHooks(output, ctx) {
3809
+ return this.#runTransform("transform_llm_output", output, ctx);
3810
+ }
3686
3811
  async #dispatchPlugin(plugin) {
3687
3812
  if (plugin.kind === "general") {
3688
3813
  const { ctx, registrations } = createPluginContext();
@@ -3710,6 +3835,22 @@ var init_manager = __esm({
3710
3835
  }
3711
3836
  this.#aggregated.injected.push(...r.injected);
3712
3837
  }
3838
+ /**
3839
+ * #68 — inverse of #merge: remove a prior registration's contributions from
3840
+ * the aggregated view by object identity. Used by `register()` to replace a
3841
+ * same-named plugin's hooks/tools instead of accumulating duplicates.
3842
+ */
3843
+ #unmerge(r) {
3844
+ removeAll(this.#aggregated.tools, r.tools);
3845
+ removeAll(this.#aggregated.commands, r.commands);
3846
+ removeAll(this.#aggregated.injected, r.injected);
3847
+ for (const [hook, handlers] of r.hooks.entries()) {
3848
+ const existing = this.#aggregated.hooks.get(hook);
3849
+ if (existing === void 0) continue;
3850
+ removeAll(existing, handlers);
3851
+ if (existing.length === 0) this.#aggregated.hooks.delete(hook);
3852
+ }
3853
+ }
3713
3854
  };
3714
3855
  }
3715
3856
  });
@@ -3734,7 +3875,12 @@ var init_span_names = __esm({
3734
3875
  LLM_CALL: "llm.call"
3735
3876
  };
3736
3877
  HISTOGRAM_NAMES = {
3737
- MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
3878
+ MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
3879
+ TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
3880
+ LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
3881
+ LLM_TOKENS: "theokit_llm_tokens",
3882
+ /** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
3883
+ LLM_USAGE_MISSING: "theokit_llm_usage_missing"
3738
3884
  };
3739
3885
  }
3740
3886
  });
@@ -4099,7 +4245,25 @@ function createTelemetry(settings) {
4099
4245
  enabled: true,
4100
4246
  includeContent: settings.includeContent === true,
4101
4247
  startSpan: startNewSpan,
4102
- startChildSpan: (_parent, name, attrs) => startNewSpan(name, attrs),
4248
+ // M3 #64 actually nest the child under its parent instead of discarding it.
4249
+ // The parent's SpanContext is set on a fresh OTel context so the child links
4250
+ // to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
4251
+ // back to a root span when the parent has no valid span id (telemetry off /
4252
+ // NOOP), preserving the pre-M3 behavior for parentless callers.
4253
+ startChildSpan: (parent, name, attrs) => {
4254
+ const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
4255
+ const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
4256
+ const pctx = safe(() => parent?.spanContext(), void 0);
4257
+ const span = safe(() => {
4258
+ if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
4259
+ const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
4260
+ return tracer.startSpan(name, opts, childCtx);
4261
+ }
4262
+ return tracer.startSpan(name, opts);
4263
+ }, NOOP_SPAN);
4264
+ if (span !== NOOP_SPAN) openSpans.add(span);
4265
+ return wrapSpan(span, openSpans);
4266
+ },
4103
4267
  recordHistogram,
4104
4268
  endAll: () => {
4105
4269
  for (const span of openSpans) safe(() => span.end(), void 0);
@@ -4169,12 +4333,65 @@ var init_tracer = __esm({
4169
4333
  warnedOnce = false;
4170
4334
  }
4171
4335
  });
4336
+
4337
+ // src/internal/runtime/lifecycle/env-policy.ts
4338
+ function isSecretName(name) {
4339
+ return SECRET_PATTERNS.some((re) => re.test(name));
4340
+ }
4341
+ function inheritsUnderPolicy(name, policy) {
4342
+ if (policy === "all") return true;
4343
+ if (policy === "core") return CORE_VARS.includes(name);
4344
+ return !isSecretName(name);
4345
+ }
4346
+ function resolveChildEnv(options = {}) {
4347
+ const parent = options.parent ?? process.env;
4348
+ const policy = options.policy ?? "inherit-scrubbed";
4349
+ const base = {};
4350
+ for (const [name, value] of Object.entries(parent)) {
4351
+ if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
4352
+ }
4353
+ for (const [name, value] of Object.entries(options.overrides ?? {})) {
4354
+ base[name] = value;
4355
+ }
4356
+ return base;
4357
+ }
4358
+ var SECRET_PATTERNS, CORE_VARS;
4359
+ var init_env_policy = __esm({
4360
+ "src/internal/runtime/lifecycle/env-policy.ts"() {
4361
+ SECRET_PATTERNS = [
4362
+ /KEY/i,
4363
+ /SECRET/i,
4364
+ /TOKEN/i,
4365
+ /PASSWORD/i,
4366
+ /PASSWD/i,
4367
+ /PASSPHRASE/i,
4368
+ /[_-]PWD/i,
4369
+ /CREDENTIAL/i,
4370
+ /PRIVATE/i,
4371
+ /_AUTH/i
4372
+ ];
4373
+ CORE_VARS = [
4374
+ "PATH",
4375
+ "HOME",
4376
+ "SHELL",
4377
+ "LANG",
4378
+ "LC_ALL",
4379
+ "LC_CTYPE",
4380
+ "TMPDIR",
4381
+ "TMP",
4382
+ "TEMP",
4383
+ "USER",
4384
+ "LOGNAME"
4385
+ ];
4386
+ }
4387
+ });
4172
4388
  function spawnAndCollect(options) {
4173
4389
  return new Promise((resolve3) => {
4174
4390
  const timeoutMs = options.timeoutMs ?? 3e4;
4175
4391
  const spawnOptions = {
4176
4392
  cwd: options.cwd,
4177
- env: { ...process.env, ...options.env ?? {} }
4393
+ // #54 — scrub secret-like parent env by default; `options.env` still wins.
4394
+ env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
4178
4395
  };
4179
4396
  const child = child_process.spawn(options.command, options.args ?? [], spawnOptions);
4180
4397
  let stdout = "";
@@ -4215,6 +4432,7 @@ function spawnAndCollect(options) {
4215
4432
  }
4216
4433
  var init_spawn_collect = __esm({
4217
4434
  "src/internal/runtime/lifecycle/spawn-collect.ts"() {
4435
+ init_env_policy();
4218
4436
  }
4219
4437
  });
4220
4438
 
@@ -4403,15 +4621,24 @@ function warnMalformed(agentId, line) {
4403
4621
  `
4404
4622
  );
4405
4623
  }
4624
+ function hydrateSessionLine(parsed) {
4625
+ if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
4626
+ if (parsed.role === "user" || parsed.role === "assistant") {
4627
+ return { role: parsed.role, text: parsed.text };
4628
+ }
4629
+ if (parsed.role === "tool_call" || parsed.role === "tool_result") {
4630
+ const label = parsed.role === "tool_call" ? "tool call" : "tool result";
4631
+ return { role: "assistant", text: `[${label}] ${parsed.text}` };
4632
+ }
4633
+ return void 0;
4634
+ }
4406
4635
  async function readSessionFile(cwd, agentId) {
4407
4636
  const lines = await readJsonlLines(cwd, agentId);
4408
4637
  const messages = [];
4409
4638
  for (const line of lines) {
4410
4639
  try {
4411
- const parsed = JSON.parse(line);
4412
- if ((parsed.role === "user" || parsed.role === "assistant") && typeof parsed.text === "string") {
4413
- messages.push({ role: parsed.role, text: parsed.text });
4414
- }
4640
+ const msg = hydrateSessionLine(JSON.parse(line));
4641
+ if (msg !== void 0) messages.push(msg);
4415
4642
  } catch {
4416
4643
  warnMalformed(agentId, line);
4417
4644
  }
@@ -4438,29 +4665,70 @@ async function readAllPersistedMessages(cwd, agentId) {
4438
4665
  return messages;
4439
4666
  }
4440
4667
  async function appendAnyPersistedMessage(cwd, agentId, record) {
4668
+ await appendPersistedMessages(cwd, agentId, [record]);
4669
+ }
4670
+ async function appendPersistedMessages(cwd, agentId, records) {
4671
+ if (records.length === 0) return;
4441
4672
  const path$1 = sessionFilePath(cwd, agentId);
4442
- await promises.mkdir(path.dirname(path$1), { recursive: true });
4443
- await promises.appendFile(path$1, `${redactSecrets(JSON.stringify(record))}
4444
- `, "utf8");
4673
+ const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
4674
+ `).join("");
4675
+ const dir = path.dirname(path$1);
4676
+ let written = false;
4677
+ const attempt = async () => {
4678
+ await promises.mkdir(dir, { recursive: true });
4679
+ await withFileLock(path$1, async () => {
4680
+ await promises.appendFile(path$1, payload, "utf8");
4681
+ written = true;
4682
+ });
4683
+ };
4684
+ try {
4685
+ await attempt();
4686
+ } catch (cause) {
4687
+ if (written || cause.code !== "ENOENT") throw cause;
4688
+ await attempt();
4689
+ }
4690
+ }
4691
+ async function rewriteLockedSession(path, transform) {
4692
+ await withFileLock(path, async () => {
4693
+ let raw;
4694
+ try {
4695
+ raw = await promises.readFile(path, "utf8");
4696
+ } catch {
4697
+ return;
4698
+ }
4699
+ const lines = raw.split("\n").filter((line) => line.length > 0);
4700
+ const next = transform(lines);
4701
+ if (next === void 0) return;
4702
+ await replaceFileAtomic(path, next);
4703
+ });
4445
4704
  }
4446
4705
  async function compactSessionFile(cwd, agentId, maxTurns) {
4447
4706
  const path = sessionFilePath(cwd, agentId);
4448
- let raw;
4449
- try {
4450
- raw = await promises.readFile(path, "utf8");
4451
- } catch {
4452
- return;
4453
- }
4454
- const lines = raw.split("\n").filter((line) => line.length > 0);
4455
- if (lines.length <= maxTurns * 2) return;
4456
- const trimmed = `${lines.slice(-maxTurns).join("\n")}
4707
+ if (!fs.existsSync(path)) return;
4708
+ await rewriteLockedSession(
4709
+ path,
4710
+ (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
4711
+ `
4712
+ );
4713
+ }
4714
+ async function truncateSessionTo(cwd, agentId, keepCount) {
4715
+ const path = sessionFilePath(cwd, agentId);
4716
+ if (!fs.existsSync(path)) return 0;
4717
+ let kept = 0;
4718
+ await rewriteLockedSession(path, (lines) => {
4719
+ const keep = Math.max(0, Math.min(keepCount, lines.length));
4720
+ kept = keep;
4721
+ if (keep === lines.length) return void 0;
4722
+ return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
4457
4723
  `;
4458
- await replaceFileAtomic(path, trimmed);
4724
+ });
4725
+ return kept;
4459
4726
  }
4460
4727
  var VALID_ROLES;
4461
4728
  var init_agent_session_store = __esm({
4462
4729
  "src/internal/runtime/session/agent-session-store.ts"() {
4463
4730
  init_atomic_write();
4731
+ init_file_lock();
4464
4732
  init_security();
4465
4733
  VALID_ROLES = /* @__PURE__ */ new Set([
4466
4734
  "user",
@@ -4471,6 +4739,18 @@ var init_agent_session_store = __esm({
4471
4739
  ]);
4472
4740
  }
4473
4741
  });
4742
+
4743
+ // src/internal/persistence/pagination.ts
4744
+ function paginate(items, opts) {
4745
+ if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
4746
+ const start = Math.max(0, opts.offset ?? 0);
4747
+ const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
4748
+ return items.slice(start, end);
4749
+ }
4750
+ var init_pagination = __esm({
4751
+ "src/internal/persistence/pagination.ts"() {
4752
+ }
4753
+ });
4474
4754
  function toStoredMessage(record) {
4475
4755
  return {
4476
4756
  role: record.role,
@@ -4478,11 +4758,15 @@ function toStoredMessage(record) {
4478
4758
  at: record.at
4479
4759
  };
4480
4760
  }
4761
+ function toRecord(message) {
4762
+ return { role: message.role, text: message.content, at: message.at ?? Date.now() };
4763
+ }
4481
4764
  var FileSystemConversationStorage;
4482
4765
  var init_conversation_storage_fs = __esm({
4483
4766
  "src/internal/persistence/conversation-storage-fs.ts"() {
4484
4767
  init_agent_session_store();
4485
4768
  init_security();
4769
+ init_pagination();
4486
4770
  FileSystemConversationStorage = class {
4487
4771
  #root;
4488
4772
  constructor(opts = {}) {
@@ -4492,23 +4776,31 @@ var init_conversation_storage_fs = __esm({
4492
4776
  get root() {
4493
4777
  return this.#root;
4494
4778
  }
4495
- async getMessages(conversationId) {
4779
+ async getMessages(conversationId, opts) {
4496
4780
  const records = await readAllPersistedMessages(this.#root, conversationId);
4497
- return records.map(toStoredMessage);
4781
+ const all = records.map(toStoredMessage);
4782
+ return paginate(all, opts);
4498
4783
  }
4499
4784
  async appendMessage(conversationId, message) {
4500
- const record = {
4501
- role: message.role,
4502
- text: message.content,
4503
- at: message.at ?? Date.now()
4504
- };
4505
- await appendAnyPersistedMessage(this.#root, conversationId, record);
4785
+ await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
4786
+ }
4787
+ async appendMessages(conversationId, messages) {
4788
+ await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
4789
+ }
4790
+ async truncateConversation(conversationId, keepCount) {
4791
+ return truncateSessionTo(this.#root, conversationId, keepCount);
4506
4792
  }
4507
4793
  async deleteConversation(conversationId) {
4508
4794
  const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
4509
4795
  const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
4510
4796
  await promises.rm(dirPath, { recursive: true, force: true });
4511
4797
  }
4798
+ async deleteScope(prefix) {
4799
+ const ids = await this.listConversationIds();
4800
+ const matching = ids.filter((id) => id.startsWith(prefix));
4801
+ for (const id of matching) await this.deleteConversation(id);
4802
+ return matching.length;
4803
+ }
4512
4804
  async listConversationIds(opts = {}) {
4513
4805
  const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
4514
4806
  let entries;
@@ -4605,12 +4897,19 @@ async function readPersistedForCache(adapter, agentId) {
4605
4897
  const records = await adapter.getMessages(agentId);
4606
4898
  const out = [];
4607
4899
  for (const r of records) {
4608
- if (r.role === "user" || r.role === "assistant") {
4609
- out.push({ role: r.role, text: r.content });
4610
- }
4900
+ const folded = foldStoredToSession(r);
4901
+ if (folded !== void 0) out.push(folded);
4611
4902
  }
4612
4903
  return out;
4613
4904
  }
4905
+ function foldStoredToSession(r) {
4906
+ if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
4907
+ if (r.role === "tool_call" || r.role === "tool_result") {
4908
+ const label = r.role === "tool_call" ? "tool call" : "tool result";
4909
+ return { role: "assistant", text: `[${label}] ${r.content}` };
4910
+ }
4911
+ return void 0;
4912
+ }
4614
4913
  async function flushSessionWrites() {
4615
4914
  while (pendingAppends.size > 0) {
4616
4915
  const all = Array.from(pendingAppends.values());
@@ -7181,11 +7480,32 @@ function reasoningEffortFromParams(params) {
7181
7480
  const thinking = params?.find((p) => p.id === "thinking");
7182
7481
  return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
7183
7482
  }
7483
+ function emitLlmMetrics(inputs, result, startAt) {
7484
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
7485
+ provider: inputs.llm.name
7486
+ });
7487
+ if (result.inputTokens === void 0 && result.outputTokens === void 0) {
7488
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
7489
+ provider: inputs.llm.name
7490
+ });
7491
+ process.stderr.write(
7492
+ `[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
7493
+ `
7494
+ );
7495
+ return;
7496
+ }
7497
+ inputs.telemetry?.recordHistogram(
7498
+ HISTOGRAM_NAMES.LLM_TOKENS,
7499
+ (result.inputTokens ?? 0) + (result.outputTokens ?? 0),
7500
+ { provider: inputs.llm.name }
7501
+ );
7502
+ }
7184
7503
  async function streamLlmTurn(inputs, ctx) {
7185
- const llmSpan = inputs.telemetry?.startSpan("llm.call", {
7504
+ const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
7186
7505
  "model.id": inputs.model.id ?? "auto",
7187
7506
  provider: inputs.llm.name
7188
7507
  });
7508
+ const startAt = Date.now();
7189
7509
  const signal = inputs.signal ?? new AbortController().signal;
7190
7510
  const generator = inputs.llm.stream(
7191
7511
  {
@@ -7228,6 +7548,7 @@ async function streamLlmTurn(inputs, ctx) {
7228
7548
  inputTokens: result.inputTokens ?? 0,
7229
7549
  outputTokens: result.outputTokens ?? 0
7230
7550
  });
7551
+ emitLlmMetrics(inputs, result, startAt);
7231
7552
  llmSpan?.end();
7232
7553
  const stripped = stripThinkBlocks(collected.accumulatedText);
7233
7554
  return {
@@ -7324,6 +7645,7 @@ async function emitReasoningDeltaCallback(inputs, text) {
7324
7645
  var init_loop_llm_stream = __esm({
7325
7646
  "src/internal/agent-loop/loop-llm-stream.ts"() {
7326
7647
  init_safe_call();
7648
+ init_span_names();
7327
7649
  init_strip_think();
7328
7650
  init_message_builders();
7329
7651
  }
@@ -7542,21 +7864,21 @@ async function executeTool(inputs, resolved, call) {
7542
7864
  }
7543
7865
  if (resolved.origin === "shell") return runShellTool(inputs, call);
7544
7866
  if (resolved.origin === "memory") return runMemoryTool(resolved, call);
7545
- if (resolved.origin === "custom") return runCustomTool(resolved, call);
7867
+ if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
7546
7868
  return runMcpTool(inputs, resolved, call);
7547
7869
  }
7548
7870
  async function runMemoryTool(resolved, call) {
7549
7871
  return runHandlerTool("memory", resolved.memoryHandler, call);
7550
7872
  }
7551
- async function runCustomTool(resolved, call) {
7552
- return runHandlerTool("custom", resolved.customHandler, call);
7873
+ async function runCustomTool(resolved, call, signal) {
7874
+ return runHandlerTool("custom", resolved.customHandler, call, signal);
7553
7875
  }
7554
- async function runHandlerTool(kind, handler, call) {
7876
+ async function runHandlerTool(kind, handler, call, signal) {
7555
7877
  if (handler === void 0) {
7556
7878
  return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
7557
7879
  }
7558
7880
  try {
7559
- const stdout = await handler(call.input);
7881
+ const stdout = await handler(call.input, { signal });
7560
7882
  return { stdout, stderr: "", exitCode: 0 };
7561
7883
  } catch (cause) {
7562
7884
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -7607,22 +7929,71 @@ var init_tool_executors = __esm({
7607
7929
  }
7608
7930
  });
7609
7931
 
7932
+ // src/internal/agent-loop/tool-timeout.ts
7933
+ function abortedResult(signal) {
7934
+ const reason = signal.reason;
7935
+ const timedOut = reason?.name === "TimeoutError";
7936
+ return {
7937
+ stdout: "",
7938
+ stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
7939
+ exitCode: TOOL_ABORTED_EXIT
7940
+ };
7941
+ }
7942
+ function raceToolExecution(exec, opts) {
7943
+ const { signal, timeoutMs } = opts;
7944
+ if (signal === void 0 && timeoutMs === void 0) return exec;
7945
+ const signals = [];
7946
+ if (signal !== void 0) signals.push(signal);
7947
+ if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
7948
+ const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
7949
+ if (merged.aborted) return Promise.resolve(abortedResult(merged));
7950
+ return new Promise((resolve3, reject) => {
7951
+ let settled = false;
7952
+ const onAbort = () => {
7953
+ if (settled) return;
7954
+ settled = true;
7955
+ resolve3(abortedResult(merged));
7956
+ };
7957
+ merged.addEventListener("abort", onAbort, { once: true });
7958
+ exec.then(
7959
+ (r) => {
7960
+ if (settled) return;
7961
+ settled = true;
7962
+ merged.removeEventListener("abort", onAbort);
7963
+ resolve3(r);
7964
+ },
7965
+ (e) => {
7966
+ if (settled) return;
7967
+ settled = true;
7968
+ merged.removeEventListener("abort", onAbort);
7969
+ reject(e);
7970
+ }
7971
+ );
7972
+ });
7973
+ }
7974
+ var TOOL_ABORTED_EXIT;
7975
+ var init_tool_timeout = __esm({
7976
+ "src/internal/agent-loop/tool-timeout.ts"() {
7977
+ TOOL_ABORTED_EXIT = 124;
7978
+ }
7979
+ });
7980
+
7610
7981
  // src/internal/agent-loop/tool-dispatch.ts
7611
- async function dispatchTools(inputs, tools, toolCalls, events) {
7982
+ async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
7612
7983
  const maxConcurrent = inputs.maxConcurrentTools ?? 4;
7613
7984
  return mapWithConcurrency(
7614
7985
  toolCalls,
7615
7986
  maxConcurrent,
7616
- (call) => dispatchSingleCall(inputs, tools, call, events)
7987
+ (call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
7617
7988
  );
7618
7989
  }
7619
- async function dispatchSingleCall(inputs, tools, call, events) {
7990
+ async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
7620
7991
  const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
7621
7992
  const callId = generateCallId();
7622
7993
  const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
7623
7994
  if (forkVeto !== void 0) return forkVeto;
7624
7995
  const resolved = tools.find((tool) => tool.name === workingCall.name);
7625
- const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
7996
+ const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
7626
7997
  events.push(buildToolUseRunning(inputs, callId, workingCall));
7627
7998
  const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
7628
7999
  if (pluginVeto !== void 0) {
@@ -7639,6 +8010,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
7639
8010
  return fileVeto;
7640
8011
  }
7641
8012
  const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
8013
+ await inputs.pluginManager?.runPostToolCallHooks({
8014
+ name: workingCall.name,
8015
+ args: workingCall.input,
8016
+ result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
8017
+ agentId: inputs.agentId,
8018
+ runId: inputs.runId
8019
+ });
7642
8020
  return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
7643
8021
  }
7644
8022
  function applyRepairAndExtractCall(tools, call) {
@@ -7671,8 +8049,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
7671
8049
  content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
7672
8050
  };
7673
8051
  }
7674
- function startToolCallSpan(inputs, call, resolved, callId, repairs) {
7675
- const toolSpan = inputs.telemetry?.startSpan("tool.call", {
8052
+ function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
8053
+ const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
7676
8054
  "tool.name": call.name,
7677
8055
  "tool.origin": resolved?.origin ?? "unknown",
7678
8056
  callId
@@ -7736,8 +8114,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
7736
8114
  conversationId: inputs.agentId,
7737
8115
  callId
7738
8116
  });
7739
- const result = await executeTool(inputs, resolved, call);
8117
+ const result = await raceToolExecution(executeTool(inputs, resolved, call), {
8118
+ signal: inputs.signal,
8119
+ timeoutMs: inputs.perToolTimeoutMs
8120
+ });
7740
8121
  const durationMs = Date.now() - startAt;
8122
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
8123
+ "tool.name": call.name
8124
+ });
7741
8125
  if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
7742
8126
  await safeEmitToolHook(inputs.onToolError, {
7743
8127
  toolName: call.name,
@@ -7840,8 +8224,44 @@ var init_tool_dispatch = __esm({
7840
8224
  init_ids();
7841
8225
  init_async_local_storage();
7842
8226
  init_map_with_concurrency();
8227
+ init_span_names();
7843
8228
  init_repair_middleware();
7844
8229
  init_tool_executors();
8230
+ init_tool_timeout();
8231
+ }
8232
+ });
8233
+
8234
+ // src/internal/agent-loop/tool-result-guard.ts
8235
+ function guardText(content, opts) {
8236
+ let out = content;
8237
+ if (opts.redactPii === true) {
8238
+ for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
8239
+ }
8240
+ if (opts.delimit === true) {
8241
+ const safe2 = out.split(CLOSE).join("</ untrusted-tool-output>");
8242
+ out = `${OPEN}
8243
+ ${safe2}
8244
+ ${CLOSE}`;
8245
+ }
8246
+ return out;
8247
+ }
8248
+ function applyToolResultGuard(parts, opts) {
8249
+ if (opts.delimit !== true && opts.redactPii !== true) return parts;
8250
+ return parts.map(
8251
+ (p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
8252
+ );
8253
+ }
8254
+ var OPEN, CLOSE, PII_PATTERNS;
8255
+ var init_tool_result_guard = __esm({
8256
+ "src/internal/agent-loop/tool-result-guard.ts"() {
8257
+ OPEN = "<untrusted-tool-output>";
8258
+ CLOSE = "</untrusted-tool-output>";
8259
+ PII_PATTERNS = [
8260
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
8261
+ // email
8262
+ /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
8263
+ // phone
8264
+ ];
7845
8265
  }
7846
8266
  });
7847
8267
 
@@ -8225,6 +8645,11 @@ async function runAgentLoop(inputs) {
8225
8645
  try {
8226
8646
  const ctx = await initLoopContext(inputs);
8227
8647
  ctxRef = ctx;
8648
+ ctx.sendSpan = sendSpan;
8649
+ await inputs.pluginManager?.runOnSessionStartHooks({
8650
+ agentId: inputs.agentId,
8651
+ runId: inputs.runId
8652
+ });
8228
8653
  const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
8229
8654
  let lastTurnDecision;
8230
8655
  while (budget.shouldContinue()) {
@@ -8252,6 +8677,7 @@ async function runAgentLoop(inputs) {
8252
8677
  }
8253
8678
  budget.consume();
8254
8679
  inputs.budgetTracker?.nextIteration?.();
8680
+ if (inputs.signal?.aborted === true) break;
8255
8681
  }
8256
8682
  if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
8257
8683
  ctx.stoppedAtIterationLimit = true;
@@ -8291,6 +8717,10 @@ async function runAgentLoop(inputs) {
8291
8717
  ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
8292
8718
  };
8293
8719
  } finally {
8720
+ await inputs.pluginManager?.runOnSessionEndHooks({
8721
+ agentId: inputs.agentId,
8722
+ runId: inputs.runId
8723
+ });
8294
8724
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
8295
8725
  try {
8296
8726
  await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
@@ -8394,7 +8824,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
8394
8824
  return "done";
8395
8825
  }
8396
8826
  async function runIteration(inputs, ctx) {
8827
+ const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
8828
+ await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
8397
8829
  const llmOutput = await streamLlmTurn(inputs, ctx);
8830
+ await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
8398
8831
  accumulateUsage(ctx.usage, llmOutput);
8399
8832
  if (inputs.budgetTracker !== void 0) {
8400
8833
  const modelId = inputs.model.id ?? "auto";
@@ -8420,6 +8853,13 @@ async function runIteration(inputs, ctx) {
8420
8853
  }
8421
8854
  return continueOrTerminate(inputs, ctx, llmOutput);
8422
8855
  }
8856
+ async function transformLlmOutputText(inputs, text, ctx) {
8857
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
8858
+ }
8859
+ async function guardAndTransformToolResults(inputs, raw, ctx) {
8860
+ const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
8861
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
8862
+ }
8423
8863
  async function continueOrTerminate(inputs, ctx, llmOutput) {
8424
8864
  if (llmOutput.errored) return "error";
8425
8865
  if (llmOutput.text.length > 0) {
@@ -8428,8 +8868,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
8428
8868
  if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
8429
8869
  return finishOrReflect(inputs, ctx, llmOutput);
8430
8870
  }
8431
- ctx.messages.push(buildAssistantTurn(llmOutput.text, llmOutput.toolCalls));
8432
- const toolResults = await dispatchTools(inputs, ctx.tools, llmOutput.toolCalls, ctx.events);
8871
+ const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
8872
+ const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
8873
+ ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
8874
+ const rawResults = await dispatchTools(
8875
+ inputs,
8876
+ ctx.tools,
8877
+ llmOutput.toolCalls,
8878
+ ctx.events,
8879
+ ctx.sendSpan
8880
+ // M3 #64 — nest tool.call spans under agent.send
8881
+ );
8882
+ const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
8433
8883
  ctx.messages.push({ role: "user", content: toolResults });
8434
8884
  if (inputs.onStep !== void 0) {
8435
8885
  const cb = inputs.onStep;
@@ -8479,6 +8929,7 @@ var init_loop = __esm({
8479
8929
  init_loop_llm_stream();
8480
8930
  init_message_builders();
8481
8931
  init_tool_dispatch();
8932
+ init_tool_result_guard();
8482
8933
  init_usage_and_cost();
8483
8934
  MAX_NUDGE_ATTEMPTS = 2;
8484
8935
  MAX_STOP_FEEDBACK_ATTEMPTS = 2;
@@ -8968,6 +9419,60 @@ var init_anthropic2 = __esm({
8968
9419
  init_shared();
8969
9420
  }
8970
9421
  });
9422
+ function loadJsonrepair() {
9423
+ if (cachedJsonrepair === void 0) {
9424
+ const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
9425
+ cachedJsonrepair = req("jsonrepair").jsonrepair;
9426
+ }
9427
+ return cachedJsonrepair;
9428
+ }
9429
+ function isPlainObject(v) {
9430
+ return v !== null && typeof v === "object" && !Array.isArray(v);
9431
+ }
9432
+ function toFiniteNumber(raw) {
9433
+ if (raw === "") return void 0;
9434
+ const n = Number(raw);
9435
+ return Number.isFinite(n) && String(n) === raw ? n : void 0;
9436
+ }
9437
+ function tryJson(raw, repair) {
9438
+ const t = raw.trimStart();
9439
+ if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
9440
+ try {
9441
+ return JSON.parse(repair ? loadJsonrepair()(t) : t);
9442
+ } catch {
9443
+ return void 0;
9444
+ }
9445
+ }
9446
+ function heuristicCoerce(raw, repairJson) {
9447
+ if (raw === "true") return true;
9448
+ if (raw === "false") return false;
9449
+ if (raw === "null") return null;
9450
+ const n = toFiniteNumber(raw);
9451
+ if (n !== void 0) return n;
9452
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
9453
+ return json === void 0 ? raw : json;
9454
+ }
9455
+ function coerceCandidates(raw, repairJson) {
9456
+ const out = [];
9457
+ if (raw === "true") out.push(true);
9458
+ else if (raw === "false") out.push(false);
9459
+ else if (raw === "null") out.push(null);
9460
+ const n = toFiniteNumber(raw);
9461
+ if (n !== void 0) out.push(n);
9462
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
9463
+ if (json !== void 0) out.push(json);
9464
+ out.push(raw);
9465
+ return out;
9466
+ }
9467
+ function objectShape(schema) {
9468
+ const shape = schema?.shape;
9469
+ return shape !== null && typeof shape === "object" ? shape : void 0;
9470
+ }
9471
+ var cachedJsonrepair;
9472
+ var init_coerce = __esm({
9473
+ "src/sanitize/coerce.ts"() {
9474
+ }
9475
+ });
8971
9476
 
8972
9477
  // src/internal/llm/finish.ts
8973
9478
  function collapseSystemText(system) {
@@ -8980,9 +9485,21 @@ function parseToolArguments(buffered) {
8980
9485
  try {
8981
9486
  return JSON.parse(buffered);
8982
9487
  } catch {
9488
+ const repaired = tryJson(buffered, true);
9489
+ if (isPlainObject(repaired)) return repaired;
8983
9490
  return { raw: buffered };
8984
9491
  }
8985
9492
  }
9493
+ function mapOpenAIFinish(reason) {
9494
+ switch (reason) {
9495
+ case "tool_calls":
9496
+ return "tool_use";
9497
+ case "length":
9498
+ return "max_tokens";
9499
+ default:
9500
+ return "end_turn";
9501
+ }
9502
+ }
8986
9503
  function makeLlmFinish(state2) {
8987
9504
  const finish = {
8988
9505
  stopReason: state2.stopReason,
@@ -8998,6 +9515,7 @@ function makeLlmFinish(state2) {
8998
9515
  }
8999
9516
  var init_finish = __esm({
9000
9517
  "src/internal/llm/finish.ts"() {
9518
+ init_coerce();
9001
9519
  }
9002
9520
  });
9003
9521
 
@@ -9441,6 +9959,16 @@ var init_credential_pool_types = __esm({
9441
9959
  });
9442
9960
 
9443
9961
  // src/internal/llm/retry.ts
9962
+ function computeBackoffMs(opts) {
9963
+ const base = opts.baseMs ?? DEFAULT_BASE_MS;
9964
+ const cap = opts.capMs ?? DEFAULT_CAP_MS;
9965
+ const rng = opts.rng ?? Math.random;
9966
+ if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
9967
+ return Math.max(base, Math.min(cap, opts.retryAfterMs));
9968
+ }
9969
+ const ceiling = Math.min(cap, base * 2 ** opts.attempt);
9970
+ return Math.floor(rng() * ceiling);
9971
+ }
9444
9972
  function sleepWithAbort(ms, signal) {
9445
9973
  if (ms <= 0 || signal.aborted) return Promise.resolve();
9446
9974
  return new Promise((resolve3) => {
@@ -9455,8 +9983,11 @@ function sleepWithAbort(ms, signal) {
9455
9983
  signal.addEventListener("abort", onAbort, { once: true });
9456
9984
  });
9457
9985
  }
9986
+ var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
9458
9987
  var init_retry = __esm({
9459
9988
  "src/internal/llm/retry.ts"() {
9989
+ DEFAULT_BASE_MS = 500;
9990
+ DEFAULT_CAP_MS = 32e3;
9460
9991
  }
9461
9992
  });
9462
9993
 
@@ -10212,60 +10743,6 @@ var init_ollama_native = __esm({
10212
10743
  ollamaSystemText = collapseSystemText;
10213
10744
  }
10214
10745
  });
10215
- function loadJsonrepair() {
10216
- if (cachedJsonrepair === void 0) {
10217
- const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
10218
- cachedJsonrepair = req("jsonrepair").jsonrepair;
10219
- }
10220
- return cachedJsonrepair;
10221
- }
10222
- function isPlainObject(v) {
10223
- return v !== null && typeof v === "object" && !Array.isArray(v);
10224
- }
10225
- function toFiniteNumber(raw) {
10226
- if (raw === "") return void 0;
10227
- const n = Number(raw);
10228
- return Number.isFinite(n) && String(n) === raw ? n : void 0;
10229
- }
10230
- function tryJson(raw, repair) {
10231
- const t = raw.trimStart();
10232
- if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
10233
- try {
10234
- return JSON.parse(repair ? loadJsonrepair()(t) : t);
10235
- } catch {
10236
- return void 0;
10237
- }
10238
- }
10239
- function heuristicCoerce(raw, repairJson) {
10240
- if (raw === "true") return true;
10241
- if (raw === "false") return false;
10242
- if (raw === "null") return null;
10243
- const n = toFiniteNumber(raw);
10244
- if (n !== void 0) return n;
10245
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
10246
- return json === void 0 ? raw : json;
10247
- }
10248
- function coerceCandidates(raw, repairJson) {
10249
- const out = [];
10250
- if (raw === "true") out.push(true);
10251
- else if (raw === "false") out.push(false);
10252
- else if (raw === "null") out.push(null);
10253
- const n = toFiniteNumber(raw);
10254
- if (n !== void 0) out.push(n);
10255
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
10256
- if (json !== void 0) out.push(json);
10257
- out.push(raw);
10258
- return out;
10259
- }
10260
- function objectShape(schema) {
10261
- const shape = schema?.shape;
10262
- return shape !== null && typeof shape === "object" ? shape : void 0;
10263
- }
10264
- var cachedJsonrepair;
10265
- var init_coerce = __esm({
10266
- "src/sanitize/coerce.ts"() {
10267
- }
10268
- });
10269
10746
 
10270
10747
  // src/sanitize/sanitize-tool-input.ts
10271
10748
  function applyTrim(key, value, ctx) {
@@ -10446,18 +10923,6 @@ var init_hermes_tool_extract = __esm({
10446
10923
  });
10447
10924
 
10448
10925
  // src/internal/llm/openai.ts
10449
- function mapOpenAIFinish(reason) {
10450
- switch (reason) {
10451
- case "tool_calls":
10452
- return "tool_use";
10453
- case "length":
10454
- return "max_tokens";
10455
- case "stop":
10456
- return "end_turn";
10457
- default:
10458
- return "end_turn";
10459
- }
10460
- }
10461
10926
  function applyReasoningRequest(body, effort, providerName) {
10462
10927
  if (providerName === "openai") {
10463
10928
  body.reasoning_effort = effort;
@@ -10554,6 +11019,7 @@ function assistantMessage(message) {
10554
11019
  var OpenAIClient, OpenAIStreamAccumulator, openAISystemText;
10555
11020
  var init_openai2 = __esm({
10556
11021
  "src/internal/llm/openai.ts"() {
11022
+ init_errors();
10557
11023
  init_ollama2();
10558
11024
  init_openai_compatible();
10559
11025
  init_finish();
@@ -10634,8 +11100,12 @@ var init_openai2 = __esm({
10634
11100
  // model was actually given. Empty set (no tools) recovers nothing.
10635
11101
  new Set(request.tools?.map((tool) => tool.name) ?? [])
10636
11102
  );
11103
+ let sawDone = false;
10637
11104
  for await (const record of parseSseStream(response.body, signal)) {
10638
- if (record.data === "[DONE]") break;
11105
+ if (record.data === "[DONE]") {
11106
+ sawDone = true;
11107
+ break;
11108
+ }
10639
11109
  let chunk;
10640
11110
  try {
10641
11111
  chunk = JSON.parse(record.data);
@@ -10655,6 +11125,11 @@ var init_openai2 = __esm({
10655
11125
  const events = accumulator.consume(chunk);
10656
11126
  for (const event of events) yield event;
10657
11127
  }
11128
+ if (!sawDone && !accumulator.finishReasonSeen) {
11129
+ throw new NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
11130
+ code: "stream_truncated"
11131
+ });
11132
+ }
10658
11133
  const drainEvent = accumulator.finalizeHeldText();
10659
11134
  if (drainEvent !== void 0) yield drainEvent;
10660
11135
  return accumulator.finish();
@@ -10754,8 +11229,15 @@ var init_openai2 = __esm({
10754
11229
  this.toolCalls.set(call.index, existing);
10755
11230
  }
10756
11231
  }
11232
+ /** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
11233
+ * stream ending without `[DONE]` is a truncation, not a clean end). */
11234
+ sawFinishReason = false;
11235
+ get finishReasonSeen() {
11236
+ return this.sawFinishReason;
11237
+ }
10757
11238
  applyFinishReason(reason) {
10758
11239
  if (reason === void 0 || reason === null) return;
11240
+ this.sawFinishReason = true;
10759
11241
  this.stopReason = mapOpenAIFinish(reason);
10760
11242
  }
10761
11243
  finish() {
@@ -10804,6 +11286,53 @@ var init_openai2 = __esm({
10804
11286
  }
10805
11287
  });
10806
11288
 
11289
+ // src/internal/resilience/circuit-breaker.ts
11290
+ var DEFAULT_MAX_TIMEOUTS, DEFAULT_COOLDOWN_MS2, CircuitBreaker;
11291
+ var init_circuit_breaker = __esm({
11292
+ "src/internal/resilience/circuit-breaker.ts"() {
11293
+ DEFAULT_MAX_TIMEOUTS = 3;
11294
+ DEFAULT_COOLDOWN_MS2 = 6e4;
11295
+ CircuitBreaker = class {
11296
+ constructor(opts = {}) {
11297
+ this.opts = opts;
11298
+ }
11299
+ opts;
11300
+ states = /* @__PURE__ */ new Map();
11301
+ /** @returns true when the breaker is open and the call should be skipped. */
11302
+ shouldSkip(key) {
11303
+ const state2 = this.states.get(key);
11304
+ if (state2 === void 0) return false;
11305
+ if (state2.cooldownUntilMs === 0) return false;
11306
+ if (this.now() < state2.cooldownUntilMs) return true;
11307
+ state2.cooldownUntilMs = 0;
11308
+ state2.consecutiveTimeouts = 0;
11309
+ return false;
11310
+ }
11311
+ recordSuccess(key) {
11312
+ const state2 = this.states.get(key);
11313
+ if (state2 === void 0) return;
11314
+ state2.consecutiveTimeouts = 0;
11315
+ state2.cooldownUntilMs = 0;
11316
+ }
11317
+ recordTimeout(key) {
11318
+ const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
11319
+ state2.consecutiveTimeouts += 1;
11320
+ if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
11321
+ state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
11322
+ }
11323
+ this.states.set(key, state2);
11324
+ }
11325
+ /** @internal — tests inspect counter state. */
11326
+ inspect(key) {
11327
+ return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
11328
+ }
11329
+ now() {
11330
+ return this.opts.now?.() ?? Date.now();
11331
+ }
11332
+ };
11333
+ }
11334
+ });
11335
+
10807
11336
  // src/internal/llm/pool-aware-client.ts
10808
11337
  function classifyAndDecide(error, hasRetried429) {
10809
11338
  if (error instanceof NetworkError) return "propagate";
@@ -10832,20 +11361,34 @@ var PoolAwareLlmClient;
10832
11361
  var init_pool_aware_client = __esm({
10833
11362
  "src/internal/llm/pool-aware-client.ts"() {
10834
11363
  init_errors();
11364
+ init_circuit_breaker();
11365
+ init_retry();
10835
11366
  init_stream_relay();
10836
11367
  PoolAwareLlmClient = class {
10837
- constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
11368
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
10838
11369
  this.pool = pool;
10839
11370
  this.buildClient = buildClient2;
10840
11371
  this.waitForAvailableMs = waitForAvailableMs;
10841
11372
  this.name = `pool-aware:${pool.provider}`;
11373
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
11374
+ this.backoffBaseMs = resilience.backoffBaseMs;
11375
+ this.rng = resilience.rng;
10842
11376
  }
10843
11377
  pool;
10844
11378
  buildClient;
10845
11379
  waitForAvailableMs;
10846
11380
  name;
11381
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
11382
+ breaker;
11383
+ backoffBaseMs;
11384
+ rng;
10847
11385
  // 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.
10848
11386
  async *stream(request, signal) {
11387
+ if (this.breaker.shouldSkip(this.pool.provider)) {
11388
+ throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
11389
+ code: "circuit_open"
11390
+ });
11391
+ }
10849
11392
  let hasRetried429 = false;
10850
11393
  while (true) {
10851
11394
  if (signal.aborted) throw abortError2(signal);
@@ -10860,6 +11403,7 @@ var init_pool_aware_client = __esm({
10860
11403
  }
10861
11404
  }
10862
11405
  if (entry === null) {
11406
+ this.breaker.recordTimeout(this.pool.provider);
10863
11407
  throw new CredentialPoolExhaustedError(
10864
11408
  `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
10865
11409
  { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
@@ -10869,10 +11413,19 @@ var init_pool_aware_client = __esm({
10869
11413
  const realClient = this.buildClient(entry.accessToken);
10870
11414
  const attempt = await tryFirstEvent(realClient, request, signal);
10871
11415
  if (attempt.kind === "ok") {
11416
+ this.breaker.recordSuccess(this.pool.provider);
10872
11417
  return yield* relayStream(attempt.generator, attempt.firstResult);
10873
11418
  }
10874
11419
  const decision = classifyAndDecide(attempt.error, hasRetried429);
10875
11420
  if (decision === "retry") {
11421
+ await sleepWithAbort(
11422
+ computeBackoffMs({
11423
+ attempt: 0,
11424
+ ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
11425
+ ...this.rng !== void 0 ? { rng: this.rng } : {}
11426
+ }),
11427
+ signal
11428
+ );
10876
11429
  hasRetried429 = true;
10877
11430
  continue;
10878
11431
  }
@@ -10892,6 +11445,7 @@ var init_pool_aware_client = __esm({
10892
11445
  hasRetried429 = false;
10893
11446
  continue;
10894
11447
  }
11448
+ this.breaker.recordTimeout(this.pool.provider);
10895
11449
  throw attempt.error;
10896
11450
  }
10897
11451
  }
@@ -11350,9 +11904,24 @@ var init_router = __esm({
11350
11904
  warnedProviders = /* @__PURE__ */ new Set();
11351
11905
  }
11352
11906
  });
11353
- function createMcpClient(name, config) {
11907
+ function createMcpClient(name, config, fetchImpl = fetch) {
11354
11908
  if (isStdio(config)) return new StdioMcpClient(name, config);
11355
- return new HttpMcpClient(name, config);
11909
+ return new HttpMcpClient(name, config, fetchImpl);
11910
+ }
11911
+ function reconnectDelay(attempt) {
11912
+ const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
11913
+ const ms = Math.floor(Math.random() * ceiling);
11914
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
11915
+ }
11916
+ function mcpTimeoutError(name, timeoutMs) {
11917
+ return new NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
11918
+ code: "mcp_timeout"
11919
+ });
11920
+ }
11921
+ function isAbortLike(cause) {
11922
+ if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
11923
+ const name = cause.name;
11924
+ return name === "TimeoutError" || name === "AbortError";
11356
11925
  }
11357
11926
  async function rpcInitialize(request) {
11358
11927
  await request("initialize", {
@@ -11382,11 +11951,16 @@ function resolveMcpCwd(configCwd) {
11382
11951
  if (path.isAbsolute(configCwd)) return configCwd;
11383
11952
  return safePathJoin(process.cwd(), configCwd);
11384
11953
  }
11385
- var BaseMcpClient, StdioMcpClient, HttpMcpClient;
11954
+ var DEFAULT_MCP_TIMEOUT_MS, MAX_STDIO_BUFFER_BYTES, RECONNECT_BASE_MS, MAX_RECONNECT_ATTEMPTS, BaseMcpClient, StdioMcpClient, HttpMcpClient;
11386
11955
  var init_client = __esm({
11387
11956
  "src/internal/mcp/client.ts"() {
11388
11957
  init_errors();
11958
+ init_env_policy();
11389
11959
  init_path_guard();
11960
+ DEFAULT_MCP_TIMEOUT_MS = 3e4;
11961
+ MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
11962
+ RECONNECT_BASE_MS = 250;
11963
+ MAX_RECONNECT_ATTEMPTS = 2;
11390
11964
  BaseMcpClient = class {
11391
11965
  initialize() {
11392
11966
  return rpcInitialize((method, params) => this.request(method, params));
@@ -11408,32 +11982,107 @@ var init_client = __esm({
11408
11982
  name;
11409
11983
  child;
11410
11984
  nextId = 1;
11985
+ // #59 — pending requests carry a reject + timer so a silent server times out
11986
+ // (typed error), a late reply after timeout is a no-op, and close() settles them.
11411
11987
  pending = /* @__PURE__ */ new Map();
11412
11988
  buffer = "";
11413
- async initialize() {
11989
+ // M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
11990
+ // unexpectedly OR times out (not via close()); the next request re-spawns with
11991
+ // backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
11992
+ // concurrent request so parallel tool dispatch after a drop awaits one handshake
11993
+ // instead of racing (or spuriously failing with mcp_not_init).
11994
+ dropped = false;
11995
+ reconnectAttempts = 0;
11996
+ reconnectPromise;
11997
+ get timeoutMs() {
11998
+ return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
11999
+ }
12000
+ /** Spawn the server child and wire stdout/stderr/error/exit handlers.
12001
+ * Shared by `initialize()` and the M2 #59 reconnect path. */
12002
+ spawnChild() {
11414
12003
  const resolvedCwd = resolveMcpCwd(this.config.cwd);
11415
12004
  const child = child_process.spawn(this.config.command, this.config.args ?? [], {
11416
12005
  cwd: resolvedCwd,
11417
- env: { ...process.env, ...this.config.env ?? {} }
12006
+ // #54 (F-H1) a third-party MCP server binary must not inherit host
12007
+ // secrets. Scrub secret-like vars by default; `config.env` still wins.
12008
+ env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
11418
12009
  });
11419
12010
  this.child = child;
11420
12011
  child.stdout.on("data", (chunk) => this.consume(chunk));
11421
12012
  child.stderr.on("data", () => void 0);
12013
+ child.stdin.on("error", () => void 0);
11422
12014
  child.on("error", () => {
11423
- for (const resolver of this.pending.values()) {
11424
- resolver({ error: { message: "MCP process crashed" } });
11425
- }
11426
- this.pending.clear();
12015
+ this.rejectAllPending(
12016
+ new NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
12017
+ );
12018
+ });
12019
+ child.on("exit", () => {
12020
+ if (this.child !== child) return;
12021
+ this.child = void 0;
12022
+ this.dropped = true;
12023
+ this.rejectAllPending(
12024
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
12025
+ );
12026
+ });
12027
+ }
12028
+ async initialize() {
12029
+ this.spawnChild();
12030
+ await super.initialize();
12031
+ }
12032
+ /** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
12033
+ * full-jitter backoff) when the client was dropped; fail fast when never
12034
+ * initialized. Concurrent callers share ONE reconnect handshake. */
12035
+ ensureConnected() {
12036
+ if (this.child !== void 0) return Promise.resolve();
12037
+ if (!this.dropped) {
12038
+ return Promise.reject(
12039
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
12040
+ );
12041
+ }
12042
+ this.reconnectPromise ??= this.reconnect().finally(() => {
12043
+ this.reconnectPromise = void 0;
11427
12044
  });
12045
+ return this.reconnectPromise;
12046
+ }
12047
+ async reconnect() {
12048
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
12049
+ throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
12050
+ }
12051
+ await reconnectDelay(this.reconnectAttempts);
12052
+ this.reconnectAttempts += 1;
12053
+ this.spawnChild();
11428
12054
  await super.initialize();
12055
+ this.dropped = false;
12056
+ this.reconnectAttempts = 0;
11429
12057
  }
11430
12058
  async close() {
11431
- if (this.child === void 0) return;
11432
- this.child.kill("SIGTERM");
12059
+ this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
12060
+ const child = this.child;
11433
12061
  this.child = void 0;
12062
+ this.dropped = false;
12063
+ child?.kill("SIGTERM");
12064
+ }
12065
+ /** Reject + clear every pending request (crash / close). @internal */
12066
+ rejectAllPending(error) {
12067
+ for (const entry of this.pending.values()) {
12068
+ clearTimeout(entry.timer);
12069
+ entry.reject(error);
12070
+ }
12071
+ this.pending.clear();
11434
12072
  }
11435
12073
  consume(chunk) {
11436
12074
  this.buffer += chunk.toString("utf8");
12075
+ if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
12076
+ this.buffer = "";
12077
+ this.rejectAllPending(
12078
+ new NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
12079
+ code: "mcp_buffer_overflow"
12080
+ })
12081
+ );
12082
+ this.child?.kill("SIGKILL");
12083
+ this.child = void 0;
12084
+ return;
12085
+ }
11437
12086
  let newlineIndex = this.buffer.indexOf("\n");
11438
12087
  while (newlineIndex !== -1) {
11439
12088
  const line = this.buffer.slice(0, newlineIndex).trim();
@@ -11450,23 +12099,47 @@ var init_client = __esm({
11450
12099
  return;
11451
12100
  }
11452
12101
  if (typeof message.id !== "number") return;
11453
- const resolver = this.pending.get(message.id);
11454
- if (resolver === void 0) return;
12102
+ const entry = this.pending.get(message.id);
12103
+ if (entry === void 0) return;
11455
12104
  this.pending.delete(message.id);
11456
- resolver(message);
12105
+ clearTimeout(entry.timer);
12106
+ entry.resolve(message);
11457
12107
  }
11458
12108
  request(method, params) {
11459
- if (this.child === void 0) {
11460
- return Promise.reject(
11461
- new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
11462
- );
12109
+ const child = this.child;
12110
+ if (child !== void 0) return this.send(child, method, params);
12111
+ if (this.dropped) return this.reconnectAndRequest(method, params);
12112
+ return Promise.reject(
12113
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
12114
+ );
12115
+ }
12116
+ /** M2 #59 — reconnect a dropped client, then send. Separate async path so the
12117
+ * happy path above never pays an extra microtask tick. */
12118
+ async reconnectAndRequest(method, params) {
12119
+ await this.ensureConnected();
12120
+ const child = this.child;
12121
+ if (child === void 0) {
12122
+ throw new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
11463
12123
  }
12124
+ return this.send(child, method, params);
12125
+ }
12126
+ send(child, method, params) {
11464
12127
  const id = this.nextId++;
11465
12128
  const payload = { jsonrpc: "2.0", id, method, params };
11466
- this.child.stdin.write(`${JSON.stringify(payload)}
12129
+ child.stdin.write(`${JSON.stringify(payload)}
11467
12130
  `);
11468
- return new Promise((resolve3) => {
11469
- this.pending.set(id, resolve3);
12131
+ return new Promise((resolve3, reject) => {
12132
+ const timer = setTimeout(() => {
12133
+ this.pending.delete(id);
12134
+ reject(mcpTimeoutError(this.name, this.timeoutMs));
12135
+ this.child?.kill("SIGKILL");
12136
+ this.child = void 0;
12137
+ this.dropped = true;
12138
+ this.rejectAllPending(
12139
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
12140
+ );
12141
+ }, this.timeoutMs);
12142
+ this.pending.set(id, { resolve: resolve3, reject, timer });
11470
12143
  });
11471
12144
  }
11472
12145
  };
@@ -11492,11 +12165,20 @@ var init_client = __esm({
11492
12165
  accept: "application/json",
11493
12166
  ...this.config.headers ?? {}
11494
12167
  };
11495
- const response = await this.fetchImpl(this.config.url, {
11496
- method: "POST",
11497
- headers,
11498
- body: JSON.stringify(payload)
11499
- });
12168
+ const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
12169
+ let response;
12170
+ try {
12171
+ response = await this.fetchImpl(this.config.url, {
12172
+ method: "POST",
12173
+ headers,
12174
+ body: JSON.stringify(payload),
12175
+ // #59 — bound the request; a non-responding endpoint aborts here.
12176
+ signal: AbortSignal.timeout(timeoutMs)
12177
+ });
12178
+ } catch (cause) {
12179
+ if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
12180
+ throw cause;
12181
+ }
11500
12182
  if (!response.ok) {
11501
12183
  throw new NetworkError(`MCP ${this.name} returned ${response.status}`, {
11502
12184
  code: "mcp_http_error"
@@ -11616,11 +12298,33 @@ function resolveRunProvider(options) {
11616
12298
  );
11617
12299
  }
11618
12300
  const parsedModel = parseModelId(options.model?.id);
11619
- const inferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
11620
- const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? inferredProvider ?? detectPrimaryProvider();
11621
- const effectiveModelId = inferredProvider !== void 0 ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
12301
+ const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
12302
+ const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
12303
+ const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
12304
+ const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
11622
12305
  return { primary, effectiveModelId };
11623
12306
  }
12307
+ function inferProviderFromApiKey(apiKey) {
12308
+ if (apiKey === void 0 || apiKey.length === 0) return void 0;
12309
+ const byPrefix = [
12310
+ { provider: "openrouter", prefix: "sk-or-" },
12311
+ { provider: "anthropic", prefix: "sk-ant-" },
12312
+ { provider: "openai", prefix: "sk-" }
12313
+ ];
12314
+ for (const { provider, prefix } of byPrefix) {
12315
+ if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
12316
+ return provider;
12317
+ }
12318
+ }
12319
+ return void 0;
12320
+ }
12321
+ function mergeExplicitApiKey(pools, primary, apiKey) {
12322
+ if (apiKey === void 0 || apiKey.length === 0) return pools;
12323
+ if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
12324
+ const existing = pools?.[primary];
12325
+ if (existing !== void 0 && existing.length > 0) return pools;
12326
+ return { ...pools ?? {}, [primary]: [apiKey] };
12327
+ }
11624
12328
  function buildLoopInputs(options, runId, userText) {
11625
12329
  const maxIterations = options.sendOptions.maxIterations;
11626
12330
  if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
@@ -11631,7 +12335,11 @@ function buildLoopInputs(options, runId, userText) {
11631
12335
  }
11632
12336
  const { primary, effectiveModelId } = resolveRunProvider(options);
11633
12337
  const fallback = options.agentOptions.providers?.fallback;
11634
- const apiKeys = options.agentOptions.providers?.apiKeys;
12338
+ const apiKeys = mergeExplicitApiKey(
12339
+ options.agentOptions.providers?.apiKeys,
12340
+ primary,
12341
+ options.agentOptions.apiKey
12342
+ );
11635
12343
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
11636
12344
  const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
11637
12345
  const chain = resolveProviderChain({
@@ -11675,6 +12383,10 @@ function buildLoopInputs(options, runId, userText) {
11675
12383
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
11676
12384
  // can attach it to the LLM `fetch({ signal })` call.
11677
12385
  ...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
12386
+ // #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
12387
+ // can enable them via SendOptions (not only internal AgentLoopInputs).
12388
+ ...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
12389
+ ...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
11678
12390
  // M1-2: per-send iteration ceiling (validated above). The loop reads
11679
12391
  // inputs.maxIterations (default 8 when unset).
11680
12392
  ...maxIterations !== void 0 ? { maxIterations } : {},
@@ -11741,6 +12453,8 @@ var init_real_local_run = __esm({
11741
12453
  "src/internal/runtime/local-agent/real-local-run.ts"() {
11742
12454
  init_errors();
11743
12455
  init_loop();
12456
+ init_api_key_validator();
12457
+ init_fixture_mode();
11744
12458
  init_fallback_client();
11745
12459
  init_model_identifier();
11746
12460
  init_router();
@@ -12017,7 +12731,12 @@ async function runActiveMemory(args) {
12017
12731
  hits: []
12018
12732
  });
12019
12733
  }
12020
- const cached2 = args.cache?.get(args.userText, cfg.queryMode);
12734
+ const tenantCtx = {
12735
+ namespace: args.namespace,
12736
+ userId: args.userId,
12737
+ scope: args.scope
12738
+ };
12739
+ const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
12021
12740
  if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
12022
12741
  const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
12023
12742
  if (query.trim().length === 0) {
@@ -12102,7 +12821,12 @@ function notifyBreaker(breaker, key, status) {
12102
12821
  else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key);
12103
12822
  }
12104
12823
  async function finalize(args, queryMode, result) {
12105
- args.cache?.set(args.userText, queryMode, result);
12824
+ const tenantCtx = {
12825
+ namespace: args.namespace,
12826
+ userId: args.userId,
12827
+ scope: args.scope
12828
+ };
12829
+ args.cache?.set(args.userText, queryMode, result, tenantCtx);
12106
12830
  if (args.persistTranscripts === true && args.cwd !== void 0) {
12107
12831
  await persistActiveMemoryTranscript(args.cwd, {
12108
12832
  runId: args.runId ?? `run-${Date.now()}`,
@@ -12807,53 +13531,6 @@ var init_catalog = __esm({
12807
13531
  }
12808
13532
  });
12809
13533
 
12810
- // src/internal/memory/circuit-breaker.ts
12811
- var DEFAULT_MAX_TIMEOUTS, DEFAULT_COOLDOWN_MS2, CircuitBreaker;
12812
- var init_circuit_breaker = __esm({
12813
- "src/internal/memory/circuit-breaker.ts"() {
12814
- DEFAULT_MAX_TIMEOUTS = 3;
12815
- DEFAULT_COOLDOWN_MS2 = 6e4;
12816
- CircuitBreaker = class {
12817
- constructor(opts = {}) {
12818
- this.opts = opts;
12819
- }
12820
- opts;
12821
- states = /* @__PURE__ */ new Map();
12822
- /** @returns true when the breaker is open and the call should be skipped. */
12823
- shouldSkip(key) {
12824
- const state2 = this.states.get(key);
12825
- if (state2 === void 0) return false;
12826
- if (state2.cooldownUntilMs === 0) return false;
12827
- if (this.now() < state2.cooldownUntilMs) return true;
12828
- state2.cooldownUntilMs = 0;
12829
- state2.consecutiveTimeouts = 0;
12830
- return false;
12831
- }
12832
- recordSuccess(key) {
12833
- const state2 = this.states.get(key);
12834
- if (state2 === void 0) return;
12835
- state2.consecutiveTimeouts = 0;
12836
- state2.cooldownUntilMs = 0;
12837
- }
12838
- recordTimeout(key) {
12839
- const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12840
- state2.consecutiveTimeouts += 1;
12841
- if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
12842
- state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
12843
- }
12844
- this.states.set(key, state2);
12845
- }
12846
- /** @internal — tests inspect counter state. */
12847
- inspect(key) {
12848
- return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12849
- }
12850
- now() {
12851
- return this.opts.now?.() ?? Date.now();
12852
- }
12853
- };
12854
- }
12855
- });
12856
-
12857
13534
  // src/internal/persistence/fts5-sanitize.ts
12858
13535
  function sanitizeFts5Query(query) {
12859
13536
  if (query.length === 0) return query;
@@ -14019,9 +14696,9 @@ var init_local_agent_memory = __esm({
14019
14696
  init_active_memory();
14020
14697
  init_active_memory_cache();
14021
14698
  init_catalog();
14022
- init_circuit_breaker();
14023
14699
  init_index_manager();
14024
14700
  init_tools();
14701
+ init_circuit_breaker();
14025
14702
  LocalAgentMemory = class {
14026
14703
  constructor(options, workspaceCwd, agentId) {
14027
14704
  this.options = options;