@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID, randomBytes } from 'crypto';
2
2
  import { existsSync, rmSync, mkdirSync, renameSync, readFileSync, realpathSync, lstatSync, readlinkSync, readdirSync } from 'fs';
3
3
  import { join, dirname, relative, resolve, sep, isAbsolute } from 'path';
4
- import { readFile, stat, rm, readdir, mkdir, appendFile, open, rename, unlink, statfs, access } from 'fs/promises';
4
+ import { readFile, stat, rm, readdir, mkdir, rename, open, unlink, statfs, access, appendFile } from 'fs/promises';
5
5
  import { z, toJSONSchema } from 'zod';
6
6
  import { AsyncLocalStorage } from 'async_hooks';
7
7
  import { createRequire } from 'module';
@@ -1198,6 +1198,16 @@ var init_credential_pool_types = __esm({
1198
1198
  });
1199
1199
 
1200
1200
  // src/internal/llm/retry.ts
1201
+ function computeBackoffMs(opts) {
1202
+ const base = opts.baseMs ?? DEFAULT_BASE_MS;
1203
+ const cap2 = opts.capMs ?? DEFAULT_CAP_MS;
1204
+ const rng = opts.rng ?? Math.random;
1205
+ if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
1206
+ return Math.max(base, Math.min(cap2, opts.retryAfterMs));
1207
+ }
1208
+ const ceiling = Math.min(cap2, base * 2 ** opts.attempt);
1209
+ return Math.floor(rng() * ceiling);
1210
+ }
1201
1211
  function sleepWithAbort(ms, signal) {
1202
1212
  if (ms <= 0 || signal.aborted) return Promise.resolve();
1203
1213
  return new Promise((resolve3) => {
@@ -1212,8 +1222,11 @@ function sleepWithAbort(ms, signal) {
1212
1222
  signal.addEventListener("abort", onAbort, { once: true });
1213
1223
  });
1214
1224
  }
1225
+ var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
1215
1226
  var init_retry = __esm({
1216
1227
  "src/internal/llm/retry.ts"() {
1228
+ DEFAULT_BASE_MS = 500;
1229
+ DEFAULT_CAP_MS = 32e3;
1217
1230
  }
1218
1231
  });
1219
1232
 
@@ -5280,7 +5293,7 @@ function abortRun(name, runId, startedAt, stepResults, signal) {
5280
5293
  }
5281
5294
  async function runStepsLoop(params) {
5282
5295
  const { options, steps, ctx, runId, startedAt, signal } = params;
5283
- const stepResults = [];
5296
+ const stepResults = [...params.initialStepResults ?? []];
5284
5297
  let acc = params.input;
5285
5298
  for (const step of steps) {
5286
5299
  if (signal.aborted) return abortRun(options.name, runId, startedAt, stepResults, signal);
@@ -5339,7 +5352,8 @@ async function executeWorkflow(options, steps, input, runOpts) {
5339
5352
  ctx,
5340
5353
  runId,
5341
5354
  startedAt,
5342
- signal
5355
+ signal,
5356
+ ...runOpts?.initialStepResults !== void 0 ? { initialStepResults: runOpts.initialStepResults } : {}
5343
5357
  });
5344
5358
  } finally {
5345
5359
  runSpan.end();
@@ -5422,7 +5436,9 @@ async function resumeWorkflow(opts) {
5422
5436
  await store.delete(opts.runId);
5423
5437
  return executeWorkflow(options, remainingSteps, resumeInput, {
5424
5438
  signal: opts.signal,
5425
- runId: opts.runId
5439
+ runId: opts.runId,
5440
+ // M3 #62 — restore prior step outputs so the resumed run is not lossy (internal seam).
5441
+ initialStepResults: snapshot.stepResults
5426
5442
  });
5427
5443
  }
5428
5444
  var init_executor = __esm({
@@ -7053,13 +7069,15 @@ function buildGitInfo() {
7053
7069
  init_errors();
7054
7070
 
7055
7071
  // src/internal/llm/sse.ts
7056
- async function* parseSseStream(body, signal) {
7072
+ init_errors();
7073
+ var DEFAULT_SSE_IDLE_MS = 6e4;
7074
+ async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
7057
7075
  if (body === null) return;
7058
7076
  const reader = body.getReader();
7059
7077
  const decoder = new TextDecoder("utf-8");
7060
7078
  const state4 = { buffer: "", event: "message", data: "" };
7061
7079
  try {
7062
- for await (const chunk of readChunks(reader, signal)) {
7080
+ for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
7063
7081
  state4.buffer += decoder.decode(chunk, { stream: true });
7064
7082
  for (const record of drainCompleteRecords(state4)) yield record;
7065
7083
  }
@@ -7069,14 +7087,38 @@ async function* parseSseStream(body, signal) {
7069
7087
  releaseReader(reader);
7070
7088
  }
7071
7089
  }
7072
- async function* readChunks(reader, signal) {
7090
+ async function* readChunks(reader, signal, idleTimeoutMs) {
7073
7091
  while (true) {
7074
7092
  if (signal.aborted) return;
7075
- const { value, done } = await reader.read();
7093
+ const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
7076
7094
  if (done) return;
7077
7095
  if (value !== void 0) yield value;
7078
7096
  }
7079
7097
  }
7098
+ function readWithIdleTimeout(reader, idleTimeoutMs) {
7099
+ if (idleTimeoutMs <= 0) return reader.read();
7100
+ return new Promise(
7101
+ (resolve3, reject) => {
7102
+ const timer = setTimeout(() => {
7103
+ reject(
7104
+ new NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
7105
+ code: "stream_idle_timeout"
7106
+ })
7107
+ );
7108
+ }, idleTimeoutMs);
7109
+ reader.read().then(
7110
+ (result) => {
7111
+ clearTimeout(timer);
7112
+ resolve3(result);
7113
+ },
7114
+ (err) => {
7115
+ clearTimeout(timer);
7116
+ reject(err);
7117
+ }
7118
+ );
7119
+ }
7120
+ );
7121
+ }
7080
7122
  async function cancelReaderQuietly(reader) {
7081
7123
  try {
7082
7124
  await reader.cancel();
@@ -7770,6 +7812,9 @@ var PersonalityStore = class {
7770
7812
  }
7771
7813
  };
7772
7814
 
7815
+ // src/internal/plugins/manager.ts
7816
+ init_errors();
7817
+
7773
7818
  // src/internal/plugins/context.ts
7774
7819
  function createPluginContext() {
7775
7820
  const registrations = {
@@ -7831,6 +7876,9 @@ var PluginManager = class {
7831
7876
  memoryProviders: []
7832
7877
  };
7833
7878
  #initialized = false;
7879
+ // #68 — registrations of plugins added post-init via `register()`, keyed by
7880
+ // plugin name so a re-register REPLACES (not appends) the prior hooks.
7881
+ #byName = /* @__PURE__ */ new Map();
7834
7882
  async initialize(plugins) {
7835
7883
  if (this.#initialized) {
7836
7884
  throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
@@ -7848,6 +7896,36 @@ var PluginManager = class {
7848
7896
  await this.#dispatchPlugin(plugin);
7849
7897
  }
7850
7898
  }
7899
+ /**
7900
+ * #68 — register a single `general` plugin AFTER `initialize()` has run.
7901
+ *
7902
+ * The bulk `initialize()` is single-shot (one call per process); late
7903
+ * registration is a distinct, named operation used by adapters that install
7904
+ * a plugin per-session/per-request (e.g. the ACP permission veto, which is
7905
+ * installed once the permission mode + connection are known — after the
7906
+ * agent's own plugins were already initialized).
7907
+ *
7908
+ * Idempotent by plugin NAME: re-registering a plugin with the same name
7909
+ * REPLACES its prior hooks/tools instead of appending duplicates (the ACP
7910
+ * permission plugin is re-installed on every prompt).
7911
+ *
7912
+ * Only `general` plugins may be registered late — model-provider / memory
7913
+ * plugins are resolved during the bulk init and cannot be added afterwards.
7914
+ */
7915
+ async register(plugin) {
7916
+ if (plugin.kind !== "general") {
7917
+ throw new ConfigurationError(
7918
+ `late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
7919
+ { code: "plugin_late_register_kind" }
7920
+ );
7921
+ }
7922
+ const prior = this.#byName.get(plugin.name);
7923
+ if (prior !== void 0) this.#unmerge(prior);
7924
+ const { ctx, registrations } = createPluginContext();
7925
+ await plugin.register(ctx);
7926
+ this.#byName.set(plugin.name, registrations);
7927
+ this.#merge(registrations);
7928
+ }
7851
7929
  get aggregated() {
7852
7930
  return this.#aggregated;
7853
7931
  }
@@ -7928,6 +8006,64 @@ var PluginManager = class {
7928
8006
  }
7929
8007
  }
7930
8008
  }
8009
+ // #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
8010
+ // in order (per-handler errors logged, never thrown); transform hooks fold
8011
+ // over the payload (a handler returning a value replaces it).
8012
+ /** @internal */
8013
+ async #runFireAndForget(name, ctx) {
8014
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
8015
+ try {
8016
+ await h(ctx);
8017
+ } catch (err) {
8018
+ process.stderr.write(
8019
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
8020
+ `
8021
+ );
8022
+ }
8023
+ }
8024
+ }
8025
+ /** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
8026
+ async #runTransform(name, payload, ctx) {
8027
+ let current = payload;
8028
+ for (const h of this.#aggregated.hooks.get(name) ?? []) {
8029
+ try {
8030
+ const out = await h(current, ctx);
8031
+ if (out !== void 0) current = out;
8032
+ } catch (err) {
8033
+ process.stderr.write(
8034
+ `[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
8035
+ `
8036
+ );
8037
+ }
8038
+ }
8039
+ return current;
8040
+ }
8041
+ /** #65 — fired after a tool call completes. @internal */
8042
+ runPostToolCallHooks(ctx) {
8043
+ return this.#runFireAndForget("post_tool_call", ctx);
8044
+ }
8045
+ /** #65 — fired before / after each LLM turn. @internal */
8046
+ runPreLlmCallHooks(ctx) {
8047
+ return this.#runFireAndForget("pre_llm_call", ctx);
8048
+ }
8049
+ runPostLlmCallHooks(ctx) {
8050
+ return this.#runFireAndForget("post_llm_call", ctx);
8051
+ }
8052
+ /** #65 — fired at run start / end. @internal */
8053
+ runOnSessionStartHooks(ctx) {
8054
+ return this.#runFireAndForget("on_session_start", ctx);
8055
+ }
8056
+ runOnSessionEndHooks(ctx) {
8057
+ return this.#runFireAndForget("on_session_end", ctx);
8058
+ }
8059
+ /** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
8060
+ runTransformToolResultHooks(results, ctx) {
8061
+ return this.#runTransform("transform_tool_result", results, ctx);
8062
+ }
8063
+ /** #65 — transform the LLM output text before it is consumed. @internal */
8064
+ runTransformLlmOutputHooks(output, ctx) {
8065
+ return this.#runTransform("transform_llm_output", output, ctx);
8066
+ }
7931
8067
  async #dispatchPlugin(plugin) {
7932
8068
  if (plugin.kind === "general") {
7933
8069
  const { ctx, registrations } = createPluginContext();
@@ -7955,7 +8091,29 @@ var PluginManager = class {
7955
8091
  }
7956
8092
  this.#aggregated.injected.push(...r.injected);
7957
8093
  }
8094
+ /**
8095
+ * #68 — inverse of #merge: remove a prior registration's contributions from
8096
+ * the aggregated view by object identity. Used by `register()` to replace a
8097
+ * same-named plugin's hooks/tools instead of accumulating duplicates.
8098
+ */
8099
+ #unmerge(r) {
8100
+ removeAll(this.#aggregated.tools, r.tools);
8101
+ removeAll(this.#aggregated.commands, r.commands);
8102
+ removeAll(this.#aggregated.injected, r.injected);
8103
+ for (const [hook, handlers] of r.hooks.entries()) {
8104
+ const existing = this.#aggregated.hooks.get(hook);
8105
+ if (existing === void 0) continue;
8106
+ removeAll(existing, handlers);
8107
+ if (existing.length === 0) this.#aggregated.hooks.delete(hook);
8108
+ }
8109
+ }
7958
8110
  };
8111
+ function removeAll(arr, toRemove) {
8112
+ for (const item of toRemove) {
8113
+ const idx = arr.indexOf(item);
8114
+ if (idx !== -1) arr.splice(idx, 1);
8115
+ }
8116
+ }
7959
8117
 
7960
8118
  // src/internal/telemetry/span-names.ts
7961
8119
  var SPAN_NAMES = {
@@ -7963,7 +8121,12 @@ var SPAN_NAMES = {
7963
8121
  AGENT_SEND: "agent.send",
7964
8122
  MEMORY_RECALL: "memory.recall"};
7965
8123
  var HISTOGRAM_NAMES = {
7966
- MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
8124
+ MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
8125
+ TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
8126
+ LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
8127
+ LLM_TOKENS: "theokit_llm_tokens",
8128
+ /** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
8129
+ LLM_USAGE_MISSING: "theokit_llm_usage_missing"
7967
8130
  };
7968
8131
 
7969
8132
  // src/internal/telemetry/tracer.ts
@@ -8301,7 +8464,25 @@ function createTelemetry(settings) {
8301
8464
  enabled: true,
8302
8465
  includeContent: settings.includeContent === true,
8303
8466
  startSpan: startNewSpan,
8304
- startChildSpan: (_parent, name, attrs) => startNewSpan(name, attrs),
8467
+ // M3 #64 actually nest the child under its parent instead of discarding it.
8468
+ // The parent's SpanContext is set on a fresh OTel context so the child links
8469
+ // to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
8470
+ // back to a root span when the parent has no valid span id (telemetry off /
8471
+ // NOOP), preserving the pre-M3 behavior for parentless callers.
8472
+ startChildSpan: (parent, name, attrs) => {
8473
+ const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
8474
+ const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
8475
+ const pctx = safe(() => parent?.spanContext(), void 0);
8476
+ const span = safe(() => {
8477
+ if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
8478
+ const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
8479
+ return tracer.startSpan(name, opts, childCtx);
8480
+ }
8481
+ return tracer.startSpan(name, opts);
8482
+ }, NOOP_SPAN);
8483
+ if (span !== NOOP_SPAN) openSpans.add(span);
8484
+ return wrapSpan(span, openSpans);
8485
+ },
8305
8486
  recordHistogram,
8306
8487
  endAll: () => {
8307
8488
  for (const span of openSpans) safe(() => span.end(), void 0);
@@ -8337,12 +8518,62 @@ function redactAttrs(attrs) {
8337
8518
  }
8338
8519
  return out;
8339
8520
  }
8521
+
8522
+ // src/internal/runtime/lifecycle/env-policy.ts
8523
+ var SECRET_PATTERNS = [
8524
+ /KEY/i,
8525
+ /SECRET/i,
8526
+ /TOKEN/i,
8527
+ /PASSWORD/i,
8528
+ /PASSWD/i,
8529
+ /PASSPHRASE/i,
8530
+ /[_-]PWD/i,
8531
+ /CREDENTIAL/i,
8532
+ /PRIVATE/i,
8533
+ /_AUTH/i
8534
+ ];
8535
+ var CORE_VARS = [
8536
+ "PATH",
8537
+ "HOME",
8538
+ "SHELL",
8539
+ "LANG",
8540
+ "LC_ALL",
8541
+ "LC_CTYPE",
8542
+ "TMPDIR",
8543
+ "TMP",
8544
+ "TEMP",
8545
+ "USER",
8546
+ "LOGNAME"
8547
+ ];
8548
+ function isSecretName(name) {
8549
+ return SECRET_PATTERNS.some((re) => re.test(name));
8550
+ }
8551
+ function inheritsUnderPolicy(name, policy) {
8552
+ if (policy === "all") return true;
8553
+ if (policy === "core") return CORE_VARS.includes(name);
8554
+ return !isSecretName(name);
8555
+ }
8556
+ function resolveChildEnv(options = {}) {
8557
+ const parent = options.parent ?? process.env;
8558
+ const policy = options.policy ?? "inherit-scrubbed";
8559
+ const base = {};
8560
+ for (const [name, value] of Object.entries(parent)) {
8561
+ if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
8562
+ }
8563
+ for (const [name, value] of Object.entries(options.overrides ?? {})) {
8564
+ base[name] = value;
8565
+ }
8566
+ return base;
8567
+ }
8568
+
8569
+ // src/internal/runtime/lifecycle/spawn-collect.ts
8340
8570
  function spawnAndCollect(options) {
8341
8571
  return new Promise((resolve3) => {
8342
8572
  const timeoutMs = options.timeoutMs ?? 3e4;
8343
8573
  const spawnOptions = {
8344
8574
  cwd: options.cwd,
8345
- env: { ...process.env, ...options.env ?? {} }
8575
+ // #54 — scrub secret-like parent env by default; `options.env` still wins.
8576
+ env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
8346
8577
  };
8347
8578
  const child = spawn(options.command, options.args ?? [], spawnOptions);
8348
8579
  let stdout = "";
@@ -8524,15 +8755,24 @@ function warnMalformed(agentId, line) {
8524
8755
  `
8525
8756
  );
8526
8757
  }
8758
+ function hydrateSessionLine(parsed) {
8759
+ if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
8760
+ if (parsed.role === "user" || parsed.role === "assistant") {
8761
+ return { role: parsed.role, text: parsed.text };
8762
+ }
8763
+ if (parsed.role === "tool_call" || parsed.role === "tool_result") {
8764
+ const label = parsed.role === "tool_call" ? "tool call" : "tool result";
8765
+ return { role: "assistant", text: `[${label}] ${parsed.text}` };
8766
+ }
8767
+ return void 0;
8768
+ }
8527
8769
  async function readSessionFile(cwd, agentId) {
8528
8770
  const lines = await readJsonlLines(cwd, agentId);
8529
8771
  const messages = [];
8530
8772
  for (const line of lines) {
8531
8773
  try {
8532
- const parsed = JSON.parse(line);
8533
- if ((parsed.role === "user" || parsed.role === "assistant") && typeof parsed.text === "string") {
8534
- messages.push({ role: parsed.role, text: parsed.text });
8535
- }
8774
+ const msg = hydrateSessionLine(JSON.parse(line));
8775
+ if (msg !== void 0) messages.push(msg);
8536
8776
  } catch {
8537
8777
  warnMalformed(agentId, line);
8538
8778
  }
@@ -8559,28 +8799,78 @@ async function readAllPersistedMessages(cwd, agentId) {
8559
8799
  return messages;
8560
8800
  }
8561
8801
  async function appendAnyPersistedMessage(cwd, agentId, record) {
8562
- const path = sessionFilePath(cwd, agentId);
8563
- await mkdir(dirname(path), { recursive: true });
8564
- await appendFile(path, `${redactSecrets(JSON.stringify(record))}
8565
- `, "utf8");
8802
+ await appendPersistedMessages(cwd, agentId, [record]);
8566
8803
  }
8567
- async function compactSessionFile(cwd, agentId, maxTurns) {
8804
+ async function appendPersistedMessages(cwd, agentId, records) {
8805
+ if (records.length === 0) return;
8568
8806
  const path = sessionFilePath(cwd, agentId);
8569
- let raw;
8807
+ const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
8808
+ `).join("");
8809
+ const dir = dirname(path);
8810
+ let written = false;
8811
+ const attempt = async () => {
8812
+ await mkdir(dir, { recursive: true });
8813
+ await withFileLock(path, async () => {
8814
+ await appendFile(path, payload, "utf8");
8815
+ written = true;
8816
+ });
8817
+ };
8570
8818
  try {
8571
- raw = await readFile(path, "utf8");
8572
- } catch {
8573
- return;
8819
+ await attempt();
8820
+ } catch (cause) {
8821
+ if (written || cause.code !== "ENOENT") throw cause;
8822
+ await attempt();
8574
8823
  }
8575
- const lines = raw.split("\n").filter((line) => line.length > 0);
8576
- if (lines.length <= maxTurns * 2) return;
8577
- const trimmed = `${lines.slice(-maxTurns).join("\n")}
8824
+ }
8825
+ async function rewriteLockedSession(path, transform) {
8826
+ await withFileLock(path, async () => {
8827
+ let raw;
8828
+ try {
8829
+ raw = await readFile(path, "utf8");
8830
+ } catch {
8831
+ return;
8832
+ }
8833
+ const lines = raw.split("\n").filter((line) => line.length > 0);
8834
+ const next = transform(lines);
8835
+ if (next === void 0) return;
8836
+ await replaceFileAtomic(path, next);
8837
+ });
8838
+ }
8839
+ async function compactSessionFile(cwd, agentId, maxTurns) {
8840
+ const path = sessionFilePath(cwd, agentId);
8841
+ if (!existsSync(path)) return;
8842
+ await rewriteLockedSession(
8843
+ path,
8844
+ (lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
8845
+ `
8846
+ );
8847
+ }
8848
+ async function truncateSessionTo(cwd, agentId, keepCount) {
8849
+ const path = sessionFilePath(cwd, agentId);
8850
+ if (!existsSync(path)) return 0;
8851
+ let kept = 0;
8852
+ await rewriteLockedSession(path, (lines) => {
8853
+ const keep = Math.max(0, Math.min(keepCount, lines.length));
8854
+ kept = keep;
8855
+ if (keep === lines.length) return void 0;
8856
+ return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
8578
8857
  `;
8579
- await replaceFileAtomic(path, trimmed);
8858
+ });
8859
+ return kept;
8580
8860
  }
8581
8861
 
8582
8862
  // src/internal/persistence/conversation-storage-fs.ts
8583
8863
  init_security();
8864
+
8865
+ // src/internal/persistence/pagination.ts
8866
+ function paginate(items, opts) {
8867
+ if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
8868
+ const start = Math.max(0, opts.offset ?? 0);
8869
+ const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
8870
+ return items.slice(start, end);
8871
+ }
8872
+
8873
+ // src/internal/persistence/conversation-storage-fs.ts
8584
8874
  var FileSystemConversationStorage = class {
8585
8875
  #root;
8586
8876
  constructor(opts = {}) {
@@ -8590,23 +8880,31 @@ var FileSystemConversationStorage = class {
8590
8880
  get root() {
8591
8881
  return this.#root;
8592
8882
  }
8593
- async getMessages(conversationId) {
8883
+ async getMessages(conversationId, opts) {
8594
8884
  const records = await readAllPersistedMessages(this.#root, conversationId);
8595
- return records.map(toStoredMessage);
8885
+ const all = records.map(toStoredMessage);
8886
+ return paginate(all, opts);
8596
8887
  }
8597
8888
  async appendMessage(conversationId, message) {
8598
- const record = {
8599
- role: message.role,
8600
- text: message.content,
8601
- at: message.at ?? Date.now()
8602
- };
8603
- await appendAnyPersistedMessage(this.#root, conversationId, record);
8889
+ await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
8890
+ }
8891
+ async appendMessages(conversationId, messages) {
8892
+ await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
8893
+ }
8894
+ async truncateConversation(conversationId, keepCount) {
8895
+ return truncateSessionTo(this.#root, conversationId, keepCount);
8604
8896
  }
8605
8897
  async deleteConversation(conversationId) {
8606
8898
  const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
8607
8899
  const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
8608
8900
  await rm(dirPath, { recursive: true, force: true });
8609
8901
  }
8902
+ async deleteScope(prefix) {
8903
+ const ids = await this.listConversationIds();
8904
+ const matching = ids.filter((id) => id.startsWith(prefix));
8905
+ for (const id of matching) await this.deleteConversation(id);
8906
+ return matching.length;
8907
+ }
8610
8908
  async listConversationIds(opts = {}) {
8611
8909
  const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
8612
8910
  let entries;
@@ -8632,6 +8930,9 @@ function toStoredMessage(record) {
8632
8930
  at: record.at
8633
8931
  };
8634
8932
  }
8933
+ function toRecord(message) {
8934
+ return { role: message.role, text: message.content, at: message.at ?? Date.now() };
8935
+ }
8635
8936
 
8636
8937
  // src/internal/runtime/session/agent-session.ts
8637
8938
  var DEFAULT_MAX_TURNS = 200;
@@ -8717,12 +9018,19 @@ async function readPersistedForCache(adapter, agentId) {
8717
9018
  const records = await adapter.getMessages(agentId);
8718
9019
  const out = [];
8719
9020
  for (const r of records) {
8720
- if (r.role === "user" || r.role === "assistant") {
8721
- out.push({ role: r.role, text: r.content });
8722
- }
9021
+ const folded = foldStoredToSession(r);
9022
+ if (folded !== void 0) out.push(folded);
8723
9023
  }
8724
9024
  return out;
8725
9025
  }
9026
+ function foldStoredToSession(r) {
9027
+ if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
9028
+ if (r.role === "tool_call" || r.role === "tool_result") {
9029
+ const label = r.role === "tool_call" ? "tool call" : "tool result";
9030
+ return { role: "assistant", text: `[${label}] ${r.content}` };
9031
+ }
9032
+ return void 0;
9033
+ }
8726
9034
  async function flushSessionWrites() {
8727
9035
  while (pendingAppends.size > 0) {
8728
9036
  const all = Array.from(pendingAppends.values());
@@ -11074,11 +11382,32 @@ function reasoningEffortFromParams(params) {
11074
11382
  const thinking = params?.find((p) => p.id === "thinking");
11075
11383
  return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
11076
11384
  }
11385
+ function emitLlmMetrics(inputs, result, startAt) {
11386
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
11387
+ provider: inputs.llm.name
11388
+ });
11389
+ if (result.inputTokens === void 0 && result.outputTokens === void 0) {
11390
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
11391
+ provider: inputs.llm.name
11392
+ });
11393
+ process.stderr.write(
11394
+ `[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
11395
+ `
11396
+ );
11397
+ return;
11398
+ }
11399
+ inputs.telemetry?.recordHistogram(
11400
+ HISTOGRAM_NAMES.LLM_TOKENS,
11401
+ (result.inputTokens ?? 0) + (result.outputTokens ?? 0),
11402
+ { provider: inputs.llm.name }
11403
+ );
11404
+ }
11077
11405
  async function streamLlmTurn(inputs, ctx) {
11078
- const llmSpan = inputs.telemetry?.startSpan("llm.call", {
11406
+ const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
11079
11407
  "model.id": inputs.model.id ?? "auto",
11080
11408
  provider: inputs.llm.name
11081
11409
  });
11410
+ const startAt = Date.now();
11082
11411
  const signal = inputs.signal ?? new AbortController().signal;
11083
11412
  const generator = inputs.llm.stream(
11084
11413
  {
@@ -11121,6 +11450,7 @@ async function streamLlmTurn(inputs, ctx) {
11121
11450
  inputTokens: result.inputTokens ?? 0,
11122
11451
  outputTokens: result.outputTokens ?? 0
11123
11452
  });
11453
+ emitLlmMetrics(inputs, result, startAt);
11124
11454
  llmSpan?.end();
11125
11455
  const stripped = stripThinkBlocks(collected.accumulatedText);
11126
11456
  return {
@@ -11350,21 +11680,21 @@ async function executeTool(inputs, resolved, call) {
11350
11680
  }
11351
11681
  if (resolved.origin === "shell") return runShellTool(inputs, call);
11352
11682
  if (resolved.origin === "memory") return runMemoryTool(resolved, call);
11353
- if (resolved.origin === "custom") return runCustomTool(resolved, call);
11683
+ if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
11354
11684
  return runMcpTool(inputs, resolved, call);
11355
11685
  }
11356
11686
  async function runMemoryTool(resolved, call) {
11357
11687
  return runHandlerTool("memory", resolved.memoryHandler, call);
11358
11688
  }
11359
- async function runCustomTool(resolved, call) {
11360
- return runHandlerTool("custom", resolved.customHandler, call);
11689
+ async function runCustomTool(resolved, call, signal) {
11690
+ return runHandlerTool("custom", resolved.customHandler, call, signal);
11361
11691
  }
11362
- async function runHandlerTool(kind, handler, call) {
11692
+ async function runHandlerTool(kind, handler, call, signal) {
11363
11693
  if (handler === void 0) {
11364
11694
  return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
11365
11695
  }
11366
11696
  try {
11367
- const stdout = await handler(call.input);
11697
+ const stdout = await handler(call.input, { signal });
11368
11698
  return { stdout, stderr: "", exitCode: 0 };
11369
11699
  } catch (cause) {
11370
11700
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -11410,22 +11740,66 @@ ${result.stderr}`.trim();
11410
11740
  return result.stdout.trim();
11411
11741
  }
11412
11742
 
11743
+ // src/internal/agent-loop/tool-timeout.ts
11744
+ var TOOL_ABORTED_EXIT = 124;
11745
+ function abortedResult(signal) {
11746
+ const reason = signal.reason;
11747
+ const timedOut = reason?.name === "TimeoutError";
11748
+ return {
11749
+ stdout: "",
11750
+ stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
11751
+ exitCode: TOOL_ABORTED_EXIT
11752
+ };
11753
+ }
11754
+ function raceToolExecution(exec, opts) {
11755
+ const { signal, timeoutMs } = opts;
11756
+ if (signal === void 0 && timeoutMs === void 0) return exec;
11757
+ const signals = [];
11758
+ if (signal !== void 0) signals.push(signal);
11759
+ if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
11760
+ const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
11761
+ if (merged.aborted) return Promise.resolve(abortedResult(merged));
11762
+ return new Promise((resolve3, reject) => {
11763
+ let settled = false;
11764
+ const onAbort = () => {
11765
+ if (settled) return;
11766
+ settled = true;
11767
+ resolve3(abortedResult(merged));
11768
+ };
11769
+ merged.addEventListener("abort", onAbort, { once: true });
11770
+ exec.then(
11771
+ (r) => {
11772
+ if (settled) return;
11773
+ settled = true;
11774
+ merged.removeEventListener("abort", onAbort);
11775
+ resolve3(r);
11776
+ },
11777
+ (e) => {
11778
+ if (settled) return;
11779
+ settled = true;
11780
+ merged.removeEventListener("abort", onAbort);
11781
+ reject(e);
11782
+ }
11783
+ );
11784
+ });
11785
+ }
11786
+
11413
11787
  // src/internal/agent-loop/tool-dispatch.ts
11414
- async function dispatchTools(inputs, tools, toolCalls, events) {
11788
+ async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
11415
11789
  const maxConcurrent = inputs.maxConcurrentTools ?? 4;
11416
11790
  return mapWithConcurrency(
11417
11791
  toolCalls,
11418
11792
  maxConcurrent,
11419
- (call) => dispatchSingleCall(inputs, tools, call, events)
11793
+ (call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
11420
11794
  );
11421
11795
  }
11422
- async function dispatchSingleCall(inputs, tools, call, events) {
11796
+ async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
11423
11797
  const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
11424
11798
  const callId = generateCallId();
11425
11799
  const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
11426
11800
  if (forkVeto !== void 0) return forkVeto;
11427
11801
  const resolved = tools.find((tool) => tool.name === workingCall.name);
11428
- const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
11802
+ const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
11429
11803
  events.push(buildToolUseRunning(inputs, callId, workingCall));
11430
11804
  const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
11431
11805
  if (pluginVeto !== void 0) {
@@ -11442,6 +11816,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
11442
11816
  return fileVeto;
11443
11817
  }
11444
11818
  const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
11819
+ await inputs.pluginManager?.runPostToolCallHooks({
11820
+ name: workingCall.name,
11821
+ args: workingCall.input,
11822
+ result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
11823
+ agentId: inputs.agentId,
11824
+ runId: inputs.runId
11825
+ });
11445
11826
  return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
11446
11827
  }
11447
11828
  function applyRepairAndExtractCall(tools, call) {
@@ -11474,8 +11855,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
11474
11855
  content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
11475
11856
  };
11476
11857
  }
11477
- function startToolCallSpan(inputs, call, resolved, callId, repairs) {
11478
- const toolSpan = inputs.telemetry?.startSpan("tool.call", {
11858
+ function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
11859
+ const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
11479
11860
  "tool.name": call.name,
11480
11861
  "tool.origin": resolved?.origin ?? "unknown",
11481
11862
  callId
@@ -11539,8 +11920,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
11539
11920
  conversationId: inputs.agentId,
11540
11921
  callId
11541
11922
  });
11542
- const result = await executeTool(inputs, resolved, call);
11923
+ const result = await raceToolExecution(executeTool(inputs, resolved, call), {
11924
+ signal: inputs.signal,
11925
+ timeoutMs: inputs.perToolTimeoutMs
11926
+ });
11543
11927
  const durationMs = Date.now() - startAt;
11928
+ inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
11929
+ "tool.name": call.name
11930
+ });
11544
11931
  if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
11545
11932
  await safeEmitToolHook(inputs.onToolError, {
11546
11933
  toolName: call.name,
@@ -11639,6 +12026,35 @@ function buildToolUseCompleted(inputs, callId, call, result) {
11639
12026
  };
11640
12027
  }
11641
12028
 
12029
+ // src/internal/agent-loop/tool-result-guard.ts
12030
+ var OPEN = "<untrusted-tool-output>";
12031
+ var CLOSE = "</untrusted-tool-output>";
12032
+ var PII_PATTERNS = [
12033
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
12034
+ // email
12035
+ /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
12036
+ // phone
12037
+ ];
12038
+ function guardText(content, opts) {
12039
+ let out = content;
12040
+ if (opts.redactPii === true) {
12041
+ for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
12042
+ }
12043
+ if (opts.delimit === true) {
12044
+ const safe2 = out.split(CLOSE).join("</ untrusted-tool-output>");
12045
+ out = `${OPEN}
12046
+ ${safe2}
12047
+ ${CLOSE}`;
12048
+ }
12049
+ return out;
12050
+ }
12051
+ function applyToolResultGuard(parts, opts) {
12052
+ if (opts.delimit !== true && opts.redactPii !== true) return parts;
12053
+ return parts.map(
12054
+ (p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
12055
+ );
12056
+ }
12057
+
11642
12058
  // src/internal/budget/pricing-data.json
11643
12059
  var pricing_data_default = {
11644
12060
  _meta: {
@@ -11989,6 +12405,11 @@ async function runAgentLoop(inputs) {
11989
12405
  try {
11990
12406
  const ctx = await initLoopContext(inputs);
11991
12407
  ctxRef = ctx;
12408
+ ctx.sendSpan = sendSpan;
12409
+ await inputs.pluginManager?.runOnSessionStartHooks({
12410
+ agentId: inputs.agentId,
12411
+ runId: inputs.runId
12412
+ });
11992
12413
  const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
11993
12414
  let lastTurnDecision;
11994
12415
  while (budget.shouldContinue()) {
@@ -12016,6 +12437,7 @@ async function runAgentLoop(inputs) {
12016
12437
  }
12017
12438
  budget.consume();
12018
12439
  inputs.budgetTracker?.nextIteration?.();
12440
+ if (inputs.signal?.aborted === true) break;
12019
12441
  }
12020
12442
  if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
12021
12443
  ctx.stoppedAtIterationLimit = true;
@@ -12055,6 +12477,10 @@ async function runAgentLoop(inputs) {
12055
12477
  ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
12056
12478
  };
12057
12479
  } finally {
12480
+ await inputs.pluginManager?.runOnSessionEndHooks({
12481
+ agentId: inputs.agentId,
12482
+ runId: inputs.runId
12483
+ });
12058
12484
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
12059
12485
  try {
12060
12486
  await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
@@ -12158,7 +12584,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
12158
12584
  return "done";
12159
12585
  }
12160
12586
  async function runIteration(inputs, ctx) {
12587
+ const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
12588
+ await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
12161
12589
  const llmOutput = await streamLlmTurn(inputs, ctx);
12590
+ await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
12162
12591
  accumulateUsage(ctx.usage, llmOutput);
12163
12592
  if (inputs.budgetTracker !== void 0) {
12164
12593
  const modelId = inputs.model.id ?? "auto";
@@ -12184,6 +12613,13 @@ async function runIteration(inputs, ctx) {
12184
12613
  }
12185
12614
  return continueOrTerminate(inputs, ctx, llmOutput);
12186
12615
  }
12616
+ async function transformLlmOutputText(inputs, text, ctx) {
12617
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
12618
+ }
12619
+ async function guardAndTransformToolResults(inputs, raw, ctx) {
12620
+ const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
12621
+ return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
12622
+ }
12187
12623
  async function continueOrTerminate(inputs, ctx, llmOutput) {
12188
12624
  if (llmOutput.errored) return "error";
12189
12625
  if (llmOutput.text.length > 0) {
@@ -12192,8 +12628,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
12192
12628
  if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
12193
12629
  return finishOrReflect(inputs, ctx, llmOutput);
12194
12630
  }
12195
- ctx.messages.push(buildAssistantTurn(llmOutput.text, llmOutput.toolCalls));
12196
- const toolResults = await dispatchTools(inputs, ctx.tools, llmOutput.toolCalls, ctx.events);
12631
+ const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
12632
+ const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
12633
+ ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
12634
+ const rawResults = await dispatchTools(
12635
+ inputs,
12636
+ ctx.tools,
12637
+ llmOutput.toolCalls,
12638
+ ctx.events,
12639
+ ctx.sendSpan
12640
+ // M3 #64 — nest tool.call spans under agent.send
12641
+ );
12642
+ const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
12197
12643
  ctx.messages.push({ role: "user", content: toolResults });
12198
12644
  if (inputs.onStep !== void 0) {
12199
12645
  const cb = inputs.onStep;
@@ -12638,6 +13084,56 @@ function mapAnthropicStatusToCode(status, body) {
12638
13084
  function formatMessage(status, code) {
12639
13085
  return `Anthropic API error: ${code} (HTTP ${status})`;
12640
13086
  }
13087
+ var cachedJsonrepair;
13088
+ function loadJsonrepair() {
13089
+ if (cachedJsonrepair === void 0) {
13090
+ const req = createRequire(import.meta.url);
13091
+ cachedJsonrepair = req("jsonrepair").jsonrepair;
13092
+ }
13093
+ return cachedJsonrepair;
13094
+ }
13095
+ function isPlainObject(v) {
13096
+ return v !== null && typeof v === "object" && !Array.isArray(v);
13097
+ }
13098
+ function toFiniteNumber(raw) {
13099
+ if (raw === "") return void 0;
13100
+ const n = Number(raw);
13101
+ return Number.isFinite(n) && String(n) === raw ? n : void 0;
13102
+ }
13103
+ function tryJson(raw, repair) {
13104
+ const t = raw.trimStart();
13105
+ if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
13106
+ try {
13107
+ return JSON.parse(repair ? loadJsonrepair()(t) : t);
13108
+ } catch {
13109
+ return void 0;
13110
+ }
13111
+ }
13112
+ function heuristicCoerce(raw, repairJson) {
13113
+ if (raw === "true") return true;
13114
+ if (raw === "false") return false;
13115
+ if (raw === "null") return null;
13116
+ const n = toFiniteNumber(raw);
13117
+ if (n !== void 0) return n;
13118
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13119
+ return json === void 0 ? raw : json;
13120
+ }
13121
+ function coerceCandidates(raw, repairJson) {
13122
+ const out = [];
13123
+ if (raw === "true") out.push(true);
13124
+ else if (raw === "false") out.push(false);
13125
+ else if (raw === "null") out.push(null);
13126
+ const n = toFiniteNumber(raw);
13127
+ if (n !== void 0) out.push(n);
13128
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13129
+ if (json !== void 0) out.push(json);
13130
+ out.push(raw);
13131
+ return out;
13132
+ }
13133
+ function objectShape(schema) {
13134
+ const shape = schema?.shape;
13135
+ return shape !== null && typeof shape === "object" ? shape : void 0;
13136
+ }
12641
13137
 
12642
13138
  // src/internal/llm/finish.ts
12643
13139
  function collapseSystemText(system) {
@@ -12650,9 +13146,21 @@ function parseToolArguments(buffered) {
12650
13146
  try {
12651
13147
  return JSON.parse(buffered);
12652
13148
  } catch {
13149
+ const repaired = tryJson(buffered, true);
13150
+ if (isPlainObject(repaired)) return repaired;
12653
13151
  return { raw: buffered };
12654
13152
  }
12655
13153
  }
13154
+ function mapOpenAIFinish(reason) {
13155
+ switch (reason) {
13156
+ case "tool_calls":
13157
+ return "tool_use";
13158
+ case "length":
13159
+ return "max_tokens";
13160
+ default:
13161
+ return "end_turn";
13162
+ }
13163
+ }
12656
13164
  function makeLlmFinish(state4) {
12657
13165
  const finish = {
12658
13166
  stopReason: state4.stopReason,
@@ -13534,56 +14042,9 @@ function toOllamaTools(tools) {
13534
14042
  }
13535
14043
  }));
13536
14044
  }
13537
- var cachedJsonrepair;
13538
- function loadJsonrepair() {
13539
- if (cachedJsonrepair === void 0) {
13540
- const req = createRequire(import.meta.url);
13541
- cachedJsonrepair = req("jsonrepair").jsonrepair;
13542
- }
13543
- return cachedJsonrepair;
13544
- }
13545
- function isPlainObject(v) {
13546
- return v !== null && typeof v === "object" && !Array.isArray(v);
13547
- }
13548
- function toFiniteNumber(raw) {
13549
- if (raw === "") return void 0;
13550
- const n = Number(raw);
13551
- return Number.isFinite(n) && String(n) === raw ? n : void 0;
13552
- }
13553
- function tryJson(raw, repair) {
13554
- const t = raw.trimStart();
13555
- if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
13556
- try {
13557
- return JSON.parse(repair ? loadJsonrepair()(t) : t);
13558
- } catch {
13559
- return void 0;
13560
- }
13561
- }
13562
- function heuristicCoerce(raw, repairJson) {
13563
- if (raw === "true") return true;
13564
- if (raw === "false") return false;
13565
- if (raw === "null") return null;
13566
- const n = toFiniteNumber(raw);
13567
- if (n !== void 0) return n;
13568
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13569
- return json === void 0 ? raw : json;
13570
- }
13571
- function coerceCandidates(raw, repairJson) {
13572
- const out = [];
13573
- if (raw === "true") out.push(true);
13574
- else if (raw === "false") out.push(false);
13575
- else if (raw === "null") out.push(null);
13576
- const n = toFiniteNumber(raw);
13577
- if (n !== void 0) out.push(n);
13578
- const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13579
- if (json !== void 0) out.push(json);
13580
- out.push(raw);
13581
- return out;
13582
- }
13583
- function objectShape(schema) {
13584
- const shape = schema?.shape;
13585
- return shape !== null && typeof shape === "object" ? shape : void 0;
13586
- }
14045
+
14046
+ // src/internal/llm/openai.ts
14047
+ init_errors();
13587
14048
 
13588
14049
  // src/sanitize/sanitize-tool-input.ts
13589
14050
  function applyTrim(key2, value, ctx) {
@@ -13680,6 +14141,77 @@ function parseHermesParams(inner) {
13680
14141
  }
13681
14142
  return sanitizeToolInput(input, { trim: true }).value;
13682
14143
  }
14144
+ var STREAM_MARKER = "<function=";
14145
+ var DEFAULT_STREAM_BUFFER_CAP = 8192;
14146
+ var isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
14147
+ function streamToolCallBufferState(held, allowedToolNames, cap2 = DEFAULT_STREAM_BUFFER_CAP) {
14148
+ if (allowedToolNames.size === 0) return "impossible";
14149
+ const t = held.trimStart();
14150
+ if (t.length < STREAM_MARKER.length) {
14151
+ return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
14152
+ }
14153
+ if (!t.startsWith(STREAM_MARKER)) return "impossible";
14154
+ const parsed = parseStreamMarkerName(t);
14155
+ if (parsed === "building") return "possible";
14156
+ if (parsed === "invalid") return "impossible";
14157
+ const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
14158
+ if (!nameOk) return "impossible";
14159
+ return held.length > cap2 ? "impossible" : "possible";
14160
+ }
14161
+ function parseStreamMarkerName(t) {
14162
+ let cursor = STREAM_MARKER.length;
14163
+ while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
14164
+ const nameStart = cursor;
14165
+ while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
14166
+ const name = t.slice(nameStart, cursor);
14167
+ if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
14168
+ return { name, complete: cursor < t.length && t[cursor] === ">" };
14169
+ }
14170
+ function someToolNameStartsWith(allowedToolNames, prefix) {
14171
+ for (const name of allowedToolNames) {
14172
+ if (name.startsWith(prefix)) return true;
14173
+ }
14174
+ return false;
14175
+ }
14176
+ function firstPossibleMarkerStart(held, allowedToolNames) {
14177
+ for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
14178
+ if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
14179
+ }
14180
+ return -1;
14181
+ }
14182
+ var StreamSuppressionBuffer = class {
14183
+ constructor(allowedToolNames) {
14184
+ this.allowedToolNames = allowedToolNames;
14185
+ }
14186
+ allowedToolNames;
14187
+ #held = "";
14188
+ /** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
14189
+ push(content) {
14190
+ this.#held += content;
14191
+ if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
14192
+ return void 0;
14193
+ const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
14194
+ if (holdStart > 0) {
14195
+ const flush2 = this.#held.slice(0, holdStart);
14196
+ this.#held = this.#held.slice(holdStart);
14197
+ return flush2;
14198
+ }
14199
+ const flush = this.#held;
14200
+ this.#held = "";
14201
+ return flush;
14202
+ }
14203
+ /** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
14204
+ * native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
14205
+ * (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
14206
+ drain(hasNativeCalls) {
14207
+ if (this.#held.length === 0) return void 0;
14208
+ const held = this.#held;
14209
+ this.#held = "";
14210
+ if (hasNativeCalls) return held;
14211
+ const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
14212
+ return residual.length > 0 ? residual : void 0;
14213
+ }
14214
+ };
13683
14215
 
13684
14216
  // src/internal/llm/openai.ts
13685
14217
  var OpenAIClient = class {
@@ -13757,8 +14289,12 @@ var OpenAIClient = class {
13757
14289
  // model was actually given. Empty set (no tools) recovers nothing.
13758
14290
  new Set(request.tools?.map((tool) => tool.name) ?? [])
13759
14291
  );
14292
+ let sawDone = false;
13760
14293
  for await (const record of parseSseStream(response.body, signal)) {
13761
- if (record.data === "[DONE]") break;
14294
+ if (record.data === "[DONE]") {
14295
+ sawDone = true;
14296
+ break;
14297
+ }
13762
14298
  let chunk;
13763
14299
  try {
13764
14300
  chunk = JSON.parse(record.data);
@@ -13778,6 +14314,13 @@ var OpenAIClient = class {
13778
14314
  const events = accumulator.consume(chunk);
13779
14315
  for (const event of events) yield event;
13780
14316
  }
14317
+ if (!sawDone && !accumulator.finishReasonSeen) {
14318
+ throw new NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
14319
+ code: "stream_truncated"
14320
+ });
14321
+ }
14322
+ const drainEvent = accumulator.finalizeHeldText();
14323
+ if (drainEvent !== void 0) yield drainEvent;
13781
14324
  return accumulator.finish();
13782
14325
  }
13783
14326
  };
@@ -13793,6 +14336,7 @@ var OpenAIStreamAccumulator = class {
13793
14336
  this.extractFromContent = extractFromContent;
13794
14337
  this.providerName = providerName;
13795
14338
  this.allowedToolNames = allowedToolNames;
14339
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
13796
14340
  }
13797
14341
  extractFromContent;
13798
14342
  providerName;
@@ -13805,18 +14349,30 @@ var OpenAIStreamAccumulator = class {
13805
14349
  cacheWriteTokens;
13806
14350
  reasoningTokens;
13807
14351
  toolCalls = /* @__PURE__ */ new Map();
14352
+ /** R7: present only when recovery is enabled AND the request declares tools — holds suspected
14353
+ * leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
14354
+ suppress;
13808
14355
  consume(chunk) {
13809
14356
  const events = [];
13810
14357
  this.applyUsage(chunk.usage);
13811
14358
  for (const choice of chunk.choices ?? []) {
13812
- const reasoningEvent = this.applyReasoningDelta(
13813
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
13814
- );
13815
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
13816
- const textEvent = this.applyContentDelta(choice.delta?.content);
13817
- if (textEvent !== void 0) events.push(textEvent);
13818
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
13819
- this.applyFinishReason(choice.finish_reason);
14359
+ events.push(...this.applyChoice(choice));
14360
+ }
14361
+ return events;
14362
+ }
14363
+ applyChoice(choice) {
14364
+ const events = [];
14365
+ const reasoningEvent = this.applyReasoningDelta(
14366
+ choice.delta?.reasoning ?? choice.delta?.reasoning_content
14367
+ );
14368
+ if (reasoningEvent !== void 0) events.push(reasoningEvent);
14369
+ const textEvent = this.applyContentDelta(choice.delta?.content);
14370
+ if (textEvent !== void 0) events.push(textEvent);
14371
+ this.mergeToolCallDeltas(choice.delta?.tool_calls);
14372
+ this.applyFinishReason(choice.finish_reason);
14373
+ if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
14374
+ const flushEvent = this.finalizeHeldText();
14375
+ if (flushEvent !== void 0) events.push(flushEvent);
13820
14376
  }
13821
14377
  return events;
13822
14378
  }
@@ -13841,7 +14397,17 @@ var OpenAIStreamAccumulator = class {
13841
14397
  applyContentDelta(content) {
13842
14398
  if (typeof content !== "string" || content.length === 0) return void 0;
13843
14399
  this.text += content;
13844
- return { type: "text_delta", text: content };
14400
+ if (this.suppress === void 0) return { type: "text_delta", text: content };
14401
+ const emit2 = this.suppress.push(content);
14402
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
14403
+ }
14404
+ /** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
14405
+ * SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
14406
+ * held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
14407
+ * buffer streams the held text whole. Idempotent once drained. */
14408
+ finalizeHeldText() {
14409
+ const emit2 = this.suppress?.drain(this.toolCalls.size > 0);
14410
+ return emit2 !== void 0 ? { type: "text_delta", text: emit2 } : void 0;
13845
14411
  }
13846
14412
  mergeToolCallDeltas(deltas) {
13847
14413
  for (const call of deltas ?? []) {
@@ -13852,8 +14418,15 @@ var OpenAIStreamAccumulator = class {
13852
14418
  this.toolCalls.set(call.index, existing);
13853
14419
  }
13854
14420
  }
14421
+ /** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
14422
+ * stream ending without `[DONE]` is a truncation, not a clean end). */
14423
+ sawFinishReason = false;
14424
+ get finishReasonSeen() {
14425
+ return this.sawFinishReason;
14426
+ }
13855
14427
  applyFinishReason(reason) {
13856
14428
  if (reason === void 0 || reason === null) return;
14429
+ this.sawFinishReason = true;
13857
14430
  this.stopReason = mapOpenAIFinish(reason);
13858
14431
  }
13859
14432
  finish() {
@@ -13898,18 +14471,6 @@ var OpenAIStreamAccumulator = class {
13898
14471
  });
13899
14472
  }
13900
14473
  };
13901
- function mapOpenAIFinish(reason) {
13902
- switch (reason) {
13903
- case "tool_calls":
13904
- return "tool_use";
13905
- case "length":
13906
- return "max_tokens";
13907
- case "stop":
13908
- return "end_turn";
13909
- default:
13910
- return "end_turn";
13911
- }
13912
- }
13913
14474
  function applyReasoningRequest(body, effort, providerName) {
13914
14475
  if (providerName === "openai") {
13915
14476
  body.reasoning_effort = effort;
@@ -14007,19 +14568,76 @@ function assistantMessage(message) {
14007
14568
 
14008
14569
  // src/internal/llm/pool-aware-client.ts
14009
14570
  init_errors();
14571
+
14572
+ // src/internal/resilience/circuit-breaker.ts
14573
+ var DEFAULT_MAX_TIMEOUTS = 3;
14574
+ var DEFAULT_COOLDOWN_MS2 = 6e4;
14575
+ var CircuitBreaker = class {
14576
+ constructor(opts = {}) {
14577
+ this.opts = opts;
14578
+ }
14579
+ opts;
14580
+ states = /* @__PURE__ */ new Map();
14581
+ /** @returns true when the breaker is open and the call should be skipped. */
14582
+ shouldSkip(key2) {
14583
+ const state4 = this.states.get(key2);
14584
+ if (state4 === void 0) return false;
14585
+ if (state4.cooldownUntilMs === 0) return false;
14586
+ if (this.now() < state4.cooldownUntilMs) return true;
14587
+ state4.cooldownUntilMs = 0;
14588
+ state4.consecutiveTimeouts = 0;
14589
+ return false;
14590
+ }
14591
+ recordSuccess(key2) {
14592
+ const state4 = this.states.get(key2);
14593
+ if (state4 === void 0) return;
14594
+ state4.consecutiveTimeouts = 0;
14595
+ state4.cooldownUntilMs = 0;
14596
+ }
14597
+ recordTimeout(key2) {
14598
+ const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
14599
+ state4.consecutiveTimeouts += 1;
14600
+ if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
14601
+ state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
14602
+ }
14603
+ this.states.set(key2, state4);
14604
+ }
14605
+ /** @internal — tests inspect counter state. */
14606
+ inspect(key2) {
14607
+ return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
14608
+ }
14609
+ now() {
14610
+ return this.opts.now?.() ?? Date.now();
14611
+ }
14612
+ };
14613
+
14614
+ // src/internal/llm/pool-aware-client.ts
14615
+ init_retry();
14010
14616
  var PoolAwareLlmClient = class {
14011
- constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
14617
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
14012
14618
  this.pool = pool;
14013
14619
  this.buildClient = buildClient2;
14014
14620
  this.waitForAvailableMs = waitForAvailableMs;
14015
14621
  this.name = `pool-aware:${pool.provider}`;
14622
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
14623
+ this.backoffBaseMs = resilience.backoffBaseMs;
14624
+ this.rng = resilience.rng;
14016
14625
  }
14017
14626
  pool;
14018
14627
  buildClient;
14019
14628
  waitForAvailableMs;
14020
14629
  name;
14630
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
14631
+ breaker;
14632
+ backoffBaseMs;
14633
+ rng;
14021
14634
  // 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.
14022
14635
  async *stream(request, signal) {
14636
+ if (this.breaker.shouldSkip(this.pool.provider)) {
14637
+ throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
14638
+ code: "circuit_open"
14639
+ });
14640
+ }
14023
14641
  let hasRetried429 = false;
14024
14642
  while (true) {
14025
14643
  if (signal.aborted) throw abortError2(signal);
@@ -14034,6 +14652,7 @@ var PoolAwareLlmClient = class {
14034
14652
  }
14035
14653
  }
14036
14654
  if (entry === null) {
14655
+ this.breaker.recordTimeout(this.pool.provider);
14037
14656
  throw new CredentialPoolExhaustedError(
14038
14657
  `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
14039
14658
  { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
@@ -14043,10 +14662,19 @@ var PoolAwareLlmClient = class {
14043
14662
  const realClient = this.buildClient(entry.accessToken);
14044
14663
  const attempt = await tryFirstEvent(realClient, request, signal);
14045
14664
  if (attempt.kind === "ok") {
14665
+ this.breaker.recordSuccess(this.pool.provider);
14046
14666
  return yield* relayStream(attempt.generator, attempt.firstResult);
14047
14667
  }
14048
14668
  const decision = classifyAndDecide(attempt.error, hasRetried429);
14049
14669
  if (decision === "retry") {
14670
+ await sleepWithAbort(
14671
+ computeBackoffMs({
14672
+ attempt: 0,
14673
+ ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
14674
+ ...this.rng !== void 0 ? { rng: this.rng } : {}
14675
+ }),
14676
+ signal
14677
+ );
14050
14678
  hasRetried429 = true;
14051
14679
  continue;
14052
14680
  }
@@ -14066,6 +14694,7 @@ var PoolAwareLlmClient = class {
14066
14694
  hasRetried429 = false;
14067
14695
  continue;
14068
14696
  }
14697
+ this.breaker.recordTimeout(this.pool.provider);
14069
14698
  throw attempt.error;
14070
14699
  }
14071
14700
  }
@@ -14501,9 +15130,28 @@ function selectTransport(profile, apiKey) {
14501
15130
  // src/internal/mcp/client.ts
14502
15131
  init_errors();
14503
15132
  init_path_guard();
14504
- function createMcpClient(name, config) {
15133
+ function createMcpClient(name, config, fetchImpl = fetch) {
14505
15134
  if (isStdio(config)) return new StdioMcpClient(name, config);
14506
- return new HttpMcpClient(name, config);
15135
+ return new HttpMcpClient(name, config, fetchImpl);
15136
+ }
15137
+ var DEFAULT_MCP_TIMEOUT_MS = 3e4;
15138
+ var MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
15139
+ var RECONNECT_BASE_MS = 250;
15140
+ var MAX_RECONNECT_ATTEMPTS = 2;
15141
+ function reconnectDelay(attempt) {
15142
+ const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
15143
+ const ms = Math.floor(Math.random() * ceiling);
15144
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
15145
+ }
15146
+ function mcpTimeoutError(name, timeoutMs) {
15147
+ return new NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
15148
+ code: "mcp_timeout"
15149
+ });
15150
+ }
15151
+ function isAbortLike(cause) {
15152
+ if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
15153
+ const name = cause.name;
15154
+ return name === "TimeoutError" || name === "AbortError";
14507
15155
  }
14508
15156
  async function rpcInitialize(request) {
14509
15157
  await request("initialize", {
@@ -14549,32 +15197,107 @@ var StdioMcpClient = class extends BaseMcpClient {
14549
15197
  name;
14550
15198
  child;
14551
15199
  nextId = 1;
15200
+ // #59 — pending requests carry a reject + timer so a silent server times out
15201
+ // (typed error), a late reply after timeout is a no-op, and close() settles them.
14552
15202
  pending = /* @__PURE__ */ new Map();
14553
15203
  buffer = "";
14554
- async initialize() {
15204
+ // M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
15205
+ // unexpectedly OR times out (not via close()); the next request re-spawns with
15206
+ // backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
15207
+ // concurrent request so parallel tool dispatch after a drop awaits one handshake
15208
+ // instead of racing (or spuriously failing with mcp_not_init).
15209
+ dropped = false;
15210
+ reconnectAttempts = 0;
15211
+ reconnectPromise;
15212
+ get timeoutMs() {
15213
+ return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
15214
+ }
15215
+ /** Spawn the server child and wire stdout/stderr/error/exit handlers.
15216
+ * Shared by `initialize()` and the M2 #59 reconnect path. */
15217
+ spawnChild() {
14555
15218
  const resolvedCwd = resolveMcpCwd(this.config.cwd);
14556
15219
  const child = spawn(this.config.command, this.config.args ?? [], {
14557
15220
  cwd: resolvedCwd,
14558
- env: { ...process.env, ...this.config.env ?? {} }
15221
+ // #54 (F-H1) a third-party MCP server binary must not inherit host
15222
+ // secrets. Scrub secret-like vars by default; `config.env` still wins.
15223
+ env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
14559
15224
  });
14560
15225
  this.child = child;
14561
15226
  child.stdout.on("data", (chunk) => this.consume(chunk));
14562
15227
  child.stderr.on("data", () => void 0);
15228
+ child.stdin.on("error", () => void 0);
14563
15229
  child.on("error", () => {
14564
- for (const resolver of this.pending.values()) {
14565
- resolver({ error: { message: "MCP process crashed" } });
14566
- }
14567
- this.pending.clear();
15230
+ this.rejectAllPending(
15231
+ new NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
15232
+ );
15233
+ });
15234
+ child.on("exit", () => {
15235
+ if (this.child !== child) return;
15236
+ this.child = void 0;
15237
+ this.dropped = true;
15238
+ this.rejectAllPending(
15239
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
15240
+ );
14568
15241
  });
15242
+ }
15243
+ async initialize() {
15244
+ this.spawnChild();
14569
15245
  await super.initialize();
14570
15246
  }
15247
+ /** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
15248
+ * full-jitter backoff) when the client was dropped; fail fast when never
15249
+ * initialized. Concurrent callers share ONE reconnect handshake. */
15250
+ ensureConnected() {
15251
+ if (this.child !== void 0) return Promise.resolve();
15252
+ if (!this.dropped) {
15253
+ return Promise.reject(
15254
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
15255
+ );
15256
+ }
15257
+ this.reconnectPromise ??= this.reconnect().finally(() => {
15258
+ this.reconnectPromise = void 0;
15259
+ });
15260
+ return this.reconnectPromise;
15261
+ }
15262
+ async reconnect() {
15263
+ if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
15264
+ throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
15265
+ }
15266
+ await reconnectDelay(this.reconnectAttempts);
15267
+ this.reconnectAttempts += 1;
15268
+ this.spawnChild();
15269
+ await super.initialize();
15270
+ this.dropped = false;
15271
+ this.reconnectAttempts = 0;
15272
+ }
14571
15273
  async close() {
14572
- if (this.child === void 0) return;
14573
- this.child.kill("SIGTERM");
15274
+ this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
15275
+ const child = this.child;
14574
15276
  this.child = void 0;
15277
+ this.dropped = false;
15278
+ child?.kill("SIGTERM");
15279
+ }
15280
+ /** Reject + clear every pending request (crash / close). @internal */
15281
+ rejectAllPending(error) {
15282
+ for (const entry of this.pending.values()) {
15283
+ clearTimeout(entry.timer);
15284
+ entry.reject(error);
15285
+ }
15286
+ this.pending.clear();
14575
15287
  }
14576
15288
  consume(chunk) {
14577
15289
  this.buffer += chunk.toString("utf8");
15290
+ if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
15291
+ this.buffer = "";
15292
+ this.rejectAllPending(
15293
+ new NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
15294
+ code: "mcp_buffer_overflow"
15295
+ })
15296
+ );
15297
+ this.child?.kill("SIGKILL");
15298
+ this.child = void 0;
15299
+ return;
15300
+ }
14578
15301
  let newlineIndex = this.buffer.indexOf("\n");
14579
15302
  while (newlineIndex !== -1) {
14580
15303
  const line = this.buffer.slice(0, newlineIndex).trim();
@@ -14591,23 +15314,47 @@ var StdioMcpClient = class extends BaseMcpClient {
14591
15314
  return;
14592
15315
  }
14593
15316
  if (typeof message.id !== "number") return;
14594
- const resolver = this.pending.get(message.id);
14595
- if (resolver === void 0) return;
15317
+ const entry = this.pending.get(message.id);
15318
+ if (entry === void 0) return;
14596
15319
  this.pending.delete(message.id);
14597
- resolver(message);
15320
+ clearTimeout(entry.timer);
15321
+ entry.resolve(message);
14598
15322
  }
14599
15323
  request(method, params) {
14600
- if (this.child === void 0) {
14601
- return Promise.reject(
14602
- new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
14603
- );
15324
+ const child = this.child;
15325
+ if (child !== void 0) return this.send(child, method, params);
15326
+ if (this.dropped) return this.reconnectAndRequest(method, params);
15327
+ return Promise.reject(
15328
+ new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
15329
+ );
15330
+ }
15331
+ /** M2 #59 — reconnect a dropped client, then send. Separate async path so the
15332
+ * happy path above never pays an extra microtask tick. */
15333
+ async reconnectAndRequest(method, params) {
15334
+ await this.ensureConnected();
15335
+ const child = this.child;
15336
+ if (child === void 0) {
15337
+ throw new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
14604
15338
  }
15339
+ return this.send(child, method, params);
15340
+ }
15341
+ send(child, method, params) {
14605
15342
  const id = this.nextId++;
14606
15343
  const payload = { jsonrpc: "2.0", id, method, params };
14607
- this.child.stdin.write(`${JSON.stringify(payload)}
15344
+ child.stdin.write(`${JSON.stringify(payload)}
14608
15345
  `);
14609
- return new Promise((resolve3) => {
14610
- this.pending.set(id, resolve3);
15346
+ return new Promise((resolve3, reject) => {
15347
+ const timer = setTimeout(() => {
15348
+ this.pending.delete(id);
15349
+ reject(mcpTimeoutError(this.name, this.timeoutMs));
15350
+ this.child?.kill("SIGKILL");
15351
+ this.child = void 0;
15352
+ this.dropped = true;
15353
+ this.rejectAllPending(
15354
+ new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
15355
+ );
15356
+ }, this.timeoutMs);
15357
+ this.pending.set(id, { resolve: resolve3, reject, timer });
14611
15358
  });
14612
15359
  }
14613
15360
  };
@@ -14633,11 +15380,20 @@ var HttpMcpClient = class extends BaseMcpClient {
14633
15380
  accept: "application/json",
14634
15381
  ...this.config.headers ?? {}
14635
15382
  };
14636
- const response = await this.fetchImpl(this.config.url, {
14637
- method: "POST",
14638
- headers,
14639
- body: JSON.stringify(payload)
14640
- });
15383
+ const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
15384
+ let response;
15385
+ try {
15386
+ response = await this.fetchImpl(this.config.url, {
15387
+ method: "POST",
15388
+ headers,
15389
+ body: JSON.stringify(payload),
15390
+ // #59 — bound the request; a non-responding endpoint aborts here.
15391
+ signal: AbortSignal.timeout(timeoutMs)
15392
+ });
15393
+ } catch (cause) {
15394
+ if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
15395
+ throw cause;
15396
+ }
14641
15397
  if (!response.ok) {
14642
15398
  throw new NetworkError(`MCP ${this.name} returned ${response.status}`, {
14643
15399
  code: "mcp_http_error"
@@ -14752,11 +15508,33 @@ function resolveRunProvider(options) {
14752
15508
  );
14753
15509
  }
14754
15510
  const parsedModel = parseModelId(options.model?.id);
14755
- const inferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
14756
- const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? inferredProvider ?? detectPrimaryProvider();
14757
- const effectiveModelId = inferredProvider !== void 0 ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
15511
+ const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
15512
+ const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
15513
+ const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
15514
+ const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
14758
15515
  return { primary, effectiveModelId };
14759
15516
  }
15517
+ function inferProviderFromApiKey(apiKey) {
15518
+ if (apiKey === void 0 || apiKey.length === 0) return void 0;
15519
+ const byPrefix = [
15520
+ { provider: "openrouter", prefix: "sk-or-" },
15521
+ { provider: "anthropic", prefix: "sk-ant-" },
15522
+ { provider: "openai", prefix: "sk-" }
15523
+ ];
15524
+ for (const { provider, prefix } of byPrefix) {
15525
+ if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
15526
+ return provider;
15527
+ }
15528
+ }
15529
+ return void 0;
15530
+ }
15531
+ function mergeExplicitApiKey(pools, primary, apiKey) {
15532
+ if (apiKey === void 0 || apiKey.length === 0) return pools;
15533
+ if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
15534
+ const existing = pools?.[primary];
15535
+ if (existing !== void 0 && existing.length > 0) return pools;
15536
+ return { ...pools ?? {}, [primary]: [apiKey] };
15537
+ }
14760
15538
  function buildLoopInputs(options, runId, userText) {
14761
15539
  const maxIterations = options.sendOptions.maxIterations;
14762
15540
  if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
@@ -14767,7 +15545,11 @@ function buildLoopInputs(options, runId, userText) {
14767
15545
  }
14768
15546
  const { primary, effectiveModelId } = resolveRunProvider(options);
14769
15547
  const fallback = options.agentOptions.providers?.fallback;
14770
- const apiKeys = options.agentOptions.providers?.apiKeys;
15548
+ const apiKeys = mergeExplicitApiKey(
15549
+ options.agentOptions.providers?.apiKeys,
15550
+ primary,
15551
+ options.agentOptions.apiKey
15552
+ );
14771
15553
  const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
14772
15554
  const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
14773
15555
  const chain = resolveProviderChain({
@@ -14811,6 +15593,10 @@ function buildLoopInputs(options, runId, userText) {
14811
15593
  // D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
14812
15594
  // can attach it to the LLM `fetch({ signal })` call.
14813
15595
  ...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
15596
+ // #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
15597
+ // can enable them via SendOptions (not only internal AgentLoopInputs).
15598
+ ...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
15599
+ ...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
14814
15600
  // M1-2: per-send iteration ceiling (validated above). The loop reads
14815
15601
  // inputs.maxIterations (default 8 when unset).
14816
15602
  ...maxIterations !== void 0 ? { maxIterations } : {},
@@ -15128,7 +15914,12 @@ async function runActiveMemory(args) {
15128
15914
  hits: []
15129
15915
  });
15130
15916
  }
15131
- const cached2 = args.cache?.get(args.userText, cfg.queryMode);
15917
+ const tenantCtx = {
15918
+ namespace: args.namespace,
15919
+ userId: args.userId,
15920
+ scope: args.scope
15921
+ };
15922
+ const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
15132
15923
  if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
15133
15924
  const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
15134
15925
  if (query.trim().length === 0) {
@@ -15213,7 +16004,12 @@ function notifyBreaker(breaker, key2, status) {
15213
16004
  else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key2);
15214
16005
  }
15215
16006
  async function finalize(args, queryMode, result) {
15216
- args.cache?.set(args.userText, queryMode, result);
16007
+ const tenantCtx = {
16008
+ namespace: args.namespace,
16009
+ userId: args.userId,
16010
+ scope: args.scope
16011
+ };
16012
+ args.cache?.set(args.userText, queryMode, result, tenantCtx);
15217
16013
  if (args.persistTranscripts === true && args.cwd !== void 0) {
15218
16014
  await persistActiveMemoryTranscript(args.cwd, {
15219
16015
  runId: args.runId ?? `run-${Date.now()}`,
@@ -15786,48 +16582,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
15786
16582
  gemini: geminiMemoryEmbeddingProviderAdapter
15787
16583
  };
15788
16584
 
15789
- // src/internal/memory/circuit-breaker.ts
15790
- var DEFAULT_MAX_TIMEOUTS = 3;
15791
- var DEFAULT_COOLDOWN_MS2 = 6e4;
15792
- var CircuitBreaker = class {
15793
- constructor(opts = {}) {
15794
- this.opts = opts;
15795
- }
15796
- opts;
15797
- states = /* @__PURE__ */ new Map();
15798
- /** @returns true when the breaker is open and the call should be skipped. */
15799
- shouldSkip(key2) {
15800
- const state4 = this.states.get(key2);
15801
- if (state4 === void 0) return false;
15802
- if (state4.cooldownUntilMs === 0) return false;
15803
- if (this.now() < state4.cooldownUntilMs) return true;
15804
- state4.cooldownUntilMs = 0;
15805
- state4.consecutiveTimeouts = 0;
15806
- return false;
15807
- }
15808
- recordSuccess(key2) {
15809
- const state4 = this.states.get(key2);
15810
- if (state4 === void 0) return;
15811
- state4.consecutiveTimeouts = 0;
15812
- state4.cooldownUntilMs = 0;
15813
- }
15814
- recordTimeout(key2) {
15815
- const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15816
- state4.consecutiveTimeouts += 1;
15817
- if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
15818
- state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
15819
- }
15820
- this.states.set(key2, state4);
15821
- }
15822
- /** @internal — tests inspect counter state. */
15823
- inspect(key2) {
15824
- return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
15825
- }
15826
- now() {
15827
- return this.opts.now?.() ?? Date.now();
15828
- }
15829
- };
15830
-
15831
16585
  // src/internal/runtime/local-agent/local-agent-memory.ts
15832
16586
  init_index_manager();
15833
16587
 
@@ -18416,13 +19170,13 @@ function defineTool(spec) {
18416
19170
  name: spec.name,
18417
19171
  description: spec.description,
18418
19172
  inputSchema,
18419
- handler: async (input) => {
19173
+ handler: async (input, ctx) => {
18420
19174
  const raw = spec.sanitize ? sanitizeToolInput(input, {
18421
19175
  ...spec.sanitize === true ? {} : spec.sanitize,
18422
19176
  schema: spec.inputSchema
18423
19177
  }).value : input;
18424
19178
  const parsed = spec.inputSchema.parse(raw);
18425
- return await spec.handler(parsed);
19179
+ return await spec.handler(parsed, ctx);
18426
19180
  }
18427
19181
  };
18428
19182
  }
@@ -18433,6 +19187,14 @@ init_errors();
18433
19187
  // src/event-bus.ts
18434
19188
  var EventBus = class {
18435
19189
  handlers = /* @__PURE__ */ new Map();
19190
+ // M3 #64 — a swallowed handler error used to vanish without a trace (fail-loud
19191
+ // violation). We now log it AND expose an observable count so ops/tests can see
19192
+ // that a subscriber is silently failing, without breaking the EC-2 contract.
19193
+ #handlerErrorCount = 0;
19194
+ /** M3 #64 — number of handler invocations that threw (and were logged). */
19195
+ get handlerErrorCount() {
19196
+ return this.#handlerErrorCount;
19197
+ }
18436
19198
  /**
18437
19199
  * Subscribe to an event. Returns an unsubscribe function.
18438
19200
  */
@@ -18455,7 +19217,13 @@ var EventBus = class {
18455
19217
  for (const handler of set) {
18456
19218
  try {
18457
19219
  handler(payload);
18458
- } catch {
19220
+ } catch (cause) {
19221
+ this.#handlerErrorCount += 1;
19222
+ const message = cause instanceof Error ? cause.message : String(cause);
19223
+ process.stderr.write(
19224
+ `[theokit-sdk] event-bus: handler for "${String(event)}" threw: ${message}
19225
+ `
19226
+ );
18459
19227
  }
18460
19228
  }
18461
19229
  }
@@ -18478,11 +19246,17 @@ init_generate_object();
18478
19246
  // src/internal/persistence/conversation-storage-memory.ts
18479
19247
  var InMemoryConversationStorage = class {
18480
19248
  #store = /* @__PURE__ */ new Map();
18481
- async getMessages(conversationId) {
19249
+ async getMessages(conversationId, opts) {
18482
19250
  const existing = this.#store.get(conversationId);
18483
- return existing === void 0 ? [] : existing.slice();
19251
+ return existing === void 0 ? [] : paginate(existing.slice(), opts);
18484
19252
  }
18485
19253
  async appendMessage(conversationId, message) {
19254
+ this.#appendOne(conversationId, message);
19255
+ }
19256
+ async appendMessages(conversationId, messages) {
19257
+ for (const message of messages) this.#appendOne(conversationId, message);
19258
+ }
19259
+ #appendOne(conversationId, message) {
18486
19260
  const existing = this.#store.get(conversationId);
18487
19261
  const stamped = message.at === void 0 ? { ...message, at: Date.now() } : message;
18488
19262
  if (existing === void 0) {
@@ -18491,9 +19265,26 @@ var InMemoryConversationStorage = class {
18491
19265
  }
18492
19266
  existing.push(stamped);
18493
19267
  }
19268
+ async truncateConversation(conversationId, keepCount) {
19269
+ const existing = this.#store.get(conversationId);
19270
+ if (existing === void 0) return 0;
19271
+ const keep = Math.max(0, Math.min(keepCount, existing.length));
19272
+ this.#store.set(conversationId, existing.slice(0, keep));
19273
+ return keep;
19274
+ }
18494
19275
  async deleteConversation(conversationId) {
18495
19276
  this.#store.delete(conversationId);
18496
19277
  }
19278
+ async deleteScope(prefix) {
19279
+ let n = 0;
19280
+ for (const id of [...this.#store.keys()]) {
19281
+ if (id.startsWith(prefix)) {
19282
+ this.#store.delete(id);
19283
+ n += 1;
19284
+ }
19285
+ }
19286
+ return n;
19287
+ }
18497
19288
  async listConversationIds(opts) {
18498
19289
  const all = Array.from(this.#store.keys());
18499
19290
  if (opts?.limit !== void 0) return all.slice(0, opts.limit);
@@ -18671,23 +19462,40 @@ function createNoopMemoryProvider() {
18671
19462
  }
18672
19463
  var JobQueue = class {
18673
19464
  jobs = /* @__PURE__ */ new Map();
19465
+ controllers = /* @__PURE__ */ new Map();
19466
+ maxConcurrency;
19467
+ running = 0;
19468
+ waiting = [];
19469
+ constructor(options = {}) {
19470
+ this.maxConcurrency = options.maxConcurrency === void 0 ? Number.POSITIVE_INFINITY : Math.max(1, options.maxConcurrency);
19471
+ }
18674
19472
  /**
18675
- * Enqueue a background function. Returns the job ID immediately.
18676
- * EC-1: wraps fn in Promise.resolve().then() so sync throws are caught.
19473
+ * Enqueue a background function. Returns the job ID immediately. The function
19474
+ * receives an `AbortSignal` that fires when the job is cancelled (#58) — a
19475
+ * cooperative job should observe it to stop early. Existing `() => Promise<T>`
19476
+ * callers are unaffected (the signal argument is simply ignored).
18677
19477
  */
18678
19478
  enqueue(fn) {
18679
19479
  const id = randomUUID();
18680
19480
  const job = { id, status: "pending" };
18681
19481
  this.jobs.set(id, job);
18682
- job.status = "running";
18683
- Promise.resolve().then(() => fn()).then((result) => {
18684
- if (job.status === "cancelled") return;
18685
- job.result = result;
18686
- job.status = "completed";
18687
- }).catch((err) => {
18688
- if (job.status === "cancelled") return;
18689
- job.error = err instanceof Error ? err.message : String(err);
18690
- job.status = "failed";
19482
+ const controller = new AbortController();
19483
+ this.controllers.set(id, controller);
19484
+ void this.#acquire().then(() => {
19485
+ if (job.status === "cancelled") {
19486
+ this.#release(id);
19487
+ return;
19488
+ }
19489
+ job.status = "running";
19490
+ Promise.resolve().then(() => fn(controller.signal)).then((result) => {
19491
+ if (job.status === "cancelled") return;
19492
+ job.result = result;
19493
+ job.status = "completed";
19494
+ }).catch((err) => {
19495
+ if (job.status === "cancelled") return;
19496
+ job.error = err instanceof Error ? err.message : String(err);
19497
+ job.status = "failed";
19498
+ }).finally(() => this.#release(id));
18691
19499
  });
18692
19500
  return id;
18693
19501
  }
@@ -18698,17 +19506,39 @@ var JobQueue = class {
18698
19506
  return [...this.jobs.values()];
18699
19507
  }
18700
19508
  /**
18701
- * Cancel a pending or running job. Returns true if cancelled.
19509
+ * Cancel a pending or running job. Returns true if cancelled. #58 — aborts the
19510
+ * job's `AbortSignal` so a cooperative running job is actually interrupted.
18702
19511
  */
18703
19512
  cancel(id) {
18704
19513
  const job = this.jobs.get(id);
18705
19514
  if (!job) return false;
18706
19515
  if (job.status === "pending" || job.status === "running") {
18707
19516
  job.status = "cancelled";
19517
+ this.controllers.get(id)?.abort();
18708
19518
  return true;
18709
19519
  }
18710
19520
  return false;
18711
19521
  }
19522
+ /** Acquire a concurrency slot (resolves immediately when unbounded/free). */
19523
+ #acquire() {
19524
+ if (this.running < this.maxConcurrency) {
19525
+ this.running += 1;
19526
+ return Promise.resolve();
19527
+ }
19528
+ return new Promise((resolve3) => {
19529
+ this.waiting.push(() => {
19530
+ this.running += 1;
19531
+ resolve3();
19532
+ });
19533
+ });
19534
+ }
19535
+ /** Release a slot + clean up the controller; start the next waiting job. */
19536
+ #release(id) {
19537
+ this.controllers.delete(id);
19538
+ this.running -= 1;
19539
+ const next = this.waiting.shift();
19540
+ if (next !== void 0) next();
19541
+ }
18712
19542
  };
18713
19543
 
18714
19544
  // src/internal/memory/dreaming/run.ts
@@ -19201,24 +20031,42 @@ async function migrateSqliteToLance2(options) {
19201
20031
  }
19202
20032
 
19203
20033
  // src/permission-engine.ts
20034
+ function argMatches(matcher, value) {
20035
+ if (typeof matcher === "function") return matcher(value);
20036
+ if (value === void 0) return false;
20037
+ if (matcher instanceof RegExp) return matcher.test(String(value));
20038
+ return matcher === value;
20039
+ }
19204
20040
  var PermissionEngine = class {
19205
20041
  constructor(rules, options = {}) {
19206
20042
  this.rules = rules;
19207
- this.defaultAction = options.defaultAction ?? "allow";
20043
+ this.defaultAction = options.defaultAction ?? "ask";
19208
20044
  }
19209
20045
  rules;
19210
20046
  defaultAction;
19211
20047
  /**
19212
- * Evaluate a tool name against the rules. First match wins; falls back to the
19213
- * configured `defaultAction` (default `"allow"`) when no rule matches.
20048
+ * Evaluate a tool name (and optional arguments) against the rules. First
20049
+ * match wins; falls back to the configured `defaultAction` (default `"ask"`,
20050
+ * fail-closed) when no rule matches. #55 — a rule with `args` gates on the
20051
+ * argument values, so the same tool name can resolve to different actions
20052
+ * depending on what it is asked to do.
19214
20053
  */
19215
- evaluate(toolName) {
20054
+ evaluate(toolName, args) {
19216
20055
  for (const rule of this.rules) {
19217
- const matches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
19218
- if (matches) return rule.action;
20056
+ const nameMatches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
20057
+ if (!nameMatches) continue;
20058
+ if (rule.args !== void 0 && !this.#argsMatch(rule.args, args)) continue;
20059
+ return rule.action;
19219
20060
  }
19220
20061
  return this.defaultAction;
19221
20062
  }
20063
+ #argsMatch(matchers, args) {
20064
+ const call = args ?? {};
20065
+ for (const [key2, matcher] of Object.entries(matchers)) {
20066
+ if (!argMatches(matcher, call[key2])) return false;
20067
+ }
20068
+ return true;
20069
+ }
19222
20070
  };
19223
20071
 
19224
20072
  // src/permission-plugin.ts
@@ -19229,8 +20077,8 @@ function createPermissionPlugin(engine, opts = {}) {
19229
20077
  kind: "general",
19230
20078
  register(ctx) {
19231
20079
  ctx.on("pre_tool_call", (rawCtx) => {
19232
- const { name } = rawCtx;
19233
- const action = engine.evaluate(name);
20080
+ const { name, args } = rawCtx;
20081
+ const action = engine.evaluate(name, args);
19234
20082
  if (action === "deny") {
19235
20083
  return { block: true, message: `denied by permission engine: ${name}` };
19236
20084
  }
@@ -19295,6 +20143,14 @@ var Security = class {
19295
20143
  }
19296
20144
  };
19297
20145
 
20146
+ // src/session-scope.ts
20147
+ function scopedConversationId(scope, id) {
20148
+ return `${scope}__${id}`;
20149
+ }
20150
+ function sessionScopePrefix(scope) {
20151
+ return `${scope}__`;
20152
+ }
20153
+
19298
20154
  // src/squad.ts
19299
20155
  init_errors();
19300
20156
  var PersistenceSchema = z.object({
@@ -20012,6 +20868,6 @@ function safeStringify2(v) {
20012
20868
  }
20013
20869
  }
20014
20870
 
20015
- export { Agent, AgentBuilder, AgentDisposedError, AgentRunError, AuthenticationError, Budget, BudgetExceededError, ConfigurationError, Cron, EventBus, FileSystemConversationStorage, GenerateObjectError, InMemoryConversationStorage, IntegrationNotConnectedError, InvalidTaskIdError, JobQueue, Memory, MemoryAdapterError, NetworkError, PermissionEngine, RateLimitError, Security, StreamObjectError, Task, TaskNotFoundError, Theokit, TheokitAgentError, UnknownAgentError, UnsupportedBudgetOperationError, UnsupportedRunOperationError, UnsupportedTaskOperationError, UsageAccumulator, buildReplayHistory, chargeAndCheckThresholds, computeCost, createAgentFactory, createCounterBudgetTracker, createNoopMemoryProvider, createPermissionPlugin, createSquad, definePlugin, defineProvider, defineTool, extractRawId, getPricingEntry, inferApiMode, isTransientError, migrateSqliteToLance2 as migrateSqliteToLance, mkMemoryId, normalizeUsage, preflightCheck, toShareGptTrajectory, withCwdMutex };
20871
+ export { Agent, AgentBuilder, AgentDisposedError, AgentRunError, AuthenticationError, Budget, BudgetExceededError, ConfigurationError, Cron, EventBus, FileSystemConversationStorage, GenerateObjectError, InMemoryConversationStorage, IntegrationNotConnectedError, InvalidTaskIdError, JobQueue, Memory, MemoryAdapterError, NetworkError, PermissionEngine, RateLimitError, Security, StreamObjectError, Task, TaskNotFoundError, Theokit, TheokitAgentError, UnknownAgentError, UnsupportedBudgetOperationError, UnsupportedRunOperationError, UnsupportedTaskOperationError, UsageAccumulator, buildReplayHistory, chargeAndCheckThresholds, computeCost, createAgentFactory, createCounterBudgetTracker, createNoopMemoryProvider, createPermissionPlugin, createSquad, definePlugin, defineProvider, defineTool, extractRawId, getPricingEntry, inferApiMode, isTransientError, migrateSqliteToLance2 as migrateSqliteToLance, mkMemoryId, normalizeUsage, preflightCheck, scopedConversationId, sessionScopePrefix, toShareGptTrajectory, withCwdMutex };
20016
20872
  //# sourceMappingURL=index.js.map
20017
20873
  //# sourceMappingURL=index.js.map