@theokit/sdk 2.15.2 → 2.18.1

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 +90 -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 +14 -14
package/dist/index.cjs CHANGED
@@ -1201,6 +1201,16 @@ var init_credential_pool_types = __esm({
1201
1201
  });
1202
1202
 
1203
1203
  // src/internal/llm/retry.ts
1204
+ function computeBackoffMs(opts) {
1205
+ const base = opts.baseMs ?? DEFAULT_BASE_MS;
1206
+ const cap2 = opts.capMs ?? DEFAULT_CAP_MS;
1207
+ const rng = opts.rng ?? Math.random;
1208
+ if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
1209
+ return Math.max(base, Math.min(cap2, opts.retryAfterMs));
1210
+ }
1211
+ const ceiling = Math.min(cap2, base * 2 ** opts.attempt);
1212
+ return Math.floor(rng() * ceiling);
1213
+ }
1204
1214
  function sleepWithAbort(ms, signal) {
1205
1215
  if (ms <= 0 || signal.aborted) return Promise.resolve();
1206
1216
  return new Promise((resolve3) => {
@@ -1215,8 +1225,11 @@ function sleepWithAbort(ms, signal) {
1215
1225
  signal.addEventListener("abort", onAbort, { once: true });
1216
1226
  });
1217
1227
  }
1228
+ var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
1218
1229
  var init_retry = __esm({
1219
1230
  "src/internal/llm/retry.ts"() {
1231
+ DEFAULT_BASE_MS = 500;
1232
+ DEFAULT_CAP_MS = 32e3;
1220
1233
  }
1221
1234
  });
1222
1235
 
@@ -5283,7 +5296,7 @@ function abortRun(name, runId, startedAt, stepResults, signal) {
5283
5296
  }
5284
5297
  async function runStepsLoop(params) {
5285
5298
  const { options, steps, ctx, runId, startedAt, signal } = params;
5286
- const stepResults = [];
5299
+ const stepResults = [...params.initialStepResults ?? []];
5287
5300
  let acc = params.input;
5288
5301
  for (const step of steps) {
5289
5302
  if (signal.aborted) return abortRun(options.name, runId, startedAt, stepResults, signal);
@@ -5342,7 +5355,8 @@ async function executeWorkflow(options, steps, input, runOpts) {
5342
5355
  ctx,
5343
5356
  runId,
5344
5357
  startedAt,
5345
- signal
5358
+ signal,
5359
+ ...runOpts?.initialStepResults !== void 0 ? { initialStepResults: runOpts.initialStepResults } : {}
5346
5360
  });
5347
5361
  } finally {
5348
5362
  runSpan.end();
@@ -5425,7 +5439,9 @@ async function resumeWorkflow(opts) {
5425
5439
  await store.delete(opts.runId);
5426
5440
  return executeWorkflow(options, remainingSteps, resumeInput, {
5427
5441
  signal: opts.signal,
5428
- runId: opts.runId
5442
+ runId: opts.runId,
5443
+ // M3 #62 — restore prior step outputs so the resumed run is not lossy (internal seam).
5444
+ initialStepResults: snapshot.stepResults
5429
5445
  });
5430
5446
  }
5431
5447
  var init_executor = __esm({
@@ -7056,13 +7072,15 @@ function buildGitInfo() {
7056
7072
  init_errors();
7057
7073
 
7058
7074
  // src/internal/llm/sse.ts
7059
- async function* parseSseStream(body, signal) {
7075
+ init_errors();
7076
+ var DEFAULT_SSE_IDLE_MS = 6e4;
7077
+ async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
7060
7078
  if (body === null) return;
7061
7079
  const reader = body.getReader();
7062
7080
  const decoder = new TextDecoder("utf-8");
7063
7081
  const state4 = { buffer: "", event: "message", data: "" };
7064
7082
  try {
7065
- for await (const chunk of readChunks(reader, signal)) {
7083
+ for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
7066
7084
  state4.buffer += decoder.decode(chunk, { stream: true });
7067
7085
  for (const record of drainCompleteRecords(state4)) yield record;
7068
7086
  }
@@ -7072,14 +7090,38 @@ async function* parseSseStream(body, signal) {
7072
7090
  releaseReader(reader);
7073
7091
  }
7074
7092
  }
7075
- async function* readChunks(reader, signal) {
7093
+ async function* readChunks(reader, signal, idleTimeoutMs) {
7076
7094
  while (true) {
7077
7095
  if (signal.aborted) return;
7078
- const { value, done } = await reader.read();
7096
+ const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
7079
7097
  if (done) return;
7080
7098
  if (value !== void 0) yield value;
7081
7099
  }
7082
7100
  }
7101
+ function readWithIdleTimeout(reader, idleTimeoutMs) {
7102
+ if (idleTimeoutMs <= 0) return reader.read();
7103
+ return new Promise(
7104
+ (resolve3, reject) => {
7105
+ const timer = setTimeout(() => {
7106
+ reject(
7107
+ new exports.NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
7108
+ code: "stream_idle_timeout"
7109
+ })
7110
+ );
7111
+ }, idleTimeoutMs);
7112
+ reader.read().then(
7113
+ (result) => {
7114
+ clearTimeout(timer);
7115
+ resolve3(result);
7116
+ },
7117
+ (err) => {
7118
+ clearTimeout(timer);
7119
+ reject(err);
7120
+ }
7121
+ );
7122
+ }
7123
+ );
7124
+ }
7083
7125
  async function cancelReaderQuietly(reader) {
7084
7126
  try {
7085
7127
  await reader.cancel();
@@ -7773,6 +7815,9 @@ var PersonalityStore = class {
7773
7815
  }
7774
7816
  };
7775
7817
 
7818
+ // src/internal/plugins/manager.ts
7819
+ init_errors();
7820
+
7776
7821
  // src/internal/plugins/context.ts
7777
7822
  function createPluginContext() {
7778
7823
  const registrations = {
@@ -7834,6 +7879,9 @@ var PluginManager = class {
7834
7879
  memoryProviders: []
7835
7880
  };
7836
7881
  #initialized = false;
7882
+ // #68 — registrations of plugins added post-init via `register()`, keyed by
7883
+ // plugin name so a re-register REPLACES (not appends) the prior hooks.
7884
+ #byName = /* @__PURE__ */ new Map();
7837
7885
  async initialize(plugins) {
7838
7886
  if (this.#initialized) {
7839
7887
  throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
@@ -7851,6 +7899,36 @@ var PluginManager = class {
7851
7899
  await this.#dispatchPlugin(plugin);
7852
7900
  }
7853
7901
  }
7902
+ /**
7903
+ * #68 — register a single `general` plugin AFTER `initialize()` has run.
7904
+ *
7905
+ * The bulk `initialize()` is single-shot (one call per process); late
7906
+ * registration is a distinct, named operation used by adapters that install
7907
+ * a plugin per-session/per-request (e.g. the ACP permission veto, which is
7908
+ * installed once the permission mode + connection are known — after the
7909
+ * agent's own plugins were already initialized).
7910
+ *
7911
+ * Idempotent by plugin NAME: re-registering a plugin with the same name
7912
+ * REPLACES its prior hooks/tools instead of appending duplicates (the ACP
7913
+ * permission plugin is re-installed on every prompt).
7914
+ *
7915
+ * Only `general` plugins may be registered late — model-provider / memory
7916
+ * plugins are resolved during the bulk init and cannot be added afterwards.
7917
+ */
7918
+ async register(plugin) {
7919
+ if (plugin.kind !== "general") {
7920
+ throw new exports.ConfigurationError(
7921
+ `late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
7922
+ { code: "plugin_late_register_kind" }
7923
+ );
7924
+ }
7925
+ const prior = this.#byName.get(plugin.name);
7926
+ if (prior !== void 0) this.#unmerge(prior);
7927
+ const { ctx, registrations } = createPluginContext();
7928
+ await plugin.register(ctx);
7929
+ this.#byName.set(plugin.name, registrations);
7930
+ this.#merge(registrations);
7931
+ }
7854
7932
  get aggregated() {
7855
7933
  return this.#aggregated;
7856
7934
  }
@@ -7931,6 +8009,64 @@ var PluginManager = class {
7931
8009
  }
7932
8010
  }
7933
8011
  }
8012
+ // #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
8013
+ // in order (per-handler errors logged, never thrown); transform hooks fold
8014
+ // over the payload (a handler returning a value replaces it).
8015
+ /** @internal */
8016
+ async #runFireAndForget(name, ctx) {
8017
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
8018
+ try {
8019
+ await h(ctx);
8020
+ } catch (err) {
8021
+ process.stderr.write(
8022
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
8023
+ `
8024
+ );
8025
+ }
8026
+ }
8027
+ }
8028
+ /** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
8029
+ async #runTransform(name, payload, ctx) {
8030
+ let current = payload;
8031
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
8032
+ try {
8033
+ const out = await h(current, ctx);
8034
+ if (out !== void 0) current = out;
8035
+ } catch (err) {
8036
+ process.stderr.write(
8037
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
8038
+ `
8039
+ );
8040
+ }
8041
+ }
8042
+ return current;
8043
+ }
8044
+ /** #65 — fired after a tool call completes. @internal */
8045
+ runPostToolCallHooks(ctx) {
8046
+ return this.#runFireAndForget("post_tool_call", ctx);
8047
+ }
8048
+ /** #65 — fired before / after each LLM turn. @internal */
8049
+ runPreLlmCallHooks(ctx) {
8050
+ return this.#runFireAndForget("pre_llm_call", ctx);
8051
+ }
8052
+ runPostLlmCallHooks(ctx) {
8053
+ return this.#runFireAndForget("post_llm_call", ctx);
8054
+ }
8055
+ /** #65 — fired at run start / end. @internal */
8056
+ runOnSessionStartHooks(ctx) {
8057
+ return this.#runFireAndForget("on_session_start", ctx);
8058
+ }
8059
+ runOnSessionEndHooks(ctx) {
8060
+ return this.#runFireAndForget("on_session_end", ctx);
8061
+ }
8062
+ /** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
8063
+ runTransformToolResultHooks(results, ctx) {
8064
+ return this.#runTransform("transform_tool_result", results, ctx);
8065
+ }
8066
+ /** #65 — transform the LLM output text before it is consumed. @internal */
8067
+ runTransformLlmOutputHooks(output, ctx) {
8068
+ return this.#runTransform("transform_llm_output", output, ctx);
8069
+ }
7934
8070
  async #dispatchPlugin(plugin) {
7935
8071
  if (plugin.kind === "general") {
7936
8072
  const { ctx, registrations } = createPluginContext();
@@ -7958,7 +8094,29 @@ var PluginManager = class {
7958
8094
  }
7959
8095
  this.#aggregated.injected.push(...r.injected);
7960
8096
  }
8097
+ /**
8098
+ * #68 — inverse of #merge: remove a prior registration's contributions from
8099
+ * the aggregated view by object identity. Used by `register()` to replace a
8100
+ * same-named plugin's hooks/tools instead of accumulating duplicates.
8101
+ */
8102
+ #unmerge(r) {
8103
+ removeAll(this.#aggregated.tools, r.tools);
8104
+ removeAll(this.#aggregated.commands, r.commands);
8105
+ removeAll(this.#aggregated.injected, r.injected);
8106
+ for (const [hook, handlers] of r.hooks.entries()) {
8107
+ const existing = this.#aggregated.hooks.get(hook);
8108
+ if (existing === void 0) continue;
8109
+ removeAll(existing, handlers);
8110
+ if (existing.length === 0) this.#aggregated.hooks.delete(hook);
8111
+ }
8112
+ }
7961
8113
  };
8114
+ function removeAll(arr, toRemove) {
8115
+ for (const item of toRemove) {
8116
+ const idx = arr.indexOf(item);
8117
+ if (idx !== -1) arr.splice(idx, 1);
8118
+ }
8119
+ }
7962
8120
 
7963
8121
  // src/internal/telemetry/span-names.ts
7964
8122
  var SPAN_NAMES = {
@@ -7966,7 +8124,12 @@ var SPAN_NAMES = {
7966
8124
  AGENT_SEND: "agent.send",
7967
8125
  MEMORY_RECALL: "memory.recall"};
7968
8126
  var HISTOGRAM_NAMES = {
7969
- MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
8127
+ MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
8128
+ TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
8129
+ LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
8130
+ LLM_TOKENS: "theokit_llm_tokens",
8131
+ /** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
8132
+ LLM_USAGE_MISSING: "theokit_llm_usage_missing"
7970
8133
  };
7971
8134
 
7972
8135
  // src/internal/telemetry/tracer.ts
@@ -8304,7 +8467,25 @@ function createTelemetry(settings) {
8304
8467
  enabled: true,
8305
8468
  includeContent: settings.includeContent === true,
8306
8469
  startSpan: startNewSpan,
8307
- startChildSpan: (_parent, name, attrs) => startNewSpan(name, attrs),
8470
+ // M3 #64 actually nest the child under its parent instead of discarding it.
8471
+ // The parent's SpanContext is set on a fresh OTel context so the child links
8472
+ // to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
8473
+ // back to a root span when the parent has no valid span id (telemetry off /
8474
+ // NOOP), preserving the pre-M3 behavior for parentless callers.
8475
+ startChildSpan: (parent, name, attrs) => {
8476
+ const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
8477
+ const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
8478
+ const pctx = safe(() => parent?.spanContext(), void 0);
8479
+ const span = safe(() => {
8480
+ if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
8481
+ const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
8482
+ return tracer.startSpan(name, opts, childCtx);
8483
+ }
8484
+ return tracer.startSpan(name, opts);
8485
+ }, NOOP_SPAN);
8486
+ if (span !== NOOP_SPAN) openSpans.add(span);
8487
+ return wrapSpan(span, openSpans);
8488
+ },
8308
8489
  recordHistogram,
8309
8490
  endAll: () => {
8310
8491
  for (const span of openSpans) safe(() => span.end(), void 0);
@@ -8340,12 +8521,62 @@ function redactAttrs(attrs) {
8340
8521
  }
8341
8522
  return out;
8342
8523
  }
8524
+
8525
+ // src/internal/runtime/lifecycle/env-policy.ts
8526
+ var SECRET_PATTERNS = [
8527
+ /KEY/i,
8528
+ /SECRET/i,
8529
+ /TOKEN/i,
8530
+ /PASSWORD/i,
8531
+ /PASSWD/i,
8532
+ /PASSPHRASE/i,
8533
+ /[_-]PWD/i,
8534
+ /CREDENTIAL/i,
8535
+ /PRIVATE/i,
8536
+ /_AUTH/i
8537
+ ];
8538
+ var CORE_VARS = [
8539
+ "PATH",
8540
+ "HOME",
8541
+ "SHELL",
8542
+ "LANG",
8543
+ "LC_ALL",
8544
+ "LC_CTYPE",
8545
+ "TMPDIR",
8546
+ "TMP",
8547
+ "TEMP",
8548
+ "USER",
8549
+ "LOGNAME"
8550
+ ];
8551
+ function isSecretName(name) {
8552
+ return SECRET_PATTERNS.some((re) => re.test(name));
8553
+ }
8554
+ function inheritsUnderPolicy(name, policy) {
8555
+ if (policy === "all") return true;
8556
+ if (policy === "core") return CORE_VARS.includes(name);
8557
+ return !isSecretName(name);
8558
+ }
8559
+ function resolveChildEnv(options = {}) {
8560
+ const parent = options.parent ?? process.env;
8561
+ const policy = options.policy ?? "inherit-scrubbed";
8562
+ const base = {};
8563
+ for (const [name, value] of Object.entries(parent)) {
8564
+ if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
8565
+ }
8566
+ for (const [name, value] of Object.entries(options.overrides ?? {})) {
8567
+ base[name] = value;
8568
+ }
8569
+ return base;
8570
+ }
8571
+
8572
+ // src/internal/runtime/lifecycle/spawn-collect.ts
8343
8573
  function spawnAndCollect(options) {
8344
8574
  return new Promise((resolve3) => {
8345
8575
  const timeoutMs = options.timeoutMs ?? 3e4;
8346
8576
  const spawnOptions = {
8347
8577
  cwd: options.cwd,
8348
- env: { ...process.env, ...options.env ?? {} }
8578
+ // #54 — scrub secret-like parent env by default; `options.env` still wins.
8579
+ env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
8349
8580
  };
8350
8581
  const child = child_process.spawn(options.command, options.args ?? [], spawnOptions);
8351
8582
  let stdout = "";
@@ -8527,15 +8758,24 @@ function warnMalformed(agentId, line) {
8527
8758
  `
8528
8759
  );
8529
8760
  }
8761
+ function hydrateSessionLine(parsed) {
8762
+ if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
8763
+ if (parsed.role === "user" || parsed.role === "assistant") {
8764
+ return { role: parsed.role, text: parsed.text };
8765
+ }
8766
+ if (parsed.role === "tool_call" || parsed.role === "tool_result") {
8767
+ const label = parsed.role === "tool_call" ? "tool call" : "tool result";
8768
+ return { role: "assistant", text: `[${label}] ${parsed.text}` };
8769
+ }
8770
+ return void 0;
8771
+ }
8530
8772
  async function readSessionFile(cwd, agentId) {
8531
8773
  const lines = await readJsonlLines(cwd, agentId);
8532
8774
  const messages = [];
8533
8775
  for (const line of lines) {
8534
8776
  try {
8535
- const parsed = JSON.parse(line);
8536
- if ((parsed.role === "user" || parsed.role === "assistant") && typeof parsed.text === "string") {
8537
- messages.push({ role: parsed.role, text: parsed.text });
8538
- }
8777
+ const msg = hydrateSessionLine(JSON.parse(line));
8778
+ if (msg !== void 0) messages.push(msg);
8539
8779
  } catch {
8540
8780
  warnMalformed(agentId, line);
8541
8781
  }
@@ -8562,28 +8802,78 @@ async function readAllPersistedMessages(cwd, agentId) {
8562
8802
  return messages;
8563
8803
  }
8564
8804
  async function appendAnyPersistedMessage(cwd, agentId, record) {
8805
+ await appendPersistedMessages(cwd, agentId, [record]);
8806
+ }
8807
+ async function appendPersistedMessages(cwd, agentId, records) {
8808
+ if (records.length === 0) return;
8565
8809
  const path$1 = sessionFilePath(cwd, agentId);
8566
- await promises.mkdir(path.dirname(path$1), { recursive: true });
8567
- await promises.appendFile(path$1, `${redactSecrets(JSON.stringify(record))}
8568
- `, "utf8");
8810
+ const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
8811
+ `).join("");
8812
+ const dir = path.dirname(path$1);
8813
+ let written = false;
8814
+ const attempt = async () => {
8815
+ await promises.mkdir(dir, { recursive: true });
8816
+ await withFileLock(path$1, async () => {
8817
+ await promises.appendFile(path$1, payload, "utf8");
8818
+ written = true;
8819
+ });
8820
+ };
8821
+ try {
8822
+ await attempt();
8823
+ } catch (cause) {
8824
+ if (written || cause.code !== "ENOENT") throw cause;
8825
+ await attempt();
8826
+ }
8827
+ }
8828
+ async function rewriteLockedSession(path, transform) {
8829
+ await withFileLock(path, async () => {
8830
+ let raw;
8831
+ try {
8832
+ raw = await promises.readFile(path, "utf8");
8833
+ } catch {
8834
+ return;
8835
+ }
8836
+ const lines = raw.split("\n").filter((line) => line.length > 0);
8837
+ const next = transform(lines);
8838
+ if (next === void 0) return;
8839
+ await replaceFileAtomic(path, next);
8840
+ });
8569
8841
  }
8570
8842
  async function compactSessionFile(cwd, agentId, maxTurns) {
8571
8843
  const path = sessionFilePath(cwd, agentId);
8572
- let raw;
8573
- try {
8574
- raw = await promises.readFile(path, "utf8");
8575
- } catch {
8576
- return;
8577
- }
8578
- const lines = raw.split("\n").filter((line) => line.length > 0);
8579
- if (lines.length <= maxTurns * 2) return;
8580
- const trimmed = `${lines.slice(-maxTurns).join("\n")}
8844
+ if (!fs.existsSync(path)) return;
8845
+ await rewriteLockedSession(
8846
+ path,
8847
+ (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
8848
+ `
8849
+ );
8850
+ }
8851
+ async function truncateSessionTo(cwd, agentId, keepCount) {
8852
+ const path = sessionFilePath(cwd, agentId);
8853
+ if (!fs.existsSync(path)) return 0;
8854
+ let kept = 0;
8855
+ await rewriteLockedSession(path, (lines) => {
8856
+ const keep = Math.max(0, Math.min(keepCount, lines.length));
8857
+ kept = keep;
8858
+ if (keep === lines.length) return void 0;
8859
+ return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
8581
8860
  `;
8582
- await replaceFileAtomic(path, trimmed);
8861
+ });
8862
+ return kept;
8583
8863
  }
8584
8864
 
8585
8865
  // src/internal/persistence/conversation-storage-fs.ts
8586
8866
  init_security();
8867
+
8868
+ // src/internal/persistence/pagination.ts
8869
+ function paginate(items, opts) {
8870
+ if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
8871
+ const start = Math.max(0, opts.offset ?? 0);
8872
+ const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
8873
+ return items.slice(start, end);
8874
+ }
8875
+
8876
+ // src/internal/persistence/conversation-storage-fs.ts
8587
8877
  var FileSystemConversationStorage = class {
8588
8878
  #root;
8589
8879
  constructor(opts = {}) {
@@ -8593,23 +8883,31 @@ var FileSystemConversationStorage = class {
8593
8883
  get root() {
8594
8884
  return this.#root;
8595
8885
  }
8596
- async getMessages(conversationId) {
8886
+ async getMessages(conversationId, opts) {
8597
8887
  const records = await readAllPersistedMessages(this.#root, conversationId);
8598
- return records.map(toStoredMessage);
8888
+ const all = records.map(toStoredMessage);
8889
+ return paginate(all, opts);
8599
8890
  }
8600
8891
  async appendMessage(conversationId, message) {
8601
- const record = {
8602
- role: message.role,
8603
- text: message.content,
8604
- at: message.at ?? Date.now()
8605
- };
8606
- await appendAnyPersistedMessage(this.#root, conversationId, record);
8892
+ await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
8893
+ }
8894
+ async appendMessages(conversationId, messages) {
8895
+ await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
8896
+ }
8897
+ async truncateConversation(conversationId, keepCount) {
8898
+ return truncateSessionTo(this.#root, conversationId, keepCount);
8607
8899
  }
8608
8900
  async deleteConversation(conversationId) {
8609
8901
  const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
8610
8902
  const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
8611
8903
  await promises.rm(dirPath, { recursive: true, force: true });
8612
8904
  }
8905
+ async deleteScope(prefix) {
8906
+ const ids = await this.listConversationIds();
8907
+ const matching = ids.filter((id) => id.startsWith(prefix));
8908
+ for (const id of matching) await this.deleteConversation(id);
8909
+ return matching.length;
8910
+ }
8613
8911
  async listConversationIds(opts = {}) {
8614
8912
  const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
8615
8913
  let entries;
@@ -8635,6 +8933,9 @@ function toStoredMessage(record) {
8635
8933
  at: record.at
8636
8934
  };
8637
8935
  }
8936
+ function toRecord(message) {
8937
+ return { role: message.role, text: message.content, at: message.at ?? Date.now() };
8938
+ }
8638
8939
 
8639
8940
  // src/internal/runtime/session/agent-session.ts
8640
8941
  var DEFAULT_MAX_TURNS = 200;
@@ -8720,12 +9021,19 @@ async function readPersistedForCache(adapter, agentId) {
8720
9021
  const records = await adapter.getMessages(agentId);
8721
9022
  const out = [];
8722
9023
  for (const r of records) {
8723
- if (r.role === "user" || r.role === "assistant") {
8724
- out.push({ role: r.role, text: r.content });
8725
- }
9024
+ const folded = foldStoredToSession(r);
9025
+ if (folded !== void 0) out.push(folded);
8726
9026
  }
8727
9027
  return out;
8728
9028
  }
9029
+ function foldStoredToSession(r) {
9030
+ if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
9031
+ if (r.role === "tool_call" || r.role === "tool_result") {
9032
+ const label = r.role === "tool_call" ? "tool call" : "tool result";
9033
+ return { role: "assistant", text: `[${label}] ${r.content}` };
9034
+ }
9035
+ return void 0;
9036
+ }
8729
9037
  async function flushSessionWrites() {
8730
9038
  while (pendingAppends.size > 0) {
8731
9039
  const all = Array.from(pendingAppends.values());
@@ -11077,11 +11385,32 @@ function reasoningEffortFromParams(params) {
11077
11385
  const thinking = params?.find((p) => p.id === "thinking");
11078
11386
  return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
11079
11387
  }
11388
+ function emitLlmMetrics(inputs, result, startAt) {
11389
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
11390
+ provider: inputs.llm.name
11391
+ });
11392
+ if (result.inputTokens === void 0 && result.outputTokens === void 0) {
11393
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
11394
+ provider: inputs.llm.name
11395
+ });
11396
+ process.stderr.write(
11397
+ `[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
11398
+ `
11399
+ );
11400
+ return;
11401
+ }
11402
+ inputs.telemetry?.recordHistogram(
11403
+ HISTOGRAM_NAMES.LLM_TOKENS,
11404
+ (result.inputTokens ?? 0) + (result.outputTokens ?? 0),
11405
+ { provider: inputs.llm.name }
11406
+ );
11407
+ }
11080
11408
  async function streamLlmTurn(inputs, ctx) {
11081
- const llmSpan = inputs.telemetry?.startSpan("llm.call", {
11409
+ const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
11082
11410
  "model.id": inputs.model.id ?? "auto",
11083
11411
  provider: inputs.llm.name
11084
11412
  });
11413
+ const startAt = Date.now();
11085
11414
  const signal = inputs.signal ?? new AbortController().signal;
11086
11415
  const generator = inputs.llm.stream(
11087
11416
  {
@@ -11124,6 +11453,7 @@ async function streamLlmTurn(inputs, ctx) {
11124
11453
  inputTokens: result.inputTokens ?? 0,
11125
11454
  outputTokens: result.outputTokens ?? 0
11126
11455
  });
11456
+ emitLlmMetrics(inputs, result, startAt);
11127
11457
  llmSpan?.end();
11128
11458
  const stripped = stripThinkBlocks(collected.accumulatedText);
11129
11459
  return {
@@ -11353,21 +11683,21 @@ async function executeTool(inputs, resolved, call) {
11353
11683
  }
11354
11684
  if (resolved.origin === "shell") return runShellTool(inputs, call);
11355
11685
  if (resolved.origin === "memory") return runMemoryTool(resolved, call);
11356
- if (resolved.origin === "custom") return runCustomTool(resolved, call);
11686
+ if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
11357
11687
  return runMcpTool(inputs, resolved, call);
11358
11688
  }
11359
11689
  async function runMemoryTool(resolved, call) {
11360
11690
  return runHandlerTool("memory", resolved.memoryHandler, call);
11361
11691
  }
11362
- async function runCustomTool(resolved, call) {
11363
- return runHandlerTool("custom", resolved.customHandler, call);
11692
+ async function runCustomTool(resolved, call, signal) {
11693
+ return runHandlerTool("custom", resolved.customHandler, call, signal);
11364
11694
  }
11365
- async function runHandlerTool(kind, handler, call) {
11695
+ async function runHandlerTool(kind, handler, call, signal) {
11366
11696
  if (handler === void 0) {
11367
11697
  return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
11368
11698
  }
11369
11699
  try {
11370
- const stdout = await handler(call.input);
11700
+ const stdout = await handler(call.input, { signal });
11371
11701
  return { stdout, stderr: "", exitCode: 0 };
11372
11702
  } catch (cause) {
11373
11703
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -11413,22 +11743,66 @@ ${result.stderr}`.trim();
11413
11743
  return result.stdout.trim();
11414
11744
  }
11415
11745
 
11746
+ // src/internal/agent-loop/tool-timeout.ts
11747
+ var TOOL_ABORTED_EXIT = 124;
11748
+ function abortedResult(signal) {
11749
+ const reason = signal.reason;
11750
+ const timedOut = reason?.name === "TimeoutError";
11751
+ return {
11752
+ stdout: "",
11753
+ stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
11754
+ exitCode: TOOL_ABORTED_EXIT
11755
+ };
11756
+ }
11757
+ function raceToolExecution(exec, opts) {
11758
+ const { signal, timeoutMs } = opts;
11759
+ if (signal === void 0 && timeoutMs === void 0) return exec;
11760
+ const signals = [];
11761
+ if (signal !== void 0) signals.push(signal);
11762
+ if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
11763
+ const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
11764
+ if (merged.aborted) return Promise.resolve(abortedResult(merged));
11765
+ return new Promise((resolve3, reject) => {
11766
+ let settled = false;
11767
+ const onAbort = () => {
11768
+ if (settled) return;
11769
+ settled = true;
11770
+ resolve3(abortedResult(merged));
11771
+ };
11772
+ merged.addEventListener("abort", onAbort, { once: true });
11773
+ exec.then(
11774
+ (r) => {
11775
+ if (settled) return;
11776
+ settled = true;
11777
+ merged.removeEventListener("abort", onAbort);
11778
+ resolve3(r);
11779
+ },
11780
+ (e) => {
11781
+ if (settled) return;
11782
+ settled = true;
11783
+ merged.removeEventListener("abort", onAbort);
11784
+ reject(e);
11785
+ }
11786
+ );
11787
+ });
11788
+ }
11789
+
11416
11790
  // src/internal/agent-loop/tool-dispatch.ts
11417
- async function dispatchTools(inputs, tools, toolCalls, events) {
11791
+ async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
11418
11792
  const maxConcurrent = inputs.maxConcurrentTools ?? 4;
11419
11793
  return mapWithConcurrency(
11420
11794
  toolCalls,
11421
11795
  maxConcurrent,
11422
- (call) => dispatchSingleCall(inputs, tools, call, events)
11796
+ (call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
11423
11797
  );
11424
11798
  }
11425
- async function dispatchSingleCall(inputs, tools, call, events) {
11799
+ async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
11426
11800
  const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
11427
11801
  const callId = generateCallId();
11428
11802
  const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
11429
11803
  if (forkVeto !== void 0) return forkVeto;
11430
11804
  const resolved = tools.find((tool) => tool.name === workingCall.name);
11431
- const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
11805
+ const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
11432
11806
  events.push(buildToolUseRunning(inputs, callId, workingCall));
11433
11807
  const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
11434
11808
  if (pluginVeto !== void 0) {
@@ -11445,6 +11819,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
11445
11819
  return fileVeto;
11446
11820
  }
11447
11821
  const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
11822
+ await inputs.pluginManager?.runPostToolCallHooks({
11823
+ name: workingCall.name,
11824
+ args: workingCall.input,
11825
+ result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
11826
+ agentId: inputs.agentId,
11827
+ runId: inputs.runId
11828
+ });
11448
11829
  return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
11449
11830
  }
11450
11831
  function applyRepairAndExtractCall(tools, call) {
@@ -11477,8 +11858,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
11477
11858
  content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
11478
11859
  };
11479
11860
  }
11480
- function startToolCallSpan(inputs, call, resolved, callId, repairs) {
11481
- const toolSpan = inputs.telemetry?.startSpan("tool.call", {
11861
+ function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
11862
+ const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
11482
11863
  "tool.name": call.name,
11483
11864
  "tool.origin": resolved?.origin ?? "unknown",
11484
11865
  callId
@@ -11542,8 +11923,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
11542
11923
  conversationId: inputs.agentId,
11543
11924
  callId
11544
11925
  });
11545
- const result = await executeTool(inputs, resolved, call);
11926
+ const result = await raceToolExecution(executeTool(inputs, resolved, call), {
11927
+ signal: inputs.signal,
11928
+ timeoutMs: inputs.perToolTimeoutMs
11929
+ });
11546
11930
  const durationMs = Date.now() - startAt;
11931
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
11932
+ "tool.name": call.name
11933
+ });
11547
11934
  if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
11548
11935
  await safeEmitToolHook(inputs.onToolError, {
11549
11936
  toolName: call.name,
@@ -11642,6 +12029,35 @@ function buildToolUseCompleted(inputs, callId, call, result) {
11642
12029
  };
11643
12030
  }
11644
12031
 
12032
+ // src/internal/agent-loop/tool-result-guard.ts
12033
+ var OPEN = "<untrusted-tool-output>";
12034
+ var CLOSE = "</untrusted-tool-output>";
12035
+ var PII_PATTERNS = [
12036
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
12037
+ // email
12038
+ /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
12039
+ // phone
12040
+ ];
12041
+ function guardText(content, opts) {
12042
+ let out = content;
12043
+ if (opts.redactPii === true) {
12044
+ for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
12045
+ }
12046
+ if (opts.delimit === true) {
12047
+ const safe2 = out.split(CLOSE).join("</ untrusted-tool-output>");
12048
+ out = `${OPEN}
12049
+ ${safe2}
12050
+ ${CLOSE}`;
12051
+ }
12052
+ return out;
12053
+ }
12054
+ function applyToolResultGuard(parts, opts) {
12055
+ if (opts.delimit !== true && opts.redactPii !== true) return parts;
12056
+ return parts.map(
12057
+ (p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
12058
+ );
12059
+ }
12060
+
11645
12061
  // src/internal/budget/pricing-data.json
11646
12062
  var pricing_data_default = {
11647
12063
  _meta: {
@@ -11992,6 +12408,11 @@ async function runAgentLoop(inputs) {
11992
12408
  try {
11993
12409
  const ctx = await initLoopContext(inputs);
11994
12410
  ctxRef = ctx;
12411
+ ctx.sendSpan = sendSpan;
12412
+ await inputs.pluginManager?.runOnSessionStartHooks({
12413
+ agentId: inputs.agentId,
12414
+ runId: inputs.runId
12415
+ });
11995
12416
  const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
11996
12417
  let lastTurnDecision;
11997
12418
  while (budget.shouldContinue()) {
@@ -12019,6 +12440,7 @@ async function runAgentLoop(inputs) {
12019
12440
  }
12020
12441
  budget.consume();
12021
12442
  inputs.budgetTracker?.nextIteration?.();
12443
+ if (inputs.signal?.aborted === true) break;
12022
12444
  }
12023
12445
  if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
12024
12446
  ctx.stoppedAtIterationLimit = true;
@@ -12058,6 +12480,10 @@ async function runAgentLoop(inputs) {
12058
12480
  ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
12059
12481
  };
12060
12482
  } finally {
12483
+ await inputs.pluginManager?.runOnSessionEndHooks({
12484
+ agentId: inputs.agentId,
12485
+ runId: inputs.runId
12486
+ });
12061
12487
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
12062
12488
  try {
12063
12489
  await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
@@ -12161,7 +12587,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
12161
12587
  return "done";
12162
12588
  }
12163
12589
  async function runIteration(inputs, ctx) {
12590
+ const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
12591
+ await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
12164
12592
  const llmOutput = await streamLlmTurn(inputs, ctx);
12593
+ await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
12165
12594
  accumulateUsage(ctx.usage, llmOutput);
12166
12595
  if (inputs.budgetTracker !== void 0) {
12167
12596
  const modelId = inputs.model.id ?? "auto";
@@ -12187,6 +12616,13 @@ async function runIteration(inputs, ctx) {
12187
12616
  }
12188
12617
  return continueOrTerminate(inputs, ctx, llmOutput);
12189
12618
  }
12619
+ async function transformLlmOutputText(inputs, text, ctx) {
12620
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
12621
+ }
12622
+ async function guardAndTransformToolResults(inputs, raw, ctx) {
12623
+ const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
12624
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
12625
+ }
12190
12626
  async function continueOrTerminate(inputs, ctx, llmOutput) {
12191
12627
  if (llmOutput.errored) return "error";
12192
12628
  if (llmOutput.text.length > 0) {
@@ -12195,8 +12631,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
12195
12631
  if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
12196
12632
  return finishOrReflect(inputs, ctx, llmOutput);
12197
12633
  }
12198
- ctx.messages.push(buildAssistantTurn(llmOutput.text, llmOutput.toolCalls));
12199
- const toolResults = await dispatchTools(inputs, ctx.tools, llmOutput.toolCalls, ctx.events);
12634
+ const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
12635
+ const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
12636
+ ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
12637
+ const rawResults = await dispatchTools(
12638
+ inputs,
12639
+ ctx.tools,
12640
+ llmOutput.toolCalls,
12641
+ ctx.events,
12642
+ ctx.sendSpan
12643
+ // M3 #64 — nest tool.call spans under agent.send
12644
+ );
12645
+ const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
12200
12646
  ctx.messages.push({ role: "user", content: toolResults });
12201
12647
  if (inputs.onStep !== void 0) {
12202
12648
  const cb = inputs.onStep;
@@ -12641,6 +13087,56 @@ function mapAnthropicStatusToCode(status, body) {
12641
13087
  function formatMessage(status, code) {
12642
13088
  return `Anthropic API error: ${code} (HTTP ${status})`;
12643
13089
  }
13090
+ var cachedJsonrepair;
13091
+ function loadJsonrepair() {
13092
+ if (cachedJsonrepair === void 0) {
13093
+ 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)));
13094
+ cachedJsonrepair = req("jsonrepair").jsonrepair;
13095
+ }
13096
+ return cachedJsonrepair;
13097
+ }
13098
+ function isPlainObject(v) {
13099
+ return v !== null && typeof v === "object" && !Array.isArray(v);
13100
+ }
13101
+ function toFiniteNumber(raw) {
13102
+ if (raw === "") return void 0;
13103
+ const n = Number(raw);
13104
+ return Number.isFinite(n) && String(n) === raw ? n : void 0;
13105
+ }
13106
+ function tryJson(raw, repair) {
13107
+ const t = raw.trimStart();
13108
+ if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
13109
+ try {
13110
+ return JSON.parse(repair ? loadJsonrepair()(t) : t);
13111
+ } catch {
13112
+ return void 0;
13113
+ }
13114
+ }
13115
+ function heuristicCoerce(raw, repairJson) {
13116
+ if (raw === "true") return true;
13117
+ if (raw === "false") return false;
13118
+ if (raw === "null") return null;
13119
+ const n = toFiniteNumber(raw);
13120
+ if (n !== void 0) return n;
13121
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13122
+ return json === void 0 ? raw : json;
13123
+ }
13124
+ function coerceCandidates(raw, repairJson) {
13125
+ const out = [];
13126
+ if (raw === "true") out.push(true);
13127
+ else if (raw === "false") out.push(false);
13128
+ else if (raw === "null") out.push(null);
13129
+ const n = toFiniteNumber(raw);
13130
+ if (n !== void 0) out.push(n);
13131
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13132
+ if (json !== void 0) out.push(json);
13133
+ out.push(raw);
13134
+ return out;
13135
+ }
13136
+ function objectShape(schema) {
13137
+ const shape = schema?.shape;
13138
+ return shape !== null && typeof shape === "object" ? shape : void 0;
13139
+ }
12644
13140
 
12645
13141
  // src/internal/llm/finish.ts
12646
13142
  function collapseSystemText(system) {
@@ -12653,9 +13149,21 @@ function parseToolArguments(buffered) {
12653
13149
  try {
12654
13150
  return JSON.parse(buffered);
12655
13151
  } catch {
13152
+ const repaired = tryJson(buffered, true);
13153
+ if (isPlainObject(repaired)) return repaired;
12656
13154
  return { raw: buffered };
12657
13155
  }
12658
13156
  }
13157
+ function mapOpenAIFinish(reason) {
13158
+ switch (reason) {
13159
+ case "tool_calls":
13160
+ return "tool_use";
13161
+ case "length":
13162
+ return "max_tokens";
13163
+ default:
13164
+ return "end_turn";
13165
+ }
13166
+ }
12659
13167
  function makeLlmFinish(state4) {
12660
13168
  const finish = {
12661
13169
  stopReason: state4.stopReason,
@@ -13537,56 +14045,9 @@ function toOllamaTools(tools) {
13537
14045
  }
13538
14046
  }));
13539
14047
  }
13540
- var cachedJsonrepair;
13541
- function loadJsonrepair() {
13542
- if (cachedJsonrepair === void 0) {
13543
- 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)));
13544
- cachedJsonrepair = req("jsonrepair").jsonrepair;
13545
- }
13546
- return cachedJsonrepair;
13547
- }
13548
- function isPlainObject(v) {
13549
- return v !== null && typeof v === "object" && !Array.isArray(v);
13550
- }
13551
- function toFiniteNumber(raw) {
13552
- if (raw === "") return void 0;
13553
- const n = Number(raw);
13554
- return Number.isFinite(n) && String(n) === raw ? n : void 0;
13555
- }
13556
- function tryJson(raw, repair) {
13557
- const t = raw.trimStart();
13558
- if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
13559
- try {
13560
- return JSON.parse(repair ? loadJsonrepair()(t) : t);
13561
- } catch {
13562
- return void 0;
13563
- }
13564
- }
13565
- function heuristicCoerce(raw, repairJson) {
13566
- if (raw === "true") return true;
13567
- if (raw === "false") return false;
13568
- if (raw === "null") return null;
13569
- const n = toFiniteNumber(raw);
13570
- if (n !== void 0) return n;
13571
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13572
- return json === void 0 ? raw : json;
13573
- }
13574
- function coerceCandidates(raw, repairJson) {
13575
- const out = [];
13576
- if (raw === "true") out.push(true);
13577
- else if (raw === "false") out.push(false);
13578
- else if (raw === "null") out.push(null);
13579
- const n = toFiniteNumber(raw);
13580
- if (n !== void 0) out.push(n);
13581
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13582
- if (json !== void 0) out.push(json);
13583
- out.push(raw);
13584
- return out;
13585
- }
13586
- function objectShape(schema) {
13587
- const shape = schema?.shape;
13588
- return shape !== null && typeof shape === "object" ? shape : void 0;
13589
- }
14048
+
14049
+ // src/internal/llm/openai.ts
14050
+ init_errors();
13590
14051
 
13591
14052
  // src/sanitize/sanitize-tool-input.ts
13592
14053
  function applyTrim(key2, value, ctx) {
@@ -13831,8 +14292,12 @@ var OpenAIClient = class {
13831
14292
  // model was actually given. Empty set (no tools) recovers nothing.
13832
14293
  new Set(request.tools?.map((tool) => tool.name) ?? [])
13833
14294
  );
14295
+ let sawDone = false;
13834
14296
  for await (const record of parseSseStream(response.body, signal)) {
13835
- if (record.data === "[DONE]") break;
14297
+ if (record.data === "[DONE]") {
14298
+ sawDone = true;
14299
+ break;
14300
+ }
13836
14301
  let chunk;
13837
14302
  try {
13838
14303
  chunk = JSON.parse(record.data);
@@ -13852,6 +14317,11 @@ var OpenAIClient = class {
13852
14317
  const events = accumulator.consume(chunk);
13853
14318
  for (const event of events) yield event;
13854
14319
  }
14320
+ if (!sawDone && !accumulator.finishReasonSeen) {
14321
+ throw new exports.NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
14322
+ code: "stream_truncated"
14323
+ });
14324
+ }
13855
14325
  const drainEvent = accumulator.finalizeHeldText();
13856
14326
  if (drainEvent !== void 0) yield drainEvent;
13857
14327
  return accumulator.finish();
@@ -13951,8 +14421,15 @@ var OpenAIStreamAccumulator = class {
13951
14421
  this.toolCalls.set(call.index, existing);
13952
14422
  }
13953
14423
  }
14424
+ /** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
14425
+ * stream ending without `[DONE]` is a truncation, not a clean end). */
14426
+ sawFinishReason = false;
14427
+ get finishReasonSeen() {
14428
+ return this.sawFinishReason;
14429
+ }
13954
14430
  applyFinishReason(reason) {
13955
14431
  if (reason === void 0 || reason === null) return;
14432
+ this.sawFinishReason = true;
13956
14433
  this.stopReason = mapOpenAIFinish(reason);
13957
14434
  }
13958
14435
  finish() {
@@ -13997,18 +14474,6 @@ var OpenAIStreamAccumulator = class {
13997
14474
  });
13998
14475
  }
13999
14476
  };
14000
- function mapOpenAIFinish(reason) {
14001
- switch (reason) {
14002
- case "tool_calls":
14003
- return "tool_use";
14004
- case "length":
14005
- return "max_tokens";
14006
- case "stop":
14007
- return "end_turn";
14008
- default:
14009
- return "end_turn";
14010
- }
14011
- }
14012
14477
  function applyReasoningRequest(body, effort, providerName) {
14013
14478
  if (providerName === "openai") {
14014
14479
  body.reasoning_effort = effort;
@@ -14106,19 +14571,76 @@ function assistantMessage(message) {
14106
14571
 
14107
14572
  // src/internal/llm/pool-aware-client.ts
14108
14573
  init_errors();
14574
+
14575
+ // src/internal/resilience/circuit-breaker.ts
14576
+ var DEFAULT_MAX_TIMEOUTS = 3;
14577
+ var DEFAULT_COOLDOWN_MS2 = 6e4;
14578
+ var CircuitBreaker = class {
14579
+ constructor(opts = {}) {
14580
+ this.opts = opts;
14581
+ }
14582
+ opts;
14583
+ states = /* @__PURE__ */ new Map();
14584
+ /** @returns true when the breaker is open and the call should be skipped. */
14585
+ shouldSkip(key2) {
14586
+ const state4 = this.states.get(key2);
14587
+ if (state4 === void 0) return false;
14588
+ if (state4.cooldownUntilMs === 0) return false;
14589
+ if (this.now() < state4.cooldownUntilMs) return true;
14590
+ state4.cooldownUntilMs = 0;
14591
+ state4.consecutiveTimeouts = 0;
14592
+ return false;
14593
+ }
14594
+ recordSuccess(key2) {
14595
+ const state4 = this.states.get(key2);
14596
+ if (state4 === void 0) return;
14597
+ state4.consecutiveTimeouts = 0;
14598
+ state4.cooldownUntilMs = 0;
14599
+ }
14600
+ recordTimeout(key2) {
14601
+ const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
14602
+ state4.consecutiveTimeouts += 1;
14603
+ if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
14604
+ state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
14605
+ }
14606
+ this.states.set(key2, state4);
14607
+ }
14608
+ /** @internal — tests inspect counter state. */
14609
+ inspect(key2) {
14610
+ return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
14611
+ }
14612
+ now() {
14613
+ return this.opts.now?.() ?? Date.now();
14614
+ }
14615
+ };
14616
+
14617
+ // src/internal/llm/pool-aware-client.ts
14618
+ init_retry();
14109
14619
  var PoolAwareLlmClient = class {
14110
- constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
14620
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
14111
14621
  this.pool = pool;
14112
14622
  this.buildClient = buildClient2;
14113
14623
  this.waitForAvailableMs = waitForAvailableMs;
14114
14624
  this.name = `pool-aware:${pool.provider}`;
14625
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
14626
+ this.backoffBaseMs = resilience.backoffBaseMs;
14627
+ this.rng = resilience.rng;
14115
14628
  }
14116
14629
  pool;
14117
14630
  buildClient;
14118
14631
  waitForAvailableMs;
14119
14632
  name;
14633
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
14634
+ breaker;
14635
+ backoffBaseMs;
14636
+ rng;
14120
14637
  // 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.
14121
14638
  async *stream(request, signal) {
14639
+ if (this.breaker.shouldSkip(this.pool.provider)) {
14640
+ throw new exports.NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
14641
+ code: "circuit_open"
14642
+ });
14643
+ }
14122
14644
  let hasRetried429 = false;
14123
14645
  while (true) {
14124
14646
  if (signal.aborted) throw abortError2(signal);
@@ -14133,6 +14655,7 @@ var PoolAwareLlmClient = class {
14133
14655
  }
14134
14656
  }
14135
14657
  if (entry === null) {
14658
+ this.breaker.recordTimeout(this.pool.provider);
14136
14659
  throw new CredentialPoolExhaustedError(
14137
14660
  `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
14138
14661
  { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
@@ -14142,10 +14665,19 @@ var PoolAwareLlmClient = class {
14142
14665
  const realClient = this.buildClient(entry.accessToken);
14143
14666
  const attempt = await tryFirstEvent(realClient, request, signal);
14144
14667
  if (attempt.kind === "ok") {
14668
+ this.breaker.recordSuccess(this.pool.provider);
14145
14669
  return yield* relayStream(attempt.generator, attempt.firstResult);
14146
14670
  }
14147
14671
  const decision = classifyAndDecide(attempt.error, hasRetried429);
14148
14672
  if (decision === "retry") {
14673
+ await sleepWithAbort(
14674
+ computeBackoffMs({
14675
+ attempt: 0,
14676
+ ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
14677
+ ...this.rng !== void 0 ? { rng: this.rng } : {}
14678
+ }),
14679
+ signal
14680
+ );
14149
14681
  hasRetried429 = true;
14150
14682
  continue;
14151
14683
  }
@@ -14165,6 +14697,7 @@ var PoolAwareLlmClient = class {
14165
14697
  hasRetried429 = false;
14166
14698
  continue;
14167
14699
  }
14700
+ this.breaker.recordTimeout(this.pool.provider);
14168
14701
  throw attempt.error;
14169
14702
  }
14170
14703
  }
@@ -14600,9 +15133,28 @@ function selectTransport(profile, apiKey) {
14600
15133
  // src/internal/mcp/client.ts
14601
15134
  init_errors();
14602
15135
  init_path_guard();
14603
- function createMcpClient(name, config) {
15136
+ function createMcpClient(name, config, fetchImpl = fetch) {
14604
15137
  if (isStdio(config)) return new StdioMcpClient(name, config);
14605
- return new HttpMcpClient(name, config);
15138
+ return new HttpMcpClient(name, config, fetchImpl);
15139
+ }
15140
+ var DEFAULT_MCP_TIMEOUT_MS = 3e4;
15141
+ var MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
15142
+ var RECONNECT_BASE_MS = 250;
15143
+ var MAX_RECONNECT_ATTEMPTS = 2;
15144
+ function reconnectDelay(attempt) {
15145
+ const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
15146
+ const ms = Math.floor(Math.random() * ceiling);
15147
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
15148
+ }
15149
+ function mcpTimeoutError(name, timeoutMs) {
15150
+ return new exports.NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
15151
+ code: "mcp_timeout"
15152
+ });
15153
+ }
15154
+ function isAbortLike(cause) {
15155
+ if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
15156
+ const name = cause.name;
15157
+ return name === "TimeoutError" || name === "AbortError";
14606
15158
  }
14607
15159
  async function rpcInitialize(request) {
14608
15160
  await request("initialize", {
@@ -14648,32 +15200,107 @@ var StdioMcpClient = class extends BaseMcpClient {
14648
15200
  name;
14649
15201
  child;
14650
15202
  nextId = 1;
15203
+ // #59 — pending requests carry a reject + timer so a silent server times out
15204
+ // (typed error), a late reply after timeout is a no-op, and close() settles them.
14651
15205
  pending = /* @__PURE__ */ new Map();
14652
15206
  buffer = "";
14653
- async initialize() {
15207
+ // M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
15208
+ // unexpectedly OR times out (not via close()); the next request re-spawns with
15209
+ // backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
15210
+ // concurrent request so parallel tool dispatch after a drop awaits one handshake
15211
+ // instead of racing (or spuriously failing with mcp_not_init).
15212
+ dropped = false;
15213
+ reconnectAttempts = 0;
15214
+ reconnectPromise;
15215
+ get timeoutMs() {
15216
+ return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
15217
+ }
15218
+ /** Spawn the server child and wire stdout/stderr/error/exit handlers.
15219
+ * Shared by `initialize()` and the M2 #59 reconnect path. */
15220
+ spawnChild() {
14654
15221
  const resolvedCwd = resolveMcpCwd(this.config.cwd);
14655
15222
  const child = child_process.spawn(this.config.command, this.config.args ?? [], {
14656
15223
  cwd: resolvedCwd,
14657
- env: { ...process.env, ...this.config.env ?? {} }
15224
+ // #54 (F-H1) a third-party MCP server binary must not inherit host
15225
+ // secrets. Scrub secret-like vars by default; `config.env` still wins.
15226
+ env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
14658
15227
  });
14659
15228
  this.child = child;
14660
15229
  child.stdout.on("data", (chunk) => this.consume(chunk));
14661
15230
  child.stderr.on("data", () => void 0);
15231
+ child.stdin.on("error", () => void 0);
14662
15232
  child.on("error", () => {
14663
- for (const resolver of this.pending.values()) {
14664
- resolver({ error: { message: "MCP process crashed" } });
14665
- }
14666
- this.pending.clear();
15233
+ this.rejectAllPending(
15234
+ new exports.NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
15235
+ );
15236
+ });
15237
+ child.on("exit", () => {
15238
+ if (this.child !== child) return;
15239
+ this.child = void 0;
15240
+ this.dropped = true;
15241
+ this.rejectAllPending(
15242
+ new exports.NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
15243
+ );
14667
15244
  });
15245
+ }
15246
+ async initialize() {
15247
+ this.spawnChild();
14668
15248
  await super.initialize();
14669
15249
  }
15250
+ /** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
15251
+ * full-jitter backoff) when the client was dropped; fail fast when never
15252
+ * initialized. Concurrent callers share ONE reconnect handshake. */
15253
+ ensureConnected() {
15254
+ if (this.child !== void 0) return Promise.resolve();
15255
+ if (!this.dropped) {
15256
+ return Promise.reject(
15257
+ new exports.ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
15258
+ );
15259
+ }
15260
+ this.reconnectPromise ??= this.reconnect().finally(() => {
15261
+ this.reconnectPromise = void 0;
15262
+ });
15263
+ return this.reconnectPromise;
15264
+ }
15265
+ async reconnect() {
15266
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
15267
+ throw new exports.NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
15268
+ }
15269
+ await reconnectDelay(this.reconnectAttempts);
15270
+ this.reconnectAttempts += 1;
15271
+ this.spawnChild();
15272
+ await super.initialize();
15273
+ this.dropped = false;
15274
+ this.reconnectAttempts = 0;
15275
+ }
14670
15276
  async close() {
14671
- if (this.child === void 0) return;
14672
- this.child.kill("SIGTERM");
15277
+ this.rejectAllPending(new exports.NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
15278
+ const child = this.child;
14673
15279
  this.child = void 0;
15280
+ this.dropped = false;
15281
+ child?.kill("SIGTERM");
15282
+ }
15283
+ /** Reject + clear every pending request (crash / close). @internal */
15284
+ rejectAllPending(error) {
15285
+ for (const entry of this.pending.values()) {
15286
+ clearTimeout(entry.timer);
15287
+ entry.reject(error);
15288
+ }
15289
+ this.pending.clear();
14674
15290
  }
14675
15291
  consume(chunk) {
14676
15292
  this.buffer += chunk.toString("utf8");
15293
+ if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
15294
+ this.buffer = "";
15295
+ this.rejectAllPending(
15296
+ new exports.NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
15297
+ code: "mcp_buffer_overflow"
15298
+ })
15299
+ );
15300
+ this.child?.kill("SIGKILL");
15301
+ this.child = void 0;
15302
+ return;
15303
+ }
14677
15304
  let newlineIndex = this.buffer.indexOf("\n");
14678
15305
  while (newlineIndex !== -1) {
14679
15306
  const line = this.buffer.slice(0, newlineIndex).trim();
@@ -14690,23 +15317,47 @@ var StdioMcpClient = class extends BaseMcpClient {
14690
15317
  return;
14691
15318
  }
14692
15319
  if (typeof message.id !== "number") return;
14693
- const resolver = this.pending.get(message.id);
14694
- if (resolver === void 0) return;
15320
+ const entry = this.pending.get(message.id);
15321
+ if (entry === void 0) return;
14695
15322
  this.pending.delete(message.id);
14696
- resolver(message);
15323
+ clearTimeout(entry.timer);
15324
+ entry.resolve(message);
14697
15325
  }
14698
15326
  request(method, params) {
14699
- if (this.child === void 0) {
14700
- return Promise.reject(
14701
- new exports.ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
14702
- );
15327
+ const child = this.child;
15328
+ if (child !== void 0) return this.send(child, method, params);
15329
+ if (this.dropped) return this.reconnectAndRequest(method, params);
15330
+ return Promise.reject(
15331
+ new exports.ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
15332
+ );
15333
+ }
15334
+ /** M2 #59 — reconnect a dropped client, then send. Separate async path so the
15335
+ * happy path above never pays an extra microtask tick. */
15336
+ async reconnectAndRequest(method, params) {
15337
+ await this.ensureConnected();
15338
+ const child = this.child;
15339
+ if (child === void 0) {
15340
+ throw new exports.ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
14703
15341
  }
15342
+ return this.send(child, method, params);
15343
+ }
15344
+ send(child, method, params) {
14704
15345
  const id = this.nextId++;
14705
15346
  const payload = { jsonrpc: "2.0", id, method, params };
14706
- this.child.stdin.write(`${JSON.stringify(payload)}
15347
+ child.stdin.write(`${JSON.stringify(payload)}
14707
15348
  `);
14708
- return new Promise((resolve3) => {
14709
- this.pending.set(id, resolve3);
15349
+ return new Promise((resolve3, reject) => {
15350
+ const timer = setTimeout(() => {
15351
+ this.pending.delete(id);
15352
+ reject(mcpTimeoutError(this.name, this.timeoutMs));
15353
+ this.child?.kill("SIGKILL");
15354
+ this.child = void 0;
15355
+ this.dropped = true;
15356
+ this.rejectAllPending(
15357
+ new exports.NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
15358
+ );
15359
+ }, this.timeoutMs);
15360
+ this.pending.set(id, { resolve: resolve3, reject, timer });
14710
15361
  });
14711
15362
  }
14712
15363
  };
@@ -14732,11 +15383,20 @@ var HttpMcpClient = class extends BaseMcpClient {
14732
15383
  accept: "application/json",
14733
15384
  ...this.config.headers ?? {}
14734
15385
  };
14735
- const response = await this.fetchImpl(this.config.url, {
14736
- method: "POST",
14737
- headers,
14738
- body: JSON.stringify(payload)
14739
- });
15386
+ const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
15387
+ let response;
15388
+ try {
15389
+ response = await this.fetchImpl(this.config.url, {
15390
+ method: "POST",
15391
+ headers,
15392
+ body: JSON.stringify(payload),
15393
+ // #59 — bound the request; a non-responding endpoint aborts here.
15394
+ signal: AbortSignal.timeout(timeoutMs)
15395
+ });
15396
+ } catch (cause) {
15397
+ if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
15398
+ throw cause;
15399
+ }
14740
15400
  if (!response.ok) {
14741
15401
  throw new exports.NetworkError(`MCP ${this.name} returned ${response.status}`, {
14742
15402
  code: "mcp_http_error"
@@ -14851,11 +15511,33 @@ function resolveRunProvider(options) {
14851
15511
  );
14852
15512
  }
14853
15513
  const parsedModel = parseModelId(options.model?.id);
14854
- const inferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
14855
- const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? inferredProvider ?? detectPrimaryProvider();
14856
- const effectiveModelId = inferredProvider !== void 0 ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
15514
+ const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
15515
+ const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
15516
+ const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
15517
+ const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
14857
15518
  return { primary, effectiveModelId };
14858
15519
  }
15520
+ function inferProviderFromApiKey(apiKey) {
15521
+ if (apiKey === void 0 || apiKey.length === 0) return void 0;
15522
+ const byPrefix = [
15523
+ { provider: "openrouter", prefix: "sk-or-" },
15524
+ { provider: "anthropic", prefix: "sk-ant-" },
15525
+ { provider: "openai", prefix: "sk-" }
15526
+ ];
15527
+ for (const { provider, prefix } of byPrefix) {
15528
+ if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
15529
+ return provider;
15530
+ }
15531
+ }
15532
+ return void 0;
15533
+ }
15534
+ function mergeExplicitApiKey(pools, primary, apiKey) {
15535
+ if (apiKey === void 0 || apiKey.length === 0) return pools;
15536
+ if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
15537
+ const existing = pools?.[primary];
15538
+ if (existing !== void 0 && existing.length > 0) return pools;
15539
+ return { ...pools ?? {}, [primary]: [apiKey] };
15540
+ }
14859
15541
  function buildLoopInputs(options, runId, userText) {
14860
15542
  const maxIterations = options.sendOptions.maxIterations;
14861
15543
  if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
@@ -14866,7 +15548,11 @@ function buildLoopInputs(options, runId, userText) {
14866
15548
  }
14867
15549
  const { primary, effectiveModelId } = resolveRunProvider(options);
14868
15550
  const fallback = options.agentOptions.providers?.fallback;
14869
- const apiKeys = options.agentOptions.providers?.apiKeys;
15551
+ const apiKeys = mergeExplicitApiKey(
15552
+ options.agentOptions.providers?.apiKeys,
15553
+ primary,
15554
+ options.agentOptions.apiKey
15555
+ );
14870
15556
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
14871
15557
  const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
14872
15558
  const chain = resolveProviderChain({
@@ -14910,6 +15596,10 @@ function buildLoopInputs(options, runId, userText) {
14910
15596
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
14911
15597
  // can attach it to the LLM `fetch({ signal })` call.
14912
15598
  ...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
15599
+ // #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
15600
+ // can enable them via SendOptions (not only internal AgentLoopInputs).
15601
+ ...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
15602
+ ...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
14913
15603
  // M1-2: per-send iteration ceiling (validated above). The loop reads
14914
15604
  // inputs.maxIterations (default 8 when unset).
14915
15605
  ...maxIterations !== void 0 ? { maxIterations } : {},
@@ -15227,7 +15917,12 @@ async function runActiveMemory(args) {
15227
15917
  hits: []
15228
15918
  });
15229
15919
  }
15230
- const cached2 = args.cache?.get(args.userText, cfg.queryMode);
15920
+ const tenantCtx = {
15921
+ namespace: args.namespace,
15922
+ userId: args.userId,
15923
+ scope: args.scope
15924
+ };
15925
+ const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
15231
15926
  if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
15232
15927
  const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
15233
15928
  if (query.trim().length === 0) {
@@ -15312,7 +16007,12 @@ function notifyBreaker(breaker, key2, status) {
15312
16007
  else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key2);
15313
16008
  }
15314
16009
  async function finalize(args, queryMode, result) {
15315
- args.cache?.set(args.userText, queryMode, result);
16010
+ const tenantCtx = {
16011
+ namespace: args.namespace,
16012
+ userId: args.userId,
16013
+ scope: args.scope
16014
+ };
16015
+ args.cache?.set(args.userText, queryMode, result, tenantCtx);
15316
16016
  if (args.persistTranscripts === true && args.cwd !== void 0) {
15317
16017
  await persistActiveMemoryTranscript(args.cwd, {
15318
16018
  runId: args.runId ?? `run-${Date.now()}`,
@@ -15885,48 +16585,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
15885
16585
  gemini: geminiMemoryEmbeddingProviderAdapter
15886
16586
  };
15887
16587
 
15888
- // src/internal/memory/circuit-breaker.ts
15889
- var DEFAULT_MAX_TIMEOUTS = 3;
15890
- var DEFAULT_COOLDOWN_MS2 = 6e4;
15891
- var CircuitBreaker = class {
15892
- constructor(opts = {}) {
15893
- this.opts = opts;
15894
- }
15895
- opts;
15896
- states = /* @__PURE__ */ new Map();
15897
- /** @returns true when the breaker is open and the call should be skipped. */
15898
- shouldSkip(key2) {
15899
- const state4 = this.states.get(key2);
15900
- if (state4 === void 0) return false;
15901
- if (state4.cooldownUntilMs === 0) return false;
15902
- if (this.now() < state4.cooldownUntilMs) return true;
15903
- state4.cooldownUntilMs = 0;
15904
- state4.consecutiveTimeouts = 0;
15905
- return false;
15906
- }
15907
- recordSuccess(key2) {
15908
- const state4 = this.states.get(key2);
15909
- if (state4 === void 0) return;
15910
- state4.consecutiveTimeouts = 0;
15911
- state4.cooldownUntilMs = 0;
15912
- }
15913
- recordTimeout(key2) {
15914
- const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15915
- state4.consecutiveTimeouts += 1;
15916
- if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
15917
- state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
15918
- }
15919
- this.states.set(key2, state4);
15920
- }
15921
- /** @internal — tests inspect counter state. */
15922
- inspect(key2) {
15923
- return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15924
- }
15925
- now() {
15926
- return this.opts.now?.() ?? Date.now();
15927
- }
15928
- };
15929
-
15930
16588
  // src/internal/runtime/local-agent/local-agent-memory.ts
15931
16589
  init_index_manager();
15932
16590
 
@@ -18515,13 +19173,13 @@ function defineTool(spec) {
18515
19173
  name: spec.name,
18516
19174
  description: spec.description,
18517
19175
  inputSchema,
18518
- handler: async (input) => {
19176
+ handler: async (input, ctx) => {
18519
19177
  const raw = spec.sanitize ? sanitizeToolInput(input, {
18520
19178
  ...spec.sanitize === true ? {} : spec.sanitize,
18521
19179
  schema: spec.inputSchema
18522
19180
  }).value : input;
18523
19181
  const parsed = spec.inputSchema.parse(raw);
18524
- return await spec.handler(parsed);
19182
+ return await spec.handler(parsed, ctx);
18525
19183
  }
18526
19184
  };
18527
19185
  }
@@ -18532,6 +19190,14 @@ init_errors();
18532
19190
  // src/event-bus.ts
18533
19191
  var EventBus = class {
18534
19192
  handlers = /* @__PURE__ */ new Map();
19193
+ // M3 #64 — a swallowed handler error used to vanish without a trace (fail-loud
19194
+ // violation). We now log it AND expose an observable count so ops/tests can see
19195
+ // that a subscriber is silently failing, without breaking the EC-2 contract.
19196
+ #handlerErrorCount = 0;
19197
+ /** M3 #64 — number of handler invocations that threw (and were logged). */
19198
+ get handlerErrorCount() {
19199
+ return this.#handlerErrorCount;
19200
+ }
18535
19201
  /**
18536
19202
  * Subscribe to an event. Returns an unsubscribe function.
18537
19203
  */
@@ -18554,7 +19220,13 @@ var EventBus = class {
18554
19220
  for (const handler of set) {
18555
19221
  try {
18556
19222
  handler(payload);
18557
- } catch {
19223
+ } catch (cause) {
19224
+ this.#handlerErrorCount += 1;
19225
+ const message = cause instanceof Error ? cause.message : String(cause);
19226
+ process.stderr.write(
19227
+ `[theokit-sdk] event-bus: handler for "${String(event)}" threw: ${message}
19228
+ `
19229
+ );
18558
19230
  }
18559
19231
  }
18560
19232
  }
@@ -18577,11 +19249,17 @@ init_generate_object();
18577
19249
  // src/internal/persistence/conversation-storage-memory.ts
18578
19250
  var InMemoryConversationStorage = class {
18579
19251
  #store = /* @__PURE__ */ new Map();
18580
- async getMessages(conversationId) {
19252
+ async getMessages(conversationId, opts) {
18581
19253
  const existing = this.#store.get(conversationId);
18582
- return existing === void 0 ? [] : existing.slice();
19254
+ return existing === void 0 ? [] : paginate(existing.slice(), opts);
18583
19255
  }
18584
19256
  async appendMessage(conversationId, message) {
19257
+ this.#appendOne(conversationId, message);
19258
+ }
19259
+ async appendMessages(conversationId, messages) {
19260
+ for (const message of messages) this.#appendOne(conversationId, message);
19261
+ }
19262
+ #appendOne(conversationId, message) {
18585
19263
  const existing = this.#store.get(conversationId);
18586
19264
  const stamped = message.at === void 0 ? { ...message, at: Date.now() } : message;
18587
19265
  if (existing === void 0) {
@@ -18590,9 +19268,26 @@ var InMemoryConversationStorage = class {
18590
19268
  }
18591
19269
  existing.push(stamped);
18592
19270
  }
19271
+ async truncateConversation(conversationId, keepCount) {
19272
+ const existing = this.#store.get(conversationId);
19273
+ if (existing === void 0) return 0;
19274
+ const keep = Math.max(0, Math.min(keepCount, existing.length));
19275
+ this.#store.set(conversationId, existing.slice(0, keep));
19276
+ return keep;
19277
+ }
18593
19278
  async deleteConversation(conversationId) {
18594
19279
  this.#store.delete(conversationId);
18595
19280
  }
19281
+ async deleteScope(prefix) {
19282
+ let n = 0;
19283
+ for (const id of [...this.#store.keys()]) {
19284
+ if (id.startsWith(prefix)) {
19285
+ this.#store.delete(id);
19286
+ n += 1;
19287
+ }
19288
+ }
19289
+ return n;
19290
+ }
18596
19291
  async listConversationIds(opts) {
18597
19292
  const all = Array.from(this.#store.keys());
18598
19293
  if (opts?.limit !== void 0) return all.slice(0, opts.limit);
@@ -18770,23 +19465,40 @@ function createNoopMemoryProvider() {
18770
19465
  }
18771
19466
  var JobQueue = class {
18772
19467
  jobs = /* @__PURE__ */ new Map();
19468
+ controllers = /* @__PURE__ */ new Map();
19469
+ maxConcurrency;
19470
+ running = 0;
19471
+ waiting = [];
19472
+ constructor(options = {}) {
19473
+ this.maxConcurrency = options.maxConcurrency === void 0 ? Number.POSITIVE_INFINITY : Math.max(1, options.maxConcurrency);
19474
+ }
18773
19475
  /**
18774
- * Enqueue a background function. Returns the job ID immediately.
18775
- * EC-1: wraps fn in Promise.resolve().then() so sync throws are caught.
19476
+ * Enqueue a background function. Returns the job ID immediately. The function
19477
+ * receives an `AbortSignal` that fires when the job is cancelled (#58) — a
19478
+ * cooperative job should observe it to stop early. Existing `() => Promise<T>`
19479
+ * callers are unaffected (the signal argument is simply ignored).
18776
19480
  */
18777
19481
  enqueue(fn) {
18778
19482
  const id = crypto.randomUUID();
18779
19483
  const job = { id, status: "pending" };
18780
19484
  this.jobs.set(id, job);
18781
- job.status = "running";
18782
- Promise.resolve().then(() => fn()).then((result) => {
18783
- if (job.status === "cancelled") return;
18784
- job.result = result;
18785
- job.status = "completed";
18786
- }).catch((err) => {
18787
- if (job.status === "cancelled") return;
18788
- job.error = err instanceof Error ? err.message : String(err);
18789
- job.status = "failed";
19485
+ const controller = new AbortController();
19486
+ this.controllers.set(id, controller);
19487
+ void this.#acquire().then(() => {
19488
+ if (job.status === "cancelled") {
19489
+ this.#release(id);
19490
+ return;
19491
+ }
19492
+ job.status = "running";
19493
+ Promise.resolve().then(() => fn(controller.signal)).then((result) => {
19494
+ if (job.status === "cancelled") return;
19495
+ job.result = result;
19496
+ job.status = "completed";
19497
+ }).catch((err) => {
19498
+ if (job.status === "cancelled") return;
19499
+ job.error = err instanceof Error ? err.message : String(err);
19500
+ job.status = "failed";
19501
+ }).finally(() => this.#release(id));
18790
19502
  });
18791
19503
  return id;
18792
19504
  }
@@ -18797,17 +19509,39 @@ var JobQueue = class {
18797
19509
  return [...this.jobs.values()];
18798
19510
  }
18799
19511
  /**
18800
- * Cancel a pending or running job. Returns true if cancelled.
19512
+ * Cancel a pending or running job. Returns true if cancelled. #58 — aborts the
19513
+ * job's `AbortSignal` so a cooperative running job is actually interrupted.
18801
19514
  */
18802
19515
  cancel(id) {
18803
19516
  const job = this.jobs.get(id);
18804
19517
  if (!job) return false;
18805
19518
  if (job.status === "pending" || job.status === "running") {
18806
19519
  job.status = "cancelled";
19520
+ this.controllers.get(id)?.abort();
18807
19521
  return true;
18808
19522
  }
18809
19523
  return false;
18810
19524
  }
19525
+ /** Acquire a concurrency slot (resolves immediately when unbounded/free). */
19526
+ #acquire() {
19527
+ if (this.running < this.maxConcurrency) {
19528
+ this.running += 1;
19529
+ return Promise.resolve();
19530
+ }
19531
+ return new Promise((resolve3) => {
19532
+ this.waiting.push(() => {
19533
+ this.running += 1;
19534
+ resolve3();
19535
+ });
19536
+ });
19537
+ }
19538
+ /** Release a slot + clean up the controller; start the next waiting job. */
19539
+ #release(id) {
19540
+ this.controllers.delete(id);
19541
+ this.running -= 1;
19542
+ const next = this.waiting.shift();
19543
+ if (next !== void 0) next();
19544
+ }
18811
19545
  };
18812
19546
 
18813
19547
  // src/internal/memory/dreaming/run.ts
@@ -19300,24 +20034,42 @@ async function migrateSqliteToLance2(options) {
19300
20034
  }
19301
20035
 
19302
20036
  // src/permission-engine.ts
20037
+ function argMatches(matcher, value) {
20038
+ if (typeof matcher === "function") return matcher(value);
20039
+ if (value === void 0) return false;
20040
+ if (matcher instanceof RegExp) return matcher.test(String(value));
20041
+ return matcher === value;
20042
+ }
19303
20043
  var PermissionEngine = class {
19304
20044
  constructor(rules, options = {}) {
19305
20045
  this.rules = rules;
19306
- this.defaultAction = options.defaultAction ?? "allow";
20046
+ this.defaultAction = options.defaultAction ?? "ask";
19307
20047
  }
19308
20048
  rules;
19309
20049
  defaultAction;
19310
20050
  /**
19311
- * Evaluate a tool name against the rules. First match wins; falls back to the
19312
- * configured `defaultAction` (default `"allow"`) when no rule matches.
20051
+ * Evaluate a tool name (and optional arguments) against the rules. First
20052
+ * match wins; falls back to the configured `defaultAction` (default `"ask"`,
20053
+ * fail-closed) when no rule matches. #55 — a rule with `args` gates on the
20054
+ * argument values, so the same tool name can resolve to different actions
20055
+ * depending on what it is asked to do.
19313
20056
  */
19314
- evaluate(toolName) {
20057
+ evaluate(toolName, args) {
19315
20058
  for (const rule of this.rules) {
19316
- const matches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
19317
- if (matches) return rule.action;
20059
+ const nameMatches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
20060
+ if (!nameMatches) continue;
20061
+ if (rule.args !== void 0 && !this.#argsMatch(rule.args, args)) continue;
20062
+ return rule.action;
19318
20063
  }
19319
20064
  return this.defaultAction;
19320
20065
  }
20066
+ #argsMatch(matchers, args) {
20067
+ const call = args ?? {};
20068
+ for (const [key2, matcher] of Object.entries(matchers)) {
20069
+ if (!argMatches(matcher, call[key2])) return false;
20070
+ }
20071
+ return true;
20072
+ }
19321
20073
  };
19322
20074
 
19323
20075
  // src/permission-plugin.ts
@@ -19328,8 +20080,8 @@ function createPermissionPlugin(engine, opts = {}) {
19328
20080
  kind: "general",
19329
20081
  register(ctx) {
19330
20082
  ctx.on("pre_tool_call", (rawCtx) => {
19331
- const { name } = rawCtx;
19332
- const action = engine.evaluate(name);
20083
+ const { name, args } = rawCtx;
20084
+ const action = engine.evaluate(name, args);
19333
20085
  if (action === "deny") {
19334
20086
  return { block: true, message: `denied by permission engine: ${name}` };
19335
20087
  }
@@ -19394,6 +20146,14 @@ var Security = class {
19394
20146
  }
19395
20147
  };
19396
20148
 
20149
+ // src/session-scope.ts
20150
+ function scopedConversationId(scope, id) {
20151
+ return `${scope}__${id}`;
20152
+ }
20153
+ function sessionScopePrefix(scope) {
20154
+ return `${scope}__`;
20155
+ }
20156
+
19397
20157
  // src/squad.ts
19398
20158
  init_errors();
19399
20159
  var PersistenceSchema = zod.z.object({
@@ -20144,6 +20904,8 @@ exports.migrateSqliteToLance = migrateSqliteToLance2;
20144
20904
  exports.mkMemoryId = mkMemoryId;
20145
20905
  exports.normalizeUsage = normalizeUsage;
20146
20906
  exports.preflightCheck = preflightCheck;
20907
+ exports.scopedConversationId = scopedConversationId;
20908
+ exports.sessionScopePrefix = sessionScopePrefix;
20147
20909
  exports.toShareGptTrajectory = toShareGptTrajectory;
20148
20910
  exports.withCwdMutex = withCwdMutex;
20149
20911
  //# sourceMappingURL=index.cjs.map