@theokit/sdk 2.15.1 → 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 +71 -0
  2. package/dist/a2a/index.cjs +981 -208
  3. package/dist/a2a/index.cjs.map +1 -1
  4. package/dist/a2a/index.js +982 -209
  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 +945 -196
  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 +945 -196
  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 +951 -198
  19. package/dist/eval.cjs.map +1 -1
  20. package/dist/eval.js +951 -198
  21. package/dist/eval.js.map +1 -1
  22. package/dist/event-bus.d.ts +3 -0
  23. package/dist/index.cjs +1082 -224
  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 +1082 -226
  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 +13 -0
  33. package/dist/internal/llm/sse.d.ts +13 -1
  34. package/dist/internal/mcp/client.d.ts +1 -1
  35. package/dist/internal/memory/active-memory.d.ts +1 -1
  36. package/dist/internal/persistence/conversation-storage-fs.d.cts +7 -1
  37. package/dist/internal/persistence/conversation-storage-fs.d.ts +7 -1
  38. package/dist/internal/persistence/conversation-storage-memory.d.cts +7 -1
  39. package/dist/internal/persistence/conversation-storage-memory.d.ts +7 -1
  40. package/dist/internal/persistence/pagination.d.cts +8 -0
  41. package/dist/internal/persistence/pagination.d.ts +8 -0
  42. package/dist/internal/plugins/index.cjs +135 -0
  43. package/dist/internal/plugins/index.cjs.map +1 -1
  44. package/dist/internal/plugins/index.js +135 -0
  45. package/dist/internal/plugins/index.js.map +1 -1
  46. package/dist/internal/plugins/manager.d.cts +21 -1
  47. package/dist/internal/plugins/manager.d.ts +21 -1
  48. package/dist/internal/plugins/types.d.cts +40 -0
  49. package/dist/internal/plugins/types.d.ts +40 -0
  50. package/dist/internal/{memory → resilience}/circuit-breaker.d.ts +5 -1
  51. package/dist/internal/runtime/hooks/hooks-frontmatter.d.ts +1 -1
  52. package/dist/internal/runtime/lifecycle/env-policy.d.ts +30 -0
  53. package/dist/internal/runtime/session/agent-session-store.d.ts +1 -0
  54. package/dist/internal/telemetry/span-names.d.ts +7 -1
  55. package/dist/job-queue.d.ts +29 -7
  56. package/dist/permission-engine.d.ts +32 -7
  57. package/dist/{run-DXy_MVwz.d.cts → run-pE-34AAo.d.cts} +64 -3
  58. package/dist/{run-DXy_MVwz.d.ts → run-pE-34AAo.d.ts} +64 -3
  59. package/dist/sandbox/index.cjs +53 -2
  60. package/dist/sandbox/index.cjs.map +1 -1
  61. package/dist/sandbox/index.js +53 -2
  62. package/dist/sandbox/index.js.map +1 -1
  63. package/dist/sandbox/local-sandbox.d.cts +11 -3
  64. package/dist/sandbox/local-sandbox.d.ts +11 -3
  65. package/dist/sandbox/types.d.cts +7 -0
  66. package/dist/sandbox/types.d.ts +7 -0
  67. package/dist/types/agent-prims.d.ts +6 -2
  68. package/dist/types/conversation-storage.d.ts +32 -2
  69. package/dist/types/mcp.d.ts +20 -0
  70. package/dist/types/run.d.ts +17 -0
  71. package/dist/workflow.cjs +6 -3
  72. package/dist/workflow.cjs.map +1 -1
  73. package/dist/workflow.js +6 -3
  74. package/dist/workflow.js.map +1 -1
  75. package/package.json +1 -1
package/dist/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) {
@@ -13683,6 +14144,77 @@ function parseHermesParams(inner) {
13683
14144
  }
13684
14145
  return sanitizeToolInput(input, { trim: true }).value;
13685
14146
  }
14147
+ var STREAM_MARKER = "<function=";
14148
+ var DEFAULT_STREAM_BUFFER_CAP = 8192;
14149
+ var isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
14150
+ function streamToolCallBufferState(held, allowedToolNames, cap2 = DEFAULT_STREAM_BUFFER_CAP) {
14151
+ if (allowedToolNames.size === 0) return "impossible";
14152
+ const t = held.trimStart();
14153
+ if (t.length < STREAM_MARKER.length) {
14154
+ return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
14155
+ }
14156
+ if (!t.startsWith(STREAM_MARKER)) return "impossible";
14157
+ const parsed = parseStreamMarkerName(t);
14158
+ if (parsed === "building") return "possible";
14159
+ if (parsed === "invalid") return "impossible";
14160
+ const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
14161
+ if (!nameOk) return "impossible";
14162
+ return held.length > cap2 ? "impossible" : "possible";
14163
+ }
14164
+ function parseStreamMarkerName(t) {
14165
+ let cursor = STREAM_MARKER.length;
14166
+ while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
14167
+ const nameStart = cursor;
14168
+ while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
14169
+ const name = t.slice(nameStart, cursor);
14170
+ if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
14171
+ return { name, complete: cursor < t.length && t[cursor] === ">" };
14172
+ }
14173
+ function someToolNameStartsWith(allowedToolNames, prefix) {
14174
+ for (const name of allowedToolNames) {
14175
+ if (name.startsWith(prefix)) return true;
14176
+ }
14177
+ return false;
14178
+ }
14179
+ function firstPossibleMarkerStart(held, allowedToolNames) {
14180
+ for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
14181
+ if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
14182
+ }
14183
+ return -1;
14184
+ }
14185
+ var StreamSuppressionBuffer = class {
14186
+ constructor(allowedToolNames) {
14187
+ this.allowedToolNames = allowedToolNames;
14188
+ }
14189
+ allowedToolNames;
14190
+ #held = "";
14191
+ /** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
14192
+ push(content) {
14193
+ this.#held += content;
14194
+ if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
14195
+ return void 0;
14196
+ const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
14197
+ if (holdStart > 0) {
14198
+ const flush2 = this.#held.slice(0, holdStart);
14199
+ this.#held = this.#held.slice(holdStart);
14200
+ return flush2;
14201
+ }
14202
+ const flush = this.#held;
14203
+ this.#held = "";
14204
+ return flush;
14205
+ }
14206
+ /** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
14207
+ * native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
14208
+ * (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
14209
+ drain(hasNativeCalls) {
14210
+ if (this.#held.length === 0) return void 0;
14211
+ const held = this.#held;
14212
+ this.#held = "";
14213
+ if (hasNativeCalls) return held;
14214
+ const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
14215
+ return residual.length > 0 ? residual : void 0;
14216
+ }
14217
+ };
13686
14218
 
13687
14219
  // src/internal/llm/openai.ts
13688
14220
  var OpenAIClient = class {
@@ -13760,8 +14292,12 @@ var OpenAIClient = class {
13760
14292
  // model was actually given. Empty set (no tools) recovers nothing.
13761
14293
  new Set(request.tools?.map((tool) => tool.name) ?? [])
13762
14294
  );
14295
+ let sawDone = false;
13763
14296
  for await (const record of parseSseStream(response.body, signal)) {
13764
- if (record.data === "[DONE]") break;
14297
+ if (record.data === "[DONE]") {
14298
+ sawDone = true;
14299
+ break;
14300
+ }
13765
14301
  let chunk;
13766
14302
  try {
13767
14303
  chunk = JSON.parse(record.data);
@@ -13781,6 +14317,13 @@ var OpenAIClient = class {
13781
14317
  const events = accumulator.consume(chunk);
13782
14318
  for (const event of events) yield event;
13783
14319
  }
14320
+ if (!sawDone && !accumulator.finishReasonSeen) {
14321
+ throw new exports.NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
14322
+ code: "stream_truncated"
14323
+ });
14324
+ }
14325
+ const drainEvent = accumulator.finalizeHeldText();
14326
+ if (drainEvent !== void 0) yield drainEvent;
13784
14327
  return accumulator.finish();
13785
14328
  }
13786
14329
  };
@@ -13796,6 +14339,7 @@ var OpenAIStreamAccumulator = class {
13796
14339
  this.extractFromContent = extractFromContent;
13797
14340
  this.providerName = providerName;
13798
14341
  this.allowedToolNames = allowedToolNames;
14342
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
13799
14343
  }
13800
14344
  extractFromContent;
13801
14345
  providerName;
@@ -13808,18 +14352,30 @@ var OpenAIStreamAccumulator = class {
13808
14352
  cacheWriteTokens;
13809
14353
  reasoningTokens;
13810
14354
  toolCalls = /* @__PURE__ */ new Map();
14355
+ /** R7: present only when recovery is enabled AND the request declares tools — holds suspected
14356
+ * leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
14357
+ suppress;
13811
14358
  consume(chunk) {
13812
14359
  const events = [];
13813
14360
  this.applyUsage(chunk.usage);
13814
14361
  for (const choice of chunk.choices ?? []) {
13815
- const reasoningEvent = this.applyReasoningDelta(
13816
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
13817
- );
13818
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
13819
- const textEvent = this.applyContentDelta(choice.delta?.content);
13820
- if (textEvent !== void 0) events.push(textEvent);
13821
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
13822
- this.applyFinishReason(choice.finish_reason);
14362
+ events.push(...this.applyChoice(choice));
14363
+ }
14364
+ return events;
14365
+ }
14366
+ applyChoice(choice) {
14367
+ const events = [];
14368
+ const reasoningEvent = this.applyReasoningDelta(
14369
+ choice.delta?.reasoning ?? choice.delta?.reasoning_content
14370
+ );
14371
+ if (reasoningEvent !== void 0) events.push(reasoningEvent);
14372
+ const textEvent = this.applyContentDelta(choice.delta?.content);
14373
+ if (textEvent !== void 0) events.push(textEvent);
14374
+ this.mergeToolCallDeltas(choice.delta?.tool_calls);
14375
+ this.applyFinishReason(choice.finish_reason);
14376
+ if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
14377
+ const flushEvent = this.finalizeHeldText();
14378
+ if (flushEvent !== void 0) events.push(flushEvent);
13823
14379
  }
13824
14380
  return events;
13825
14381
  }
@@ -13844,7 +14400,17 @@ var OpenAIStreamAccumulator = class {
13844
14400
  applyContentDelta(content) {
13845
14401
  if (typeof content !== "string" || content.length === 0) return void 0;
13846
14402
  this.text += content;
13847
- return { type: "text_delta", text: content };
14403
+ if (this.suppress === void 0) return { type: "text_delta", text: content };
14404
+ const emit2 = this.suppress.push(content);
14405
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
14406
+ }
14407
+ /** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
14408
+ * SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
14409
+ * held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
14410
+ * buffer streams the held text whole. Idempotent once drained. */
14411
+ finalizeHeldText() {
14412
+ const emit2 = this.suppress?.drain(this.toolCalls.size > 0);
14413
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
13848
14414
  }
13849
14415
  mergeToolCallDeltas(deltas) {
13850
14416
  for (const call of deltas ?? []) {
@@ -13855,8 +14421,15 @@ var OpenAIStreamAccumulator = class {
13855
14421
  this.toolCalls.set(call.index, existing);
13856
14422
  }
13857
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
+ }
13858
14430
  applyFinishReason(reason) {
13859
14431
  if (reason === void 0 || reason === null) return;
14432
+ this.sawFinishReason = true;
13860
14433
  this.stopReason = mapOpenAIFinish(reason);
13861
14434
  }
13862
14435
  finish() {
@@ -13901,18 +14474,6 @@ var OpenAIStreamAccumulator = class {
13901
14474
  });
13902
14475
  }
13903
14476
  };
13904
- function mapOpenAIFinish(reason) {
13905
- switch (reason) {
13906
- case "tool_calls":
13907
- return "tool_use";
13908
- case "length":
13909
- return "max_tokens";
13910
- case "stop":
13911
- return "end_turn";
13912
- default:
13913
- return "end_turn";
13914
- }
13915
- }
13916
14477
  function applyReasoningRequest(body, effort, providerName) {
13917
14478
  if (providerName === "openai") {
13918
14479
  body.reasoning_effort = effort;
@@ -14010,19 +14571,76 @@ function assistantMessage(message) {
14010
14571
 
14011
14572
  // src/internal/llm/pool-aware-client.ts
14012
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();
14013
14619
  var PoolAwareLlmClient = class {
14014
- constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
14620
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
14015
14621
  this.pool = pool;
14016
14622
  this.buildClient = buildClient2;
14017
14623
  this.waitForAvailableMs = waitForAvailableMs;
14018
14624
  this.name = `pool-aware:${pool.provider}`;
14625
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
14626
+ this.backoffBaseMs = resilience.backoffBaseMs;
14627
+ this.rng = resilience.rng;
14019
14628
  }
14020
14629
  pool;
14021
14630
  buildClient;
14022
14631
  waitForAvailableMs;
14023
14632
  name;
14633
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
14634
+ breaker;
14635
+ backoffBaseMs;
14636
+ rng;
14024
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.
14025
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
+ }
14026
14644
  let hasRetried429 = false;
14027
14645
  while (true) {
14028
14646
  if (signal.aborted) throw abortError2(signal);
@@ -14037,6 +14655,7 @@ var PoolAwareLlmClient = class {
14037
14655
  }
14038
14656
  }
14039
14657
  if (entry === null) {
14658
+ this.breaker.recordTimeout(this.pool.provider);
14040
14659
  throw new CredentialPoolExhaustedError(
14041
14660
  `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
14042
14661
  { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
@@ -14046,10 +14665,19 @@ var PoolAwareLlmClient = class {
14046
14665
  const realClient = this.buildClient(entry.accessToken);
14047
14666
  const attempt = await tryFirstEvent(realClient, request, signal);
14048
14667
  if (attempt.kind === "ok") {
14668
+ this.breaker.recordSuccess(this.pool.provider);
14049
14669
  return yield* relayStream(attempt.generator, attempt.firstResult);
14050
14670
  }
14051
14671
  const decision = classifyAndDecide(attempt.error, hasRetried429);
14052
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
+ );
14053
14681
  hasRetried429 = true;
14054
14682
  continue;
14055
14683
  }
@@ -14069,6 +14697,7 @@ var PoolAwareLlmClient = class {
14069
14697
  hasRetried429 = false;
14070
14698
  continue;
14071
14699
  }
14700
+ this.breaker.recordTimeout(this.pool.provider);
14072
14701
  throw attempt.error;
14073
14702
  }
14074
14703
  }
@@ -14504,9 +15133,28 @@ function selectTransport(profile, apiKey) {
14504
15133
  // src/internal/mcp/client.ts
14505
15134
  init_errors();
14506
15135
  init_path_guard();
14507
- function createMcpClient(name, config) {
15136
+ function createMcpClient(name, config, fetchImpl = fetch) {
14508
15137
  if (isStdio(config)) return new StdioMcpClient(name, config);
14509
- 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";
14510
15158
  }
14511
15159
  async function rpcInitialize(request) {
14512
15160
  await request("initialize", {
@@ -14552,32 +15200,107 @@ var StdioMcpClient = class extends BaseMcpClient {
14552
15200
  name;
14553
15201
  child;
14554
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.
14555
15205
  pending = /* @__PURE__ */ new Map();
14556
15206
  buffer = "";
14557
- 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() {
14558
15221
  const resolvedCwd = resolveMcpCwd(this.config.cwd);
14559
15222
  const child = child_process.spawn(this.config.command, this.config.args ?? [], {
14560
15223
  cwd: resolvedCwd,
14561
- 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 })
14562
15227
  });
14563
15228
  this.child = child;
14564
15229
  child.stdout.on("data", (chunk) => this.consume(chunk));
14565
15230
  child.stderr.on("data", () => void 0);
15231
+ child.stdin.on("error", () => void 0);
14566
15232
  child.on("error", () => {
14567
- for (const resolver of this.pending.values()) {
14568
- resolver({ error: { message: "MCP process crashed" } });
14569
- }
14570
- 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
+ );
14571
15244
  });
15245
+ }
15246
+ async initialize() {
15247
+ this.spawnChild();
14572
15248
  await super.initialize();
14573
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
+ }
14574
15276
  async close() {
14575
- if (this.child === void 0) return;
14576
- this.child.kill("SIGTERM");
15277
+ this.rejectAllPending(new exports.NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
15278
+ const child = this.child;
14577
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();
14578
15290
  }
14579
15291
  consume(chunk) {
14580
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
+ }
14581
15304
  let newlineIndex = this.buffer.indexOf("\n");
14582
15305
  while (newlineIndex !== -1) {
14583
15306
  const line = this.buffer.slice(0, newlineIndex).trim();
@@ -14594,23 +15317,47 @@ var StdioMcpClient = class extends BaseMcpClient {
14594
15317
  return;
14595
15318
  }
14596
15319
  if (typeof message.id !== "number") return;
14597
- const resolver = this.pending.get(message.id);
14598
- if (resolver === void 0) return;
15320
+ const entry = this.pending.get(message.id);
15321
+ if (entry === void 0) return;
14599
15322
  this.pending.delete(message.id);
14600
- resolver(message);
15323
+ clearTimeout(entry.timer);
15324
+ entry.resolve(message);
14601
15325
  }
14602
15326
  request(method, params) {
14603
- if (this.child === void 0) {
14604
- return Promise.reject(
14605
- new exports.ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
14606
- );
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" });
14607
15341
  }
15342
+ return this.send(child, method, params);
15343
+ }
15344
+ send(child, method, params) {
14608
15345
  const id = this.nextId++;
14609
15346
  const payload = { jsonrpc: "2.0", id, method, params };
14610
- this.child.stdin.write(`${JSON.stringify(payload)}
15347
+ child.stdin.write(`${JSON.stringify(payload)}
14611
15348
  `);
14612
- return new Promise((resolve3) => {
14613
- 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 });
14614
15361
  });
14615
15362
  }
14616
15363
  };
@@ -14636,11 +15383,20 @@ var HttpMcpClient = class extends BaseMcpClient {
14636
15383
  accept: "application/json",
14637
15384
  ...this.config.headers ?? {}
14638
15385
  };
14639
- const response = await this.fetchImpl(this.config.url, {
14640
- method: "POST",
14641
- headers,
14642
- body: JSON.stringify(payload)
14643
- });
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
+ }
14644
15400
  if (!response.ok) {
14645
15401
  throw new exports.NetworkError(`MCP ${this.name} returned ${response.status}`, {
14646
15402
  code: "mcp_http_error"
@@ -14755,11 +15511,33 @@ function resolveRunProvider(options) {
14755
15511
  );
14756
15512
  }
14757
15513
  const parsedModel = parseModelId(options.model?.id);
14758
- const inferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
14759
- const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? inferredProvider ?? detectPrimaryProvider();
14760
- 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";
14761
15518
  return { primary, effectiveModelId };
14762
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
+ }
14763
15541
  function buildLoopInputs(options, runId, userText) {
14764
15542
  const maxIterations = options.sendOptions.maxIterations;
14765
15543
  if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
@@ -14770,7 +15548,11 @@ function buildLoopInputs(options, runId, userText) {
14770
15548
  }
14771
15549
  const { primary, effectiveModelId } = resolveRunProvider(options);
14772
15550
  const fallback = options.agentOptions.providers?.fallback;
14773
- const apiKeys = options.agentOptions.providers?.apiKeys;
15551
+ const apiKeys = mergeExplicitApiKey(
15552
+ options.agentOptions.providers?.apiKeys,
15553
+ primary,
15554
+ options.agentOptions.apiKey
15555
+ );
14774
15556
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
14775
15557
  const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
14776
15558
  const chain = resolveProviderChain({
@@ -14814,6 +15596,10 @@ function buildLoopInputs(options, runId, userText) {
14814
15596
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
14815
15597
  // can attach it to the LLM `fetch({ signal })` call.
14816
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 } : {},
14817
15603
  // M1-2: per-send iteration ceiling (validated above). The loop reads
14818
15604
  // inputs.maxIterations (default 8 when unset).
14819
15605
  ...maxIterations !== void 0 ? { maxIterations } : {},
@@ -15131,7 +15917,12 @@ async function runActiveMemory(args) {
15131
15917
  hits: []
15132
15918
  });
15133
15919
  }
15134
- 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);
15135
15926
  if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
15136
15927
  const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
15137
15928
  if (query.trim().length === 0) {
@@ -15216,7 +16007,12 @@ function notifyBreaker(breaker, key2, status) {
15216
16007
  else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key2);
15217
16008
  }
15218
16009
  async function finalize(args, queryMode, result) {
15219
- 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);
15220
16016
  if (args.persistTranscripts === true && args.cwd !== void 0) {
15221
16017
  await persistActiveMemoryTranscript(args.cwd, {
15222
16018
  runId: args.runId ?? `run-${Date.now()}`,
@@ -15789,48 +16585,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
15789
16585
  gemini: geminiMemoryEmbeddingProviderAdapter
15790
16586
  };
15791
16587
 
15792
- // src/internal/memory/circuit-breaker.ts
15793
- var DEFAULT_MAX_TIMEOUTS = 3;
15794
- var DEFAULT_COOLDOWN_MS2 = 6e4;
15795
- var CircuitBreaker = class {
15796
- constructor(opts = {}) {
15797
- this.opts = opts;
15798
- }
15799
- opts;
15800
- states = /* @__PURE__ */ new Map();
15801
- /** @returns true when the breaker is open and the call should be skipped. */
15802
- shouldSkip(key2) {
15803
- const state4 = this.states.get(key2);
15804
- if (state4 === void 0) return false;
15805
- if (state4.cooldownUntilMs === 0) return false;
15806
- if (this.now() < state4.cooldownUntilMs) return true;
15807
- state4.cooldownUntilMs = 0;
15808
- state4.consecutiveTimeouts = 0;
15809
- return false;
15810
- }
15811
- recordSuccess(key2) {
15812
- const state4 = this.states.get(key2);
15813
- if (state4 === void 0) return;
15814
- state4.consecutiveTimeouts = 0;
15815
- state4.cooldownUntilMs = 0;
15816
- }
15817
- recordTimeout(key2) {
15818
- const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15819
- state4.consecutiveTimeouts += 1;
15820
- if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
15821
- state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
15822
- }
15823
- this.states.set(key2, state4);
15824
- }
15825
- /** @internal — tests inspect counter state. */
15826
- inspect(key2) {
15827
- return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15828
- }
15829
- now() {
15830
- return this.opts.now?.() ?? Date.now();
15831
- }
15832
- };
15833
-
15834
16588
  // src/internal/runtime/local-agent/local-agent-memory.ts
15835
16589
  init_index_manager();
15836
16590
 
@@ -18419,13 +19173,13 @@ function defineTool(spec) {
18419
19173
  name: spec.name,
18420
19174
  description: spec.description,
18421
19175
  inputSchema,
18422
- handler: async (input) => {
19176
+ handler: async (input, ctx) => {
18423
19177
  const raw = spec.sanitize ? sanitizeToolInput(input, {
18424
19178
  ...spec.sanitize === true ? {} : spec.sanitize,
18425
19179
  schema: spec.inputSchema
18426
19180
  }).value : input;
18427
19181
  const parsed = spec.inputSchema.parse(raw);
18428
- return await spec.handler(parsed);
19182
+ return await spec.handler(parsed, ctx);
18429
19183
  }
18430
19184
  };
18431
19185
  }
@@ -18436,6 +19190,14 @@ init_errors();
18436
19190
  // src/event-bus.ts
18437
19191
  var EventBus = class {
18438
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
+ }
18439
19201
  /**
18440
19202
  * Subscribe to an event. Returns an unsubscribe function.
18441
19203
  */
@@ -18458,7 +19220,13 @@ var EventBus = class {
18458
19220
  for (const handler of set) {
18459
19221
  try {
18460
19222
  handler(payload);
18461
- } 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
+ );
18462
19230
  }
18463
19231
  }
18464
19232
  }
@@ -18481,11 +19249,17 @@ init_generate_object();
18481
19249
  // src/internal/persistence/conversation-storage-memory.ts
18482
19250
  var InMemoryConversationStorage = class {
18483
19251
  #store = /* @__PURE__ */ new Map();
18484
- async getMessages(conversationId) {
19252
+ async getMessages(conversationId, opts) {
18485
19253
  const existing = this.#store.get(conversationId);
18486
- return existing === void 0 ? [] : existing.slice();
19254
+ return existing === void 0 ? [] : paginate(existing.slice(), opts);
18487
19255
  }
18488
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) {
18489
19263
  const existing = this.#store.get(conversationId);
18490
19264
  const stamped = message.at === void 0 ? { ...message, at: Date.now() } : message;
18491
19265
  if (existing === void 0) {
@@ -18494,9 +19268,26 @@ var InMemoryConversationStorage = class {
18494
19268
  }
18495
19269
  existing.push(stamped);
18496
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
+ }
18497
19278
  async deleteConversation(conversationId) {
18498
19279
  this.#store.delete(conversationId);
18499
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
+ }
18500
19291
  async listConversationIds(opts) {
18501
19292
  const all = Array.from(this.#store.keys());
18502
19293
  if (opts?.limit !== void 0) return all.slice(0, opts.limit);
@@ -18674,23 +19465,40 @@ function createNoopMemoryProvider() {
18674
19465
  }
18675
19466
  var JobQueue = class {
18676
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
+ }
18677
19475
  /**
18678
- * Enqueue a background function. Returns the job ID immediately.
18679
- * 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).
18680
19480
  */
18681
19481
  enqueue(fn) {
18682
19482
  const id = crypto.randomUUID();
18683
19483
  const job = { id, status: "pending" };
18684
19484
  this.jobs.set(id, job);
18685
- job.status = "running";
18686
- Promise.resolve().then(() => fn()).then((result) => {
18687
- if (job.status === "cancelled") return;
18688
- job.result = result;
18689
- job.status = "completed";
18690
- }).catch((err) => {
18691
- if (job.status === "cancelled") return;
18692
- job.error = err instanceof Error ? err.message : String(err);
18693
- 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));
18694
19502
  });
18695
19503
  return id;
18696
19504
  }
@@ -18701,17 +19509,39 @@ var JobQueue = class {
18701
19509
  return [...this.jobs.values()];
18702
19510
  }
18703
19511
  /**
18704
- * 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.
18705
19514
  */
18706
19515
  cancel(id) {
18707
19516
  const job = this.jobs.get(id);
18708
19517
  if (!job) return false;
18709
19518
  if (job.status === "pending" || job.status === "running") {
18710
19519
  job.status = "cancelled";
19520
+ this.controllers.get(id)?.abort();
18711
19521
  return true;
18712
19522
  }
18713
19523
  return false;
18714
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
+ }
18715
19545
  };
18716
19546
 
18717
19547
  // src/internal/memory/dreaming/run.ts
@@ -19204,24 +20034,42 @@ async function migrateSqliteToLance2(options) {
19204
20034
  }
19205
20035
 
19206
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
+ }
19207
20043
  var PermissionEngine = class {
19208
20044
  constructor(rules, options = {}) {
19209
20045
  this.rules = rules;
19210
- this.defaultAction = options.defaultAction ?? "allow";
20046
+ this.defaultAction = options.defaultAction ?? "ask";
19211
20047
  }
19212
20048
  rules;
19213
20049
  defaultAction;
19214
20050
  /**
19215
- * Evaluate a tool name against the rules. First match wins; falls back to the
19216
- * 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.
19217
20056
  */
19218
- evaluate(toolName) {
20057
+ evaluate(toolName, args) {
19219
20058
  for (const rule of this.rules) {
19220
- const matches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
19221
- 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;
19222
20063
  }
19223
20064
  return this.defaultAction;
19224
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
+ }
19225
20073
  };
19226
20074
 
19227
20075
  // src/permission-plugin.ts
@@ -19232,8 +20080,8 @@ function createPermissionPlugin(engine, opts = {}) {
19232
20080
  kind: "general",
19233
20081
  register(ctx) {
19234
20082
  ctx.on("pre_tool_call", (rawCtx) => {
19235
- const { name } = rawCtx;
19236
- const action = engine.evaluate(name);
20083
+ const { name, args } = rawCtx;
20084
+ const action = engine.evaluate(name, args);
19237
20085
  if (action === "deny") {
19238
20086
  return { block: true, message: `denied by permission engine: ${name}` };
19239
20087
  }
@@ -19298,6 +20146,14 @@ var Security = class {
19298
20146
  }
19299
20147
  };
19300
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
+
19301
20157
  // src/squad.ts
19302
20158
  init_errors();
19303
20159
  var PersistenceSchema = zod.z.object({
@@ -20048,6 +20904,8 @@ exports.migrateSqliteToLance = migrateSqliteToLance2;
20048
20904
  exports.mkMemoryId = mkMemoryId;
20049
20905
  exports.normalizeUsage = normalizeUsage;
20050
20906
  exports.preflightCheck = preflightCheck;
20907
+ exports.scopedConversationId = scopedConversationId;
20908
+ exports.sessionScopePrefix = sessionScopePrefix;
20051
20909
  exports.toShareGptTrajectory = toShareGptTrajectory;
20052
20910
  exports.withCwdMutex = withCwdMutex;
20053
20911
  //# sourceMappingURL=index.cjs.map