@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.
- package/CHANGELOG.md +71 -0
- package/dist/a2a/index.cjs +981 -208
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +982 -209
- package/dist/a2a/index.js.map +1 -1
- package/dist/{cron-BxLSz1UH.d.cts → cron-Bbg0mBOv.d.ts} +33 -3
- package/dist/{cron-DcaoP7aW.d.ts → cron-ZLSKbDbB.d.cts} +33 -3
- package/dist/cron.cjs +945 -196
- package/dist/cron.cjs.map +1 -1
- package/dist/cron.d.cts +2 -2
- package/dist/cron.d.ts +2 -2
- package/dist/cron.js +945 -196
- package/dist/cron.js.map +1 -1
- package/dist/define-tool.d.ts +9 -2
- package/dist/{errors-Bart0ptP.d.cts → errors-1tVcX3Fq.d.cts} +1 -1
- package/dist/{errors-DJuuubJK.d.ts → errors-qyVYfk9H.d.ts} +1 -1
- package/dist/errors.d.cts +2 -2
- package/dist/eval.cjs +951 -198
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +951 -198
- package/dist/eval.js.map +1 -1
- package/dist/event-bus.d.ts +3 -0
- package/dist/index.cjs +1082 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -27
- package/dist/index.d.ts +122 -27
- package/dist/index.js +1082 -226
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-loop/tool-dispatch.d.ts +3 -1
- package/dist/internal/agent-loop/tool-result-guard.d.ts +24 -0
- package/dist/internal/agent-loop/tool-timeout.d.ts +23 -0
- package/dist/internal/llm/openai.d.ts +13 -0
- package/dist/internal/llm/sse.d.ts +13 -1
- package/dist/internal/mcp/client.d.ts +1 -1
- package/dist/internal/memory/active-memory.d.ts +1 -1
- package/dist/internal/persistence/conversation-storage-fs.d.cts +7 -1
- package/dist/internal/persistence/conversation-storage-fs.d.ts +7 -1
- package/dist/internal/persistence/conversation-storage-memory.d.cts +7 -1
- package/dist/internal/persistence/conversation-storage-memory.d.ts +7 -1
- package/dist/internal/persistence/pagination.d.cts +8 -0
- package/dist/internal/persistence/pagination.d.ts +8 -0
- package/dist/internal/plugins/index.cjs +135 -0
- package/dist/internal/plugins/index.cjs.map +1 -1
- package/dist/internal/plugins/index.js +135 -0
- package/dist/internal/plugins/index.js.map +1 -1
- package/dist/internal/plugins/manager.d.cts +21 -1
- package/dist/internal/plugins/manager.d.ts +21 -1
- package/dist/internal/plugins/types.d.cts +40 -0
- package/dist/internal/plugins/types.d.ts +40 -0
- package/dist/internal/{memory → resilience}/circuit-breaker.d.ts +5 -1
- package/dist/internal/runtime/hooks/hooks-frontmatter.d.ts +1 -1
- package/dist/internal/runtime/lifecycle/env-policy.d.ts +30 -0
- package/dist/internal/runtime/session/agent-session-store.d.ts +1 -0
- package/dist/internal/telemetry/span-names.d.ts +7 -1
- package/dist/job-queue.d.ts +29 -7
- package/dist/permission-engine.d.ts +32 -7
- package/dist/{run-DXy_MVwz.d.cts → run-pE-34AAo.d.cts} +64 -3
- package/dist/{run-DXy_MVwz.d.ts → run-pE-34AAo.d.ts} +64 -3
- package/dist/sandbox/index.cjs +53 -2
- package/dist/sandbox/index.cjs.map +1 -1
- package/dist/sandbox/index.js +53 -2
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/local-sandbox.d.cts +11 -3
- package/dist/sandbox/local-sandbox.d.ts +11 -3
- package/dist/sandbox/types.d.cts +7 -0
- package/dist/sandbox/types.d.ts +7 -0
- package/dist/types/agent-prims.d.ts +6 -2
- package/dist/types/conversation-storage.d.ts +32 -2
- package/dist/types/mcp.d.ts +20 -0
- package/dist/types/run.d.ts +17 -0
- package/dist/workflow.cjs +6 -3
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.js +6 -3
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
package/dist/eval.js
CHANGED
|
@@ -885,6 +885,16 @@ var init_credential_pool_types = __esm({
|
|
|
885
885
|
});
|
|
886
886
|
|
|
887
887
|
// src/internal/llm/retry.ts
|
|
888
|
+
function computeBackoffMs(opts) {
|
|
889
|
+
const base = opts.baseMs ?? DEFAULT_BASE_MS;
|
|
890
|
+
const cap = opts.capMs ?? DEFAULT_CAP_MS;
|
|
891
|
+
const rng = opts.rng ?? Math.random;
|
|
892
|
+
if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
|
|
893
|
+
return Math.max(base, Math.min(cap, opts.retryAfterMs));
|
|
894
|
+
}
|
|
895
|
+
const ceiling = Math.min(cap, base * 2 ** opts.attempt);
|
|
896
|
+
return Math.floor(rng() * ceiling);
|
|
897
|
+
}
|
|
888
898
|
function sleepWithAbort(ms, signal) {
|
|
889
899
|
if (ms <= 0 || signal.aborted) return Promise.resolve();
|
|
890
900
|
return new Promise((resolve3) => {
|
|
@@ -899,8 +909,11 @@ function sleepWithAbort(ms, signal) {
|
|
|
899
909
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
900
910
|
});
|
|
901
911
|
}
|
|
912
|
+
var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
|
|
902
913
|
var init_retry = __esm({
|
|
903
914
|
"src/internal/llm/retry.ts"() {
|
|
915
|
+
DEFAULT_BASE_MS = 500;
|
|
916
|
+
DEFAULT_CAP_MS = 32e3;
|
|
904
917
|
}
|
|
905
918
|
});
|
|
906
919
|
|
|
@@ -4561,13 +4574,15 @@ function buildGitInfo() {
|
|
|
4561
4574
|
init_errors();
|
|
4562
4575
|
|
|
4563
4576
|
// src/internal/llm/sse.ts
|
|
4564
|
-
|
|
4577
|
+
init_errors();
|
|
4578
|
+
var DEFAULT_SSE_IDLE_MS = 6e4;
|
|
4579
|
+
async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
|
|
4565
4580
|
if (body === null) return;
|
|
4566
4581
|
const reader = body.getReader();
|
|
4567
4582
|
const decoder = new TextDecoder("utf-8");
|
|
4568
4583
|
const state2 = { buffer: "", event: "message", data: "" };
|
|
4569
4584
|
try {
|
|
4570
|
-
for await (const chunk of readChunks(reader, signal)) {
|
|
4585
|
+
for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
|
|
4571
4586
|
state2.buffer += decoder.decode(chunk, { stream: true });
|
|
4572
4587
|
for (const record of drainCompleteRecords(state2)) yield record;
|
|
4573
4588
|
}
|
|
@@ -4577,14 +4592,38 @@ async function* parseSseStream(body, signal) {
|
|
|
4577
4592
|
releaseReader(reader);
|
|
4578
4593
|
}
|
|
4579
4594
|
}
|
|
4580
|
-
async function* readChunks(reader, signal) {
|
|
4595
|
+
async function* readChunks(reader, signal, idleTimeoutMs) {
|
|
4581
4596
|
while (true) {
|
|
4582
4597
|
if (signal.aborted) return;
|
|
4583
|
-
const { value, done } = await reader
|
|
4598
|
+
const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
|
|
4584
4599
|
if (done) return;
|
|
4585
4600
|
if (value !== void 0) yield value;
|
|
4586
4601
|
}
|
|
4587
4602
|
}
|
|
4603
|
+
function readWithIdleTimeout(reader, idleTimeoutMs) {
|
|
4604
|
+
if (idleTimeoutMs <= 0) return reader.read();
|
|
4605
|
+
return new Promise(
|
|
4606
|
+
(resolve3, reject) => {
|
|
4607
|
+
const timer = setTimeout(() => {
|
|
4608
|
+
reject(
|
|
4609
|
+
new NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
|
|
4610
|
+
code: "stream_idle_timeout"
|
|
4611
|
+
})
|
|
4612
|
+
);
|
|
4613
|
+
}, idleTimeoutMs);
|
|
4614
|
+
reader.read().then(
|
|
4615
|
+
(result) => {
|
|
4616
|
+
clearTimeout(timer);
|
|
4617
|
+
resolve3(result);
|
|
4618
|
+
},
|
|
4619
|
+
(err) => {
|
|
4620
|
+
clearTimeout(timer);
|
|
4621
|
+
reject(err);
|
|
4622
|
+
}
|
|
4623
|
+
);
|
|
4624
|
+
}
|
|
4625
|
+
);
|
|
4626
|
+
}
|
|
4588
4627
|
async function cancelReaderQuietly(reader) {
|
|
4589
4628
|
try {
|
|
4590
4629
|
await reader.cancel();
|
|
@@ -5278,6 +5317,9 @@ var PersonalityStore = class {
|
|
|
5278
5317
|
}
|
|
5279
5318
|
};
|
|
5280
5319
|
|
|
5320
|
+
// src/internal/plugins/manager.ts
|
|
5321
|
+
init_errors();
|
|
5322
|
+
|
|
5281
5323
|
// src/internal/plugins/context.ts
|
|
5282
5324
|
function createPluginContext() {
|
|
5283
5325
|
const registrations = {
|
|
@@ -5339,6 +5381,9 @@ var PluginManager = class {
|
|
|
5339
5381
|
memoryProviders: []
|
|
5340
5382
|
};
|
|
5341
5383
|
#initialized = false;
|
|
5384
|
+
// #68 — registrations of plugins added post-init via `register()`, keyed by
|
|
5385
|
+
// plugin name so a re-register REPLACES (not appends) the prior hooks.
|
|
5386
|
+
#byName = /* @__PURE__ */ new Map();
|
|
5342
5387
|
async initialize(plugins) {
|
|
5343
5388
|
if (this.#initialized) {
|
|
5344
5389
|
throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
|
|
@@ -5356,6 +5401,36 @@ var PluginManager = class {
|
|
|
5356
5401
|
await this.#dispatchPlugin(plugin);
|
|
5357
5402
|
}
|
|
5358
5403
|
}
|
|
5404
|
+
/**
|
|
5405
|
+
* #68 — register a single `general` plugin AFTER `initialize()` has run.
|
|
5406
|
+
*
|
|
5407
|
+
* The bulk `initialize()` is single-shot (one call per process); late
|
|
5408
|
+
* registration is a distinct, named operation used by adapters that install
|
|
5409
|
+
* a plugin per-session/per-request (e.g. the ACP permission veto, which is
|
|
5410
|
+
* installed once the permission mode + connection are known — after the
|
|
5411
|
+
* agent's own plugins were already initialized).
|
|
5412
|
+
*
|
|
5413
|
+
* Idempotent by plugin NAME: re-registering a plugin with the same name
|
|
5414
|
+
* REPLACES its prior hooks/tools instead of appending duplicates (the ACP
|
|
5415
|
+
* permission plugin is re-installed on every prompt).
|
|
5416
|
+
*
|
|
5417
|
+
* Only `general` plugins may be registered late — model-provider / memory
|
|
5418
|
+
* plugins are resolved during the bulk init and cannot be added afterwards.
|
|
5419
|
+
*/
|
|
5420
|
+
async register(plugin) {
|
|
5421
|
+
if (plugin.kind !== "general") {
|
|
5422
|
+
throw new ConfigurationError(
|
|
5423
|
+
`late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
|
|
5424
|
+
{ code: "plugin_late_register_kind" }
|
|
5425
|
+
);
|
|
5426
|
+
}
|
|
5427
|
+
const prior = this.#byName.get(plugin.name);
|
|
5428
|
+
if (prior !== void 0) this.#unmerge(prior);
|
|
5429
|
+
const { ctx, registrations } = createPluginContext();
|
|
5430
|
+
await plugin.register(ctx);
|
|
5431
|
+
this.#byName.set(plugin.name, registrations);
|
|
5432
|
+
this.#merge(registrations);
|
|
5433
|
+
}
|
|
5359
5434
|
get aggregated() {
|
|
5360
5435
|
return this.#aggregated;
|
|
5361
5436
|
}
|
|
@@ -5436,6 +5511,64 @@ var PluginManager = class {
|
|
|
5436
5511
|
}
|
|
5437
5512
|
}
|
|
5438
5513
|
}
|
|
5514
|
+
// #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
|
|
5515
|
+
// in order (per-handler errors logged, never thrown); transform hooks fold
|
|
5516
|
+
// over the payload (a handler returning a value replaces it).
|
|
5517
|
+
/** @internal */
|
|
5518
|
+
async #runFireAndForget(name, ctx) {
|
|
5519
|
+
for (const h of this.#aggregated.hooks.get(name) ?? []) {
|
|
5520
|
+
try {
|
|
5521
|
+
await h(ctx);
|
|
5522
|
+
} catch (err) {
|
|
5523
|
+
process.stderr.write(
|
|
5524
|
+
`[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
|
|
5525
|
+
`
|
|
5526
|
+
);
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
/** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
|
|
5531
|
+
async #runTransform(name, payload, ctx) {
|
|
5532
|
+
let current = payload;
|
|
5533
|
+
for (const h of this.#aggregated.hooks.get(name) ?? []) {
|
|
5534
|
+
try {
|
|
5535
|
+
const out = await h(current, ctx);
|
|
5536
|
+
if (out !== void 0) current = out;
|
|
5537
|
+
} catch (err) {
|
|
5538
|
+
process.stderr.write(
|
|
5539
|
+
`[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
|
|
5540
|
+
`
|
|
5541
|
+
);
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
return current;
|
|
5545
|
+
}
|
|
5546
|
+
/** #65 — fired after a tool call completes. @internal */
|
|
5547
|
+
runPostToolCallHooks(ctx) {
|
|
5548
|
+
return this.#runFireAndForget("post_tool_call", ctx);
|
|
5549
|
+
}
|
|
5550
|
+
/** #65 — fired before / after each LLM turn. @internal */
|
|
5551
|
+
runPreLlmCallHooks(ctx) {
|
|
5552
|
+
return this.#runFireAndForget("pre_llm_call", ctx);
|
|
5553
|
+
}
|
|
5554
|
+
runPostLlmCallHooks(ctx) {
|
|
5555
|
+
return this.#runFireAndForget("post_llm_call", ctx);
|
|
5556
|
+
}
|
|
5557
|
+
/** #65 — fired at run start / end. @internal */
|
|
5558
|
+
runOnSessionStartHooks(ctx) {
|
|
5559
|
+
return this.#runFireAndForget("on_session_start", ctx);
|
|
5560
|
+
}
|
|
5561
|
+
runOnSessionEndHooks(ctx) {
|
|
5562
|
+
return this.#runFireAndForget("on_session_end", ctx);
|
|
5563
|
+
}
|
|
5564
|
+
/** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
|
|
5565
|
+
runTransformToolResultHooks(results, ctx) {
|
|
5566
|
+
return this.#runTransform("transform_tool_result", results, ctx);
|
|
5567
|
+
}
|
|
5568
|
+
/** #65 — transform the LLM output text before it is consumed. @internal */
|
|
5569
|
+
runTransformLlmOutputHooks(output, ctx) {
|
|
5570
|
+
return this.#runTransform("transform_llm_output", output, ctx);
|
|
5571
|
+
}
|
|
5439
5572
|
async #dispatchPlugin(plugin) {
|
|
5440
5573
|
if (plugin.kind === "general") {
|
|
5441
5574
|
const { ctx, registrations } = createPluginContext();
|
|
@@ -5463,7 +5596,29 @@ var PluginManager = class {
|
|
|
5463
5596
|
}
|
|
5464
5597
|
this.#aggregated.injected.push(...r.injected);
|
|
5465
5598
|
}
|
|
5599
|
+
/**
|
|
5600
|
+
* #68 — inverse of #merge: remove a prior registration's contributions from
|
|
5601
|
+
* the aggregated view by object identity. Used by `register()` to replace a
|
|
5602
|
+
* same-named plugin's hooks/tools instead of accumulating duplicates.
|
|
5603
|
+
*/
|
|
5604
|
+
#unmerge(r) {
|
|
5605
|
+
removeAll(this.#aggregated.tools, r.tools);
|
|
5606
|
+
removeAll(this.#aggregated.commands, r.commands);
|
|
5607
|
+
removeAll(this.#aggregated.injected, r.injected);
|
|
5608
|
+
for (const [hook, handlers] of r.hooks.entries()) {
|
|
5609
|
+
const existing = this.#aggregated.hooks.get(hook);
|
|
5610
|
+
if (existing === void 0) continue;
|
|
5611
|
+
removeAll(existing, handlers);
|
|
5612
|
+
if (existing.length === 0) this.#aggregated.hooks.delete(hook);
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5466
5615
|
};
|
|
5616
|
+
function removeAll(arr, toRemove) {
|
|
5617
|
+
for (const item of toRemove) {
|
|
5618
|
+
const idx = arr.indexOf(item);
|
|
5619
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5467
5622
|
|
|
5468
5623
|
// src/internal/telemetry/span-names.ts
|
|
5469
5624
|
var SPAN_NAMES = {
|
|
@@ -5471,7 +5626,12 @@ var SPAN_NAMES = {
|
|
|
5471
5626
|
AGENT_SEND: "agent.send",
|
|
5472
5627
|
MEMORY_RECALL: "memory.recall"};
|
|
5473
5628
|
var HISTOGRAM_NAMES = {
|
|
5474
|
-
MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
|
|
5629
|
+
MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
|
|
5630
|
+
TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
|
|
5631
|
+
LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
|
|
5632
|
+
LLM_TOKENS: "theokit_llm_tokens",
|
|
5633
|
+
/** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
|
|
5634
|
+
LLM_USAGE_MISSING: "theokit_llm_usage_missing"
|
|
5475
5635
|
};
|
|
5476
5636
|
function safeRequire(moduleName) {
|
|
5477
5637
|
try {
|
|
@@ -5806,7 +5966,25 @@ function createTelemetry(settings) {
|
|
|
5806
5966
|
enabled: true,
|
|
5807
5967
|
includeContent: settings.includeContent === true,
|
|
5808
5968
|
startSpan: startNewSpan,
|
|
5809
|
-
|
|
5969
|
+
// M3 #64 — actually nest the child under its parent instead of discarding it.
|
|
5970
|
+
// The parent's SpanContext is set on a fresh OTel context so the child links
|
|
5971
|
+
// to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
|
|
5972
|
+
// back to a root span when the parent has no valid span id (telemetry off /
|
|
5973
|
+
// NOOP), preserving the pre-M3 behavior for parentless callers.
|
|
5974
|
+
startChildSpan: (parent, name, attrs) => {
|
|
5975
|
+
const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
|
|
5976
|
+
const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
|
|
5977
|
+
const pctx = safe(() => parent?.spanContext(), void 0);
|
|
5978
|
+
const span = safe(() => {
|
|
5979
|
+
if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
|
|
5980
|
+
const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
|
|
5981
|
+
return tracer.startSpan(name, opts, childCtx);
|
|
5982
|
+
}
|
|
5983
|
+
return tracer.startSpan(name, opts);
|
|
5984
|
+
}, NOOP_SPAN);
|
|
5985
|
+
if (span !== NOOP_SPAN) openSpans.add(span);
|
|
5986
|
+
return wrapSpan(span, openSpans);
|
|
5987
|
+
},
|
|
5810
5988
|
recordHistogram,
|
|
5811
5989
|
endAll: () => {
|
|
5812
5990
|
for (const span of openSpans) safe(() => span.end(), void 0);
|
|
@@ -5842,12 +6020,62 @@ function redactAttrs(attrs) {
|
|
|
5842
6020
|
}
|
|
5843
6021
|
return out;
|
|
5844
6022
|
}
|
|
6023
|
+
|
|
6024
|
+
// src/internal/runtime/lifecycle/env-policy.ts
|
|
6025
|
+
var SECRET_PATTERNS = [
|
|
6026
|
+
/KEY/i,
|
|
6027
|
+
/SECRET/i,
|
|
6028
|
+
/TOKEN/i,
|
|
6029
|
+
/PASSWORD/i,
|
|
6030
|
+
/PASSWD/i,
|
|
6031
|
+
/PASSPHRASE/i,
|
|
6032
|
+
/[_-]PWD/i,
|
|
6033
|
+
/CREDENTIAL/i,
|
|
6034
|
+
/PRIVATE/i,
|
|
6035
|
+
/_AUTH/i
|
|
6036
|
+
];
|
|
6037
|
+
var CORE_VARS = [
|
|
6038
|
+
"PATH",
|
|
6039
|
+
"HOME",
|
|
6040
|
+
"SHELL",
|
|
6041
|
+
"LANG",
|
|
6042
|
+
"LC_ALL",
|
|
6043
|
+
"LC_CTYPE",
|
|
6044
|
+
"TMPDIR",
|
|
6045
|
+
"TMP",
|
|
6046
|
+
"TEMP",
|
|
6047
|
+
"USER",
|
|
6048
|
+
"LOGNAME"
|
|
6049
|
+
];
|
|
6050
|
+
function isSecretName(name) {
|
|
6051
|
+
return SECRET_PATTERNS.some((re) => re.test(name));
|
|
6052
|
+
}
|
|
6053
|
+
function inheritsUnderPolicy(name, policy) {
|
|
6054
|
+
if (policy === "all") return true;
|
|
6055
|
+
if (policy === "core") return CORE_VARS.includes(name);
|
|
6056
|
+
return !isSecretName(name);
|
|
6057
|
+
}
|
|
6058
|
+
function resolveChildEnv(options = {}) {
|
|
6059
|
+
const parent = options.parent ?? process.env;
|
|
6060
|
+
const policy = options.policy ?? "inherit-scrubbed";
|
|
6061
|
+
const base = {};
|
|
6062
|
+
for (const [name, value] of Object.entries(parent)) {
|
|
6063
|
+
if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
|
|
6064
|
+
}
|
|
6065
|
+
for (const [name, value] of Object.entries(options.overrides ?? {})) {
|
|
6066
|
+
base[name] = value;
|
|
6067
|
+
}
|
|
6068
|
+
return base;
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
// src/internal/runtime/lifecycle/spawn-collect.ts
|
|
5845
6072
|
function spawnAndCollect(options) {
|
|
5846
6073
|
return new Promise((resolve3) => {
|
|
5847
6074
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
5848
6075
|
const spawnOptions = {
|
|
5849
6076
|
cwd: options.cwd,
|
|
5850
|
-
|
|
6077
|
+
// #54 — scrub secret-like parent env by default; `options.env` still wins.
|
|
6078
|
+
env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
|
|
5851
6079
|
};
|
|
5852
6080
|
const child = spawn(options.command, options.args ?? [], spawnOptions);
|
|
5853
6081
|
let stdout = "";
|
|
@@ -6065,15 +6293,24 @@ function warnMalformed(agentId, line) {
|
|
|
6065
6293
|
`
|
|
6066
6294
|
);
|
|
6067
6295
|
}
|
|
6296
|
+
function hydrateSessionLine(parsed) {
|
|
6297
|
+
if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
|
|
6298
|
+
if (parsed.role === "user" || parsed.role === "assistant") {
|
|
6299
|
+
return { role: parsed.role, text: parsed.text };
|
|
6300
|
+
}
|
|
6301
|
+
if (parsed.role === "tool_call" || parsed.role === "tool_result") {
|
|
6302
|
+
const label = parsed.role === "tool_call" ? "tool call" : "tool result";
|
|
6303
|
+
return { role: "assistant", text: `[${label}] ${parsed.text}` };
|
|
6304
|
+
}
|
|
6305
|
+
return void 0;
|
|
6306
|
+
}
|
|
6068
6307
|
async function readSessionFile(cwd, agentId) {
|
|
6069
6308
|
const lines = await readJsonlLines(cwd, agentId);
|
|
6070
6309
|
const messages = [];
|
|
6071
6310
|
for (const line of lines) {
|
|
6072
6311
|
try {
|
|
6073
|
-
const
|
|
6074
|
-
if (
|
|
6075
|
-
messages.push({ role: parsed.role, text: parsed.text });
|
|
6076
|
-
}
|
|
6312
|
+
const msg = hydrateSessionLine(JSON.parse(line));
|
|
6313
|
+
if (msg !== void 0) messages.push(msg);
|
|
6077
6314
|
} catch {
|
|
6078
6315
|
warnMalformed(agentId, line);
|
|
6079
6316
|
}
|
|
@@ -6100,24 +6337,72 @@ async function readAllPersistedMessages(cwd, agentId) {
|
|
|
6100
6337
|
return messages;
|
|
6101
6338
|
}
|
|
6102
6339
|
async function appendAnyPersistedMessage(cwd, agentId, record) {
|
|
6103
|
-
|
|
6104
|
-
await mkdir(dirname(path), { recursive: true });
|
|
6105
|
-
await appendFile(path, `${redactSecrets(JSON.stringify(record))}
|
|
6106
|
-
`, "utf8");
|
|
6340
|
+
await appendPersistedMessages(cwd, agentId, [record]);
|
|
6107
6341
|
}
|
|
6108
|
-
async function
|
|
6342
|
+
async function appendPersistedMessages(cwd, agentId, records) {
|
|
6343
|
+
if (records.length === 0) return;
|
|
6109
6344
|
const path = sessionFilePath(cwd, agentId);
|
|
6110
|
-
|
|
6345
|
+
const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
|
|
6346
|
+
`).join("");
|
|
6347
|
+
const dir = dirname(path);
|
|
6348
|
+
let written = false;
|
|
6349
|
+
const attempt = async () => {
|
|
6350
|
+
await mkdir(dir, { recursive: true });
|
|
6351
|
+
await withFileLock(path, async () => {
|
|
6352
|
+
await appendFile(path, payload, "utf8");
|
|
6353
|
+
written = true;
|
|
6354
|
+
});
|
|
6355
|
+
};
|
|
6111
6356
|
try {
|
|
6112
|
-
|
|
6113
|
-
} catch {
|
|
6114
|
-
|
|
6357
|
+
await attempt();
|
|
6358
|
+
} catch (cause) {
|
|
6359
|
+
if (written || cause.code !== "ENOENT") throw cause;
|
|
6360
|
+
await attempt();
|
|
6115
6361
|
}
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6362
|
+
}
|
|
6363
|
+
async function rewriteLockedSession(path, transform) {
|
|
6364
|
+
await withFileLock(path, async () => {
|
|
6365
|
+
let raw;
|
|
6366
|
+
try {
|
|
6367
|
+
raw = await readFile(path, "utf8");
|
|
6368
|
+
} catch {
|
|
6369
|
+
return;
|
|
6370
|
+
}
|
|
6371
|
+
const lines = raw.split("\n").filter((line) => line.length > 0);
|
|
6372
|
+
const next = transform(lines);
|
|
6373
|
+
if (next === void 0) return;
|
|
6374
|
+
await replaceFileAtomic(path, next);
|
|
6375
|
+
});
|
|
6376
|
+
}
|
|
6377
|
+
async function compactSessionFile(cwd, agentId, maxTurns) {
|
|
6378
|
+
const path = sessionFilePath(cwd, agentId);
|
|
6379
|
+
if (!existsSync(path)) return;
|
|
6380
|
+
await rewriteLockedSession(
|
|
6381
|
+
path,
|
|
6382
|
+
(lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
|
|
6383
|
+
`
|
|
6384
|
+
);
|
|
6385
|
+
}
|
|
6386
|
+
async function truncateSessionTo(cwd, agentId, keepCount) {
|
|
6387
|
+
const path = sessionFilePath(cwd, agentId);
|
|
6388
|
+
if (!existsSync(path)) return 0;
|
|
6389
|
+
let kept = 0;
|
|
6390
|
+
await rewriteLockedSession(path, (lines) => {
|
|
6391
|
+
const keep = Math.max(0, Math.min(keepCount, lines.length));
|
|
6392
|
+
kept = keep;
|
|
6393
|
+
if (keep === lines.length) return void 0;
|
|
6394
|
+
return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
|
|
6119
6395
|
`;
|
|
6120
|
-
|
|
6396
|
+
});
|
|
6397
|
+
return kept;
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
// src/internal/persistence/pagination.ts
|
|
6401
|
+
function paginate(items, opts) {
|
|
6402
|
+
if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
|
|
6403
|
+
const start = Math.max(0, opts.offset ?? 0);
|
|
6404
|
+
const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
|
|
6405
|
+
return items.slice(start, end);
|
|
6121
6406
|
}
|
|
6122
6407
|
|
|
6123
6408
|
// src/internal/persistence/conversation-storage-fs.ts
|
|
@@ -6130,23 +6415,31 @@ var FileSystemConversationStorage = class {
|
|
|
6130
6415
|
get root() {
|
|
6131
6416
|
return this.#root;
|
|
6132
6417
|
}
|
|
6133
|
-
async getMessages(conversationId) {
|
|
6418
|
+
async getMessages(conversationId, opts) {
|
|
6134
6419
|
const records = await readAllPersistedMessages(this.#root, conversationId);
|
|
6135
|
-
|
|
6420
|
+
const all = records.map(toStoredMessage);
|
|
6421
|
+
return paginate(all, opts);
|
|
6136
6422
|
}
|
|
6137
6423
|
async appendMessage(conversationId, message) {
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6424
|
+
await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
|
|
6425
|
+
}
|
|
6426
|
+
async appendMessages(conversationId, messages) {
|
|
6427
|
+
await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
|
|
6428
|
+
}
|
|
6429
|
+
async truncateConversation(conversationId, keepCount) {
|
|
6430
|
+
return truncateSessionTo(this.#root, conversationId, keepCount);
|
|
6144
6431
|
}
|
|
6145
6432
|
async deleteConversation(conversationId) {
|
|
6146
6433
|
const safe3 = sanitizeIdentifier(conversationId, { maxLen: 128 });
|
|
6147
6434
|
const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe3);
|
|
6148
6435
|
await rm(dirPath, { recursive: true, force: true });
|
|
6149
6436
|
}
|
|
6437
|
+
async deleteScope(prefix) {
|
|
6438
|
+
const ids = await this.listConversationIds();
|
|
6439
|
+
const matching = ids.filter((id) => id.startsWith(prefix));
|
|
6440
|
+
for (const id of matching) await this.deleteConversation(id);
|
|
6441
|
+
return matching.length;
|
|
6442
|
+
}
|
|
6150
6443
|
async listConversationIds(opts = {}) {
|
|
6151
6444
|
const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
|
|
6152
6445
|
let entries;
|
|
@@ -6172,6 +6465,9 @@ function toStoredMessage(record) {
|
|
|
6172
6465
|
at: record.at
|
|
6173
6466
|
};
|
|
6174
6467
|
}
|
|
6468
|
+
function toRecord(message) {
|
|
6469
|
+
return { role: message.role, text: message.content, at: message.at ?? Date.now() };
|
|
6470
|
+
}
|
|
6175
6471
|
|
|
6176
6472
|
// src/internal/runtime/session/agent-session.ts
|
|
6177
6473
|
var DEFAULT_MAX_TURNS = 200;
|
|
@@ -6257,12 +6553,19 @@ async function readPersistedForCache(adapter, agentId) {
|
|
|
6257
6553
|
const records = await adapter.getMessages(agentId);
|
|
6258
6554
|
const out = [];
|
|
6259
6555
|
for (const r of records) {
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
}
|
|
6556
|
+
const folded = foldStoredToSession(r);
|
|
6557
|
+
if (folded !== void 0) out.push(folded);
|
|
6263
6558
|
}
|
|
6264
6559
|
return out;
|
|
6265
6560
|
}
|
|
6561
|
+
function foldStoredToSession(r) {
|
|
6562
|
+
if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
|
|
6563
|
+
if (r.role === "tool_call" || r.role === "tool_result") {
|
|
6564
|
+
const label = r.role === "tool_call" ? "tool call" : "tool result";
|
|
6565
|
+
return { role: "assistant", text: `[${label}] ${r.content}` };
|
|
6566
|
+
}
|
|
6567
|
+
return void 0;
|
|
6568
|
+
}
|
|
6266
6569
|
async function flushSessionWrites() {
|
|
6267
6570
|
while (pendingAppends.size > 0) {
|
|
6268
6571
|
const all = Array.from(pendingAppends.values());
|
|
@@ -8612,11 +8915,32 @@ function reasoningEffortFromParams(params) {
|
|
|
8612
8915
|
const thinking = params?.find((p) => p.id === "thinking");
|
|
8613
8916
|
return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
|
|
8614
8917
|
}
|
|
8918
|
+
function emitLlmMetrics(inputs, result, startAt) {
|
|
8919
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
|
|
8920
|
+
provider: inputs.llm.name
|
|
8921
|
+
});
|
|
8922
|
+
if (result.inputTokens === void 0 && result.outputTokens === void 0) {
|
|
8923
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
|
|
8924
|
+
provider: inputs.llm.name
|
|
8925
|
+
});
|
|
8926
|
+
process.stderr.write(
|
|
8927
|
+
`[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
|
|
8928
|
+
`
|
|
8929
|
+
);
|
|
8930
|
+
return;
|
|
8931
|
+
}
|
|
8932
|
+
inputs.telemetry?.recordHistogram(
|
|
8933
|
+
HISTOGRAM_NAMES.LLM_TOKENS,
|
|
8934
|
+
(result.inputTokens ?? 0) + (result.outputTokens ?? 0),
|
|
8935
|
+
{ provider: inputs.llm.name }
|
|
8936
|
+
);
|
|
8937
|
+
}
|
|
8615
8938
|
async function streamLlmTurn(inputs, ctx) {
|
|
8616
|
-
const llmSpan = inputs.telemetry?.
|
|
8939
|
+
const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
|
|
8617
8940
|
"model.id": inputs.model.id ?? "auto",
|
|
8618
8941
|
provider: inputs.llm.name
|
|
8619
8942
|
});
|
|
8943
|
+
const startAt = Date.now();
|
|
8620
8944
|
const signal = inputs.signal ?? new AbortController().signal;
|
|
8621
8945
|
const generator = inputs.llm.stream(
|
|
8622
8946
|
{
|
|
@@ -8659,6 +8983,7 @@ async function streamLlmTurn(inputs, ctx) {
|
|
|
8659
8983
|
inputTokens: result.inputTokens ?? 0,
|
|
8660
8984
|
outputTokens: result.outputTokens ?? 0
|
|
8661
8985
|
});
|
|
8986
|
+
emitLlmMetrics(inputs, result, startAt);
|
|
8662
8987
|
llmSpan?.end();
|
|
8663
8988
|
const stripped = stripThinkBlocks(collected.accumulatedText);
|
|
8664
8989
|
return {
|
|
@@ -8888,21 +9213,21 @@ async function executeTool(inputs, resolved, call) {
|
|
|
8888
9213
|
}
|
|
8889
9214
|
if (resolved.origin === "shell") return runShellTool(inputs, call);
|
|
8890
9215
|
if (resolved.origin === "memory") return runMemoryTool(resolved, call);
|
|
8891
|
-
if (resolved.origin === "custom") return runCustomTool(resolved, call);
|
|
9216
|
+
if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
|
|
8892
9217
|
return runMcpTool(inputs, resolved, call);
|
|
8893
9218
|
}
|
|
8894
9219
|
async function runMemoryTool(resolved, call) {
|
|
8895
9220
|
return runHandlerTool("memory", resolved.memoryHandler, call);
|
|
8896
9221
|
}
|
|
8897
|
-
async function runCustomTool(resolved, call) {
|
|
8898
|
-
return runHandlerTool("custom", resolved.customHandler, call);
|
|
9222
|
+
async function runCustomTool(resolved, call, signal) {
|
|
9223
|
+
return runHandlerTool("custom", resolved.customHandler, call, signal);
|
|
8899
9224
|
}
|
|
8900
|
-
async function runHandlerTool(kind, handler, call) {
|
|
9225
|
+
async function runHandlerTool(kind, handler, call, signal) {
|
|
8901
9226
|
if (handler === void 0) {
|
|
8902
9227
|
return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
|
|
8903
9228
|
}
|
|
8904
9229
|
try {
|
|
8905
|
-
const stdout = await handler(call.input);
|
|
9230
|
+
const stdout = await handler(call.input, { signal });
|
|
8906
9231
|
return { stdout, stderr: "", exitCode: 0 };
|
|
8907
9232
|
} catch (cause) {
|
|
8908
9233
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -8948,22 +9273,66 @@ ${result.stderr}`.trim();
|
|
|
8948
9273
|
return result.stdout.trim();
|
|
8949
9274
|
}
|
|
8950
9275
|
|
|
9276
|
+
// src/internal/agent-loop/tool-timeout.ts
|
|
9277
|
+
var TOOL_ABORTED_EXIT = 124;
|
|
9278
|
+
function abortedResult(signal) {
|
|
9279
|
+
const reason = signal.reason;
|
|
9280
|
+
const timedOut = reason?.name === "TimeoutError";
|
|
9281
|
+
return {
|
|
9282
|
+
stdout: "",
|
|
9283
|
+
stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
|
|
9284
|
+
exitCode: TOOL_ABORTED_EXIT
|
|
9285
|
+
};
|
|
9286
|
+
}
|
|
9287
|
+
function raceToolExecution(exec, opts) {
|
|
9288
|
+
const { signal, timeoutMs } = opts;
|
|
9289
|
+
if (signal === void 0 && timeoutMs === void 0) return exec;
|
|
9290
|
+
const signals = [];
|
|
9291
|
+
if (signal !== void 0) signals.push(signal);
|
|
9292
|
+
if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
|
|
9293
|
+
const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
9294
|
+
if (merged.aborted) return Promise.resolve(abortedResult(merged));
|
|
9295
|
+
return new Promise((resolve3, reject) => {
|
|
9296
|
+
let settled = false;
|
|
9297
|
+
const onAbort = () => {
|
|
9298
|
+
if (settled) return;
|
|
9299
|
+
settled = true;
|
|
9300
|
+
resolve3(abortedResult(merged));
|
|
9301
|
+
};
|
|
9302
|
+
merged.addEventListener("abort", onAbort, { once: true });
|
|
9303
|
+
exec.then(
|
|
9304
|
+
(r) => {
|
|
9305
|
+
if (settled) return;
|
|
9306
|
+
settled = true;
|
|
9307
|
+
merged.removeEventListener("abort", onAbort);
|
|
9308
|
+
resolve3(r);
|
|
9309
|
+
},
|
|
9310
|
+
(e) => {
|
|
9311
|
+
if (settled) return;
|
|
9312
|
+
settled = true;
|
|
9313
|
+
merged.removeEventListener("abort", onAbort);
|
|
9314
|
+
reject(e);
|
|
9315
|
+
}
|
|
9316
|
+
);
|
|
9317
|
+
});
|
|
9318
|
+
}
|
|
9319
|
+
|
|
8951
9320
|
// src/internal/agent-loop/tool-dispatch.ts
|
|
8952
|
-
async function dispatchTools(inputs, tools, toolCalls, events) {
|
|
9321
|
+
async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
|
|
8953
9322
|
const maxConcurrent = inputs.maxConcurrentTools ?? 4;
|
|
8954
9323
|
return mapWithConcurrency(
|
|
8955
9324
|
toolCalls,
|
|
8956
9325
|
maxConcurrent,
|
|
8957
|
-
(call) => dispatchSingleCall(inputs, tools, call, events)
|
|
9326
|
+
(call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
|
|
8958
9327
|
);
|
|
8959
9328
|
}
|
|
8960
|
-
async function dispatchSingleCall(inputs, tools, call, events) {
|
|
9329
|
+
async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
|
|
8961
9330
|
const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
|
|
8962
9331
|
const callId = generateCallId();
|
|
8963
9332
|
const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
|
|
8964
9333
|
if (forkVeto !== void 0) return forkVeto;
|
|
8965
9334
|
const resolved = tools.find((tool) => tool.name === workingCall.name);
|
|
8966
|
-
const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
|
|
9335
|
+
const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
|
|
8967
9336
|
events.push(buildToolUseRunning(inputs, callId, workingCall));
|
|
8968
9337
|
const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
|
|
8969
9338
|
if (pluginVeto !== void 0) {
|
|
@@ -8980,6 +9349,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
|
|
|
8980
9349
|
return fileVeto;
|
|
8981
9350
|
}
|
|
8982
9351
|
const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
|
|
9352
|
+
await inputs.pluginManager?.runPostToolCallHooks({
|
|
9353
|
+
name: workingCall.name,
|
|
9354
|
+
args: workingCall.input,
|
|
9355
|
+
result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
|
|
9356
|
+
agentId: inputs.agentId,
|
|
9357
|
+
runId: inputs.runId
|
|
9358
|
+
});
|
|
8983
9359
|
return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
|
|
8984
9360
|
}
|
|
8985
9361
|
function applyRepairAndExtractCall(tools, call) {
|
|
@@ -9012,8 +9388,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
|
|
|
9012
9388
|
content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
|
|
9013
9389
|
};
|
|
9014
9390
|
}
|
|
9015
|
-
function startToolCallSpan(inputs, call, resolved, callId, repairs) {
|
|
9016
|
-
const toolSpan = inputs.telemetry?.
|
|
9391
|
+
function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
|
|
9392
|
+
const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
|
|
9017
9393
|
"tool.name": call.name,
|
|
9018
9394
|
"tool.origin": resolved?.origin ?? "unknown",
|
|
9019
9395
|
callId
|
|
@@ -9077,8 +9453,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
|
|
|
9077
9453
|
conversationId: inputs.agentId,
|
|
9078
9454
|
callId
|
|
9079
9455
|
});
|
|
9080
|
-
const result = await executeTool(inputs, resolved, call)
|
|
9456
|
+
const result = await raceToolExecution(executeTool(inputs, resolved, call), {
|
|
9457
|
+
signal: inputs.signal,
|
|
9458
|
+
timeoutMs: inputs.perToolTimeoutMs
|
|
9459
|
+
});
|
|
9081
9460
|
const durationMs = Date.now() - startAt;
|
|
9461
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
|
|
9462
|
+
"tool.name": call.name
|
|
9463
|
+
});
|
|
9082
9464
|
if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
|
|
9083
9465
|
await safeEmitToolHook(inputs.onToolError, {
|
|
9084
9466
|
toolName: call.name,
|
|
@@ -9177,6 +9559,35 @@ function buildToolUseCompleted(inputs, callId, call, result) {
|
|
|
9177
9559
|
};
|
|
9178
9560
|
}
|
|
9179
9561
|
|
|
9562
|
+
// src/internal/agent-loop/tool-result-guard.ts
|
|
9563
|
+
var OPEN = "<untrusted-tool-output>";
|
|
9564
|
+
var CLOSE = "</untrusted-tool-output>";
|
|
9565
|
+
var PII_PATTERNS = [
|
|
9566
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
9567
|
+
// email
|
|
9568
|
+
/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
|
|
9569
|
+
// phone
|
|
9570
|
+
];
|
|
9571
|
+
function guardText(content, opts) {
|
|
9572
|
+
let out = content;
|
|
9573
|
+
if (opts.redactPii === true) {
|
|
9574
|
+
for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
|
|
9575
|
+
}
|
|
9576
|
+
if (opts.delimit === true) {
|
|
9577
|
+
const safe3 = out.split(CLOSE).join("</ untrusted-tool-output>");
|
|
9578
|
+
out = `${OPEN}
|
|
9579
|
+
${safe3}
|
|
9580
|
+
${CLOSE}`;
|
|
9581
|
+
}
|
|
9582
|
+
return out;
|
|
9583
|
+
}
|
|
9584
|
+
function applyToolResultGuard(parts, opts) {
|
|
9585
|
+
if (opts.delimit !== true && opts.redactPii !== true) return parts;
|
|
9586
|
+
return parts.map(
|
|
9587
|
+
(p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
|
|
9588
|
+
);
|
|
9589
|
+
}
|
|
9590
|
+
|
|
9180
9591
|
// src/internal/budget/pricing-data.json
|
|
9181
9592
|
var pricing_data_default = {
|
|
9182
9593
|
_meta: {
|
|
@@ -9527,6 +9938,11 @@ async function runAgentLoop(inputs) {
|
|
|
9527
9938
|
try {
|
|
9528
9939
|
const ctx = await initLoopContext(inputs);
|
|
9529
9940
|
ctxRef = ctx;
|
|
9941
|
+
ctx.sendSpan = sendSpan;
|
|
9942
|
+
await inputs.pluginManager?.runOnSessionStartHooks({
|
|
9943
|
+
agentId: inputs.agentId,
|
|
9944
|
+
runId: inputs.runId
|
|
9945
|
+
});
|
|
9530
9946
|
const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
|
|
9531
9947
|
let lastTurnDecision;
|
|
9532
9948
|
while (budget.shouldContinue()) {
|
|
@@ -9554,6 +9970,7 @@ async function runAgentLoop(inputs) {
|
|
|
9554
9970
|
}
|
|
9555
9971
|
budget.consume();
|
|
9556
9972
|
inputs.budgetTracker?.nextIteration?.();
|
|
9973
|
+
if (inputs.signal?.aborted === true) break;
|
|
9557
9974
|
}
|
|
9558
9975
|
if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
|
|
9559
9976
|
ctx.stoppedAtIterationLimit = true;
|
|
@@ -9593,6 +10010,10 @@ async function runAgentLoop(inputs) {
|
|
|
9593
10010
|
...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
|
|
9594
10011
|
};
|
|
9595
10012
|
} finally {
|
|
10013
|
+
await inputs.pluginManager?.runOnSessionEndHooks({
|
|
10014
|
+
agentId: inputs.agentId,
|
|
10015
|
+
runId: inputs.runId
|
|
10016
|
+
});
|
|
9596
10017
|
if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
|
|
9597
10018
|
try {
|
|
9598
10019
|
await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
|
|
@@ -9696,7 +10117,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
|
|
|
9696
10117
|
return "done";
|
|
9697
10118
|
}
|
|
9698
10119
|
async function runIteration(inputs, ctx) {
|
|
10120
|
+
const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
|
|
10121
|
+
await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
|
|
9699
10122
|
const llmOutput = await streamLlmTurn(inputs, ctx);
|
|
10123
|
+
await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
|
|
9700
10124
|
accumulateUsage(ctx.usage, llmOutput);
|
|
9701
10125
|
if (inputs.budgetTracker !== void 0) {
|
|
9702
10126
|
const modelId = inputs.model.id ?? "auto";
|
|
@@ -9722,6 +10146,13 @@ async function runIteration(inputs, ctx) {
|
|
|
9722
10146
|
}
|
|
9723
10147
|
return continueOrTerminate(inputs, ctx, llmOutput);
|
|
9724
10148
|
}
|
|
10149
|
+
async function transformLlmOutputText(inputs, text, ctx) {
|
|
10150
|
+
return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
|
|
10151
|
+
}
|
|
10152
|
+
async function guardAndTransformToolResults(inputs, raw, ctx) {
|
|
10153
|
+
const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
|
|
10154
|
+
return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
|
|
10155
|
+
}
|
|
9725
10156
|
async function continueOrTerminate(inputs, ctx, llmOutput) {
|
|
9726
10157
|
if (llmOutput.errored) return "error";
|
|
9727
10158
|
if (llmOutput.text.length > 0) {
|
|
@@ -9730,8 +10161,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
|
|
|
9730
10161
|
if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
|
|
9731
10162
|
return finishOrReflect(inputs, ctx, llmOutput);
|
|
9732
10163
|
}
|
|
9733
|
-
|
|
9734
|
-
const
|
|
10164
|
+
const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
|
|
10165
|
+
const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
|
|
10166
|
+
ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
|
|
10167
|
+
const rawResults = await dispatchTools(
|
|
10168
|
+
inputs,
|
|
10169
|
+
ctx.tools,
|
|
10170
|
+
llmOutput.toolCalls,
|
|
10171
|
+
ctx.events,
|
|
10172
|
+
ctx.sendSpan
|
|
10173
|
+
// M3 #64 — nest tool.call spans under agent.send
|
|
10174
|
+
);
|
|
10175
|
+
const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
|
|
9735
10176
|
ctx.messages.push({ role: "user", content: toolResults });
|
|
9736
10177
|
if (inputs.onStep !== void 0) {
|
|
9737
10178
|
const cb = inputs.onStep;
|
|
@@ -10161,6 +10602,56 @@ function mapAnthropicStatusToCode(status, body) {
|
|
|
10161
10602
|
function formatMessage(status, code) {
|
|
10162
10603
|
return `Anthropic API error: ${code} (HTTP ${status})`;
|
|
10163
10604
|
}
|
|
10605
|
+
var cachedJsonrepair;
|
|
10606
|
+
function loadJsonrepair() {
|
|
10607
|
+
if (cachedJsonrepair === void 0) {
|
|
10608
|
+
const req = createRequire(import.meta.url);
|
|
10609
|
+
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
10610
|
+
}
|
|
10611
|
+
return cachedJsonrepair;
|
|
10612
|
+
}
|
|
10613
|
+
function isPlainObject(v) {
|
|
10614
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10615
|
+
}
|
|
10616
|
+
function toFiniteNumber(raw) {
|
|
10617
|
+
if (raw === "") return void 0;
|
|
10618
|
+
const n = Number(raw);
|
|
10619
|
+
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
10620
|
+
}
|
|
10621
|
+
function tryJson(raw, repair) {
|
|
10622
|
+
const t = raw.trimStart();
|
|
10623
|
+
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
10624
|
+
try {
|
|
10625
|
+
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
10626
|
+
} catch {
|
|
10627
|
+
return void 0;
|
|
10628
|
+
}
|
|
10629
|
+
}
|
|
10630
|
+
function heuristicCoerce(raw, repairJson) {
|
|
10631
|
+
if (raw === "true") return true;
|
|
10632
|
+
if (raw === "false") return false;
|
|
10633
|
+
if (raw === "null") return null;
|
|
10634
|
+
const n = toFiniteNumber(raw);
|
|
10635
|
+
if (n !== void 0) return n;
|
|
10636
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
10637
|
+
return json === void 0 ? raw : json;
|
|
10638
|
+
}
|
|
10639
|
+
function coerceCandidates(raw, repairJson) {
|
|
10640
|
+
const out = [];
|
|
10641
|
+
if (raw === "true") out.push(true);
|
|
10642
|
+
else if (raw === "false") out.push(false);
|
|
10643
|
+
else if (raw === "null") out.push(null);
|
|
10644
|
+
const n = toFiniteNumber(raw);
|
|
10645
|
+
if (n !== void 0) out.push(n);
|
|
10646
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
10647
|
+
if (json !== void 0) out.push(json);
|
|
10648
|
+
out.push(raw);
|
|
10649
|
+
return out;
|
|
10650
|
+
}
|
|
10651
|
+
function objectShape(schema) {
|
|
10652
|
+
const shape = schema?.shape;
|
|
10653
|
+
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
10654
|
+
}
|
|
10164
10655
|
|
|
10165
10656
|
// src/internal/llm/finish.ts
|
|
10166
10657
|
function collapseSystemText(system) {
|
|
@@ -10173,9 +10664,21 @@ function parseToolArguments(buffered) {
|
|
|
10173
10664
|
try {
|
|
10174
10665
|
return JSON.parse(buffered);
|
|
10175
10666
|
} catch {
|
|
10667
|
+
const repaired = tryJson(buffered, true);
|
|
10668
|
+
if (isPlainObject(repaired)) return repaired;
|
|
10176
10669
|
return { raw: buffered };
|
|
10177
10670
|
}
|
|
10178
10671
|
}
|
|
10672
|
+
function mapOpenAIFinish(reason) {
|
|
10673
|
+
switch (reason) {
|
|
10674
|
+
case "tool_calls":
|
|
10675
|
+
return "tool_use";
|
|
10676
|
+
case "length":
|
|
10677
|
+
return "max_tokens";
|
|
10678
|
+
default:
|
|
10679
|
+
return "end_turn";
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10179
10682
|
function makeLlmFinish(state2) {
|
|
10180
10683
|
const finish = {
|
|
10181
10684
|
stopReason: state2.stopReason,
|
|
@@ -11057,56 +11560,9 @@ function toOllamaTools(tools) {
|
|
|
11057
11560
|
}
|
|
11058
11561
|
}));
|
|
11059
11562
|
}
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
const req = createRequire(import.meta.url);
|
|
11064
|
-
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
11065
|
-
}
|
|
11066
|
-
return cachedJsonrepair;
|
|
11067
|
-
}
|
|
11068
|
-
function isPlainObject(v) {
|
|
11069
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
11070
|
-
}
|
|
11071
|
-
function toFiniteNumber(raw) {
|
|
11072
|
-
if (raw === "") return void 0;
|
|
11073
|
-
const n = Number(raw);
|
|
11074
|
-
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
11075
|
-
}
|
|
11076
|
-
function tryJson(raw, repair) {
|
|
11077
|
-
const t = raw.trimStart();
|
|
11078
|
-
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
11079
|
-
try {
|
|
11080
|
-
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
11081
|
-
} catch {
|
|
11082
|
-
return void 0;
|
|
11083
|
-
}
|
|
11084
|
-
}
|
|
11085
|
-
function heuristicCoerce(raw, repairJson) {
|
|
11086
|
-
if (raw === "true") return true;
|
|
11087
|
-
if (raw === "false") return false;
|
|
11088
|
-
if (raw === "null") return null;
|
|
11089
|
-
const n = toFiniteNumber(raw);
|
|
11090
|
-
if (n !== void 0) return n;
|
|
11091
|
-
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
11092
|
-
return json === void 0 ? raw : json;
|
|
11093
|
-
}
|
|
11094
|
-
function coerceCandidates(raw, repairJson) {
|
|
11095
|
-
const out = [];
|
|
11096
|
-
if (raw === "true") out.push(true);
|
|
11097
|
-
else if (raw === "false") out.push(false);
|
|
11098
|
-
else if (raw === "null") out.push(null);
|
|
11099
|
-
const n = toFiniteNumber(raw);
|
|
11100
|
-
if (n !== void 0) out.push(n);
|
|
11101
|
-
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
11102
|
-
if (json !== void 0) out.push(json);
|
|
11103
|
-
out.push(raw);
|
|
11104
|
-
return out;
|
|
11105
|
-
}
|
|
11106
|
-
function objectShape(schema) {
|
|
11107
|
-
const shape = schema?.shape;
|
|
11108
|
-
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
11109
|
-
}
|
|
11563
|
+
|
|
11564
|
+
// src/internal/llm/openai.ts
|
|
11565
|
+
init_errors();
|
|
11110
11566
|
|
|
11111
11567
|
// src/sanitize/sanitize-tool-input.ts
|
|
11112
11568
|
function applyTrim(key, value, ctx) {
|
|
@@ -11203,6 +11659,77 @@ function parseHermesParams(inner) {
|
|
|
11203
11659
|
}
|
|
11204
11660
|
return sanitizeToolInput(input, { trim: true }).value;
|
|
11205
11661
|
}
|
|
11662
|
+
var STREAM_MARKER = "<function=";
|
|
11663
|
+
var DEFAULT_STREAM_BUFFER_CAP = 8192;
|
|
11664
|
+
var isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
|
|
11665
|
+
function streamToolCallBufferState(held, allowedToolNames, cap = DEFAULT_STREAM_BUFFER_CAP) {
|
|
11666
|
+
if (allowedToolNames.size === 0) return "impossible";
|
|
11667
|
+
const t = held.trimStart();
|
|
11668
|
+
if (t.length < STREAM_MARKER.length) {
|
|
11669
|
+
return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
|
|
11670
|
+
}
|
|
11671
|
+
if (!t.startsWith(STREAM_MARKER)) return "impossible";
|
|
11672
|
+
const parsed = parseStreamMarkerName(t);
|
|
11673
|
+
if (parsed === "building") return "possible";
|
|
11674
|
+
if (parsed === "invalid") return "impossible";
|
|
11675
|
+
const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
|
|
11676
|
+
if (!nameOk) return "impossible";
|
|
11677
|
+
return held.length > cap ? "impossible" : "possible";
|
|
11678
|
+
}
|
|
11679
|
+
function parseStreamMarkerName(t) {
|
|
11680
|
+
let cursor = STREAM_MARKER.length;
|
|
11681
|
+
while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
|
|
11682
|
+
const nameStart = cursor;
|
|
11683
|
+
while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
|
|
11684
|
+
const name = t.slice(nameStart, cursor);
|
|
11685
|
+
if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
|
|
11686
|
+
return { name, complete: cursor < t.length && t[cursor] === ">" };
|
|
11687
|
+
}
|
|
11688
|
+
function someToolNameStartsWith(allowedToolNames, prefix) {
|
|
11689
|
+
for (const name of allowedToolNames) {
|
|
11690
|
+
if (name.startsWith(prefix)) return true;
|
|
11691
|
+
}
|
|
11692
|
+
return false;
|
|
11693
|
+
}
|
|
11694
|
+
function firstPossibleMarkerStart(held, allowedToolNames) {
|
|
11695
|
+
for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
|
|
11696
|
+
if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
|
|
11697
|
+
}
|
|
11698
|
+
return -1;
|
|
11699
|
+
}
|
|
11700
|
+
var StreamSuppressionBuffer = class {
|
|
11701
|
+
constructor(allowedToolNames) {
|
|
11702
|
+
this.allowedToolNames = allowedToolNames;
|
|
11703
|
+
}
|
|
11704
|
+
allowedToolNames;
|
|
11705
|
+
#held = "";
|
|
11706
|
+
/** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
|
|
11707
|
+
push(content) {
|
|
11708
|
+
this.#held += content;
|
|
11709
|
+
if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
|
|
11710
|
+
return void 0;
|
|
11711
|
+
const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
|
|
11712
|
+
if (holdStart > 0) {
|
|
11713
|
+
const flush2 = this.#held.slice(0, holdStart);
|
|
11714
|
+
this.#held = this.#held.slice(holdStart);
|
|
11715
|
+
return flush2;
|
|
11716
|
+
}
|
|
11717
|
+
const flush = this.#held;
|
|
11718
|
+
this.#held = "";
|
|
11719
|
+
return flush;
|
|
11720
|
+
}
|
|
11721
|
+
/** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
|
|
11722
|
+
* native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
|
|
11723
|
+
* (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
|
|
11724
|
+
drain(hasNativeCalls) {
|
|
11725
|
+
if (this.#held.length === 0) return void 0;
|
|
11726
|
+
const held = this.#held;
|
|
11727
|
+
this.#held = "";
|
|
11728
|
+
if (hasNativeCalls) return held;
|
|
11729
|
+
const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
|
|
11730
|
+
return residual.length > 0 ? residual : void 0;
|
|
11731
|
+
}
|
|
11732
|
+
};
|
|
11206
11733
|
|
|
11207
11734
|
// src/internal/llm/openai.ts
|
|
11208
11735
|
var OpenAIClient = class {
|
|
@@ -11280,8 +11807,12 @@ var OpenAIClient = class {
|
|
|
11280
11807
|
// model was actually given. Empty set (no tools) recovers nothing.
|
|
11281
11808
|
new Set(request.tools?.map((tool) => tool.name) ?? [])
|
|
11282
11809
|
);
|
|
11810
|
+
let sawDone = false;
|
|
11283
11811
|
for await (const record of parseSseStream(response.body, signal)) {
|
|
11284
|
-
if (record.data === "[DONE]")
|
|
11812
|
+
if (record.data === "[DONE]") {
|
|
11813
|
+
sawDone = true;
|
|
11814
|
+
break;
|
|
11815
|
+
}
|
|
11285
11816
|
let chunk;
|
|
11286
11817
|
try {
|
|
11287
11818
|
chunk = JSON.parse(record.data);
|
|
@@ -11301,6 +11832,13 @@ var OpenAIClient = class {
|
|
|
11301
11832
|
const events = accumulator.consume(chunk);
|
|
11302
11833
|
for (const event of events) yield event;
|
|
11303
11834
|
}
|
|
11835
|
+
if (!sawDone && !accumulator.finishReasonSeen) {
|
|
11836
|
+
throw new NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
|
|
11837
|
+
code: "stream_truncated"
|
|
11838
|
+
});
|
|
11839
|
+
}
|
|
11840
|
+
const drainEvent = accumulator.finalizeHeldText();
|
|
11841
|
+
if (drainEvent !== void 0) yield drainEvent;
|
|
11304
11842
|
return accumulator.finish();
|
|
11305
11843
|
}
|
|
11306
11844
|
};
|
|
@@ -11316,6 +11854,7 @@ var OpenAIStreamAccumulator = class {
|
|
|
11316
11854
|
this.extractFromContent = extractFromContent;
|
|
11317
11855
|
this.providerName = providerName;
|
|
11318
11856
|
this.allowedToolNames = allowedToolNames;
|
|
11857
|
+
this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
|
|
11319
11858
|
}
|
|
11320
11859
|
extractFromContent;
|
|
11321
11860
|
providerName;
|
|
@@ -11328,18 +11867,30 @@ var OpenAIStreamAccumulator = class {
|
|
|
11328
11867
|
cacheWriteTokens;
|
|
11329
11868
|
reasoningTokens;
|
|
11330
11869
|
toolCalls = /* @__PURE__ */ new Map();
|
|
11870
|
+
/** R7: present only when recovery is enabled AND the request declares tools — holds suspected
|
|
11871
|
+
* leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
|
|
11872
|
+
suppress;
|
|
11331
11873
|
consume(chunk) {
|
|
11332
11874
|
const events = [];
|
|
11333
11875
|
this.applyUsage(chunk.usage);
|
|
11334
11876
|
for (const choice of chunk.choices ?? []) {
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11877
|
+
events.push(...this.applyChoice(choice));
|
|
11878
|
+
}
|
|
11879
|
+
return events;
|
|
11880
|
+
}
|
|
11881
|
+
applyChoice(choice) {
|
|
11882
|
+
const events = [];
|
|
11883
|
+
const reasoningEvent = this.applyReasoningDelta(
|
|
11884
|
+
choice.delta?.reasoning ?? choice.delta?.reasoning_content
|
|
11885
|
+
);
|
|
11886
|
+
if (reasoningEvent !== void 0) events.push(reasoningEvent);
|
|
11887
|
+
const textEvent = this.applyContentDelta(choice.delta?.content);
|
|
11888
|
+
if (textEvent !== void 0) events.push(textEvent);
|
|
11889
|
+
this.mergeToolCallDeltas(choice.delta?.tool_calls);
|
|
11890
|
+
this.applyFinishReason(choice.finish_reason);
|
|
11891
|
+
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
11892
|
+
const flushEvent = this.finalizeHeldText();
|
|
11893
|
+
if (flushEvent !== void 0) events.push(flushEvent);
|
|
11343
11894
|
}
|
|
11344
11895
|
return events;
|
|
11345
11896
|
}
|
|
@@ -11364,7 +11915,17 @@ var OpenAIStreamAccumulator = class {
|
|
|
11364
11915
|
applyContentDelta(content) {
|
|
11365
11916
|
if (typeof content !== "string" || content.length === 0) return void 0;
|
|
11366
11917
|
this.text += content;
|
|
11367
|
-
return { type: "text_delta", text: content };
|
|
11918
|
+
if (this.suppress === void 0) return { type: "text_delta", text: content };
|
|
11919
|
+
const emit = this.suppress.push(content);
|
|
11920
|
+
return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
|
|
11921
|
+
}
|
|
11922
|
+
/** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
|
|
11923
|
+
* SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
|
|
11924
|
+
* held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
|
|
11925
|
+
* buffer streams the held text whole. Idempotent once drained. */
|
|
11926
|
+
finalizeHeldText() {
|
|
11927
|
+
const emit = this.suppress?.drain(this.toolCalls.size > 0);
|
|
11928
|
+
return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
|
|
11368
11929
|
}
|
|
11369
11930
|
mergeToolCallDeltas(deltas) {
|
|
11370
11931
|
for (const call of deltas ?? []) {
|
|
@@ -11375,8 +11936,15 @@ var OpenAIStreamAccumulator = class {
|
|
|
11375
11936
|
this.toolCalls.set(call.index, existing);
|
|
11376
11937
|
}
|
|
11377
11938
|
}
|
|
11939
|
+
/** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
|
|
11940
|
+
* stream ending without `[DONE]` is a truncation, not a clean end). */
|
|
11941
|
+
sawFinishReason = false;
|
|
11942
|
+
get finishReasonSeen() {
|
|
11943
|
+
return this.sawFinishReason;
|
|
11944
|
+
}
|
|
11378
11945
|
applyFinishReason(reason) {
|
|
11379
11946
|
if (reason === void 0 || reason === null) return;
|
|
11947
|
+
this.sawFinishReason = true;
|
|
11380
11948
|
this.stopReason = mapOpenAIFinish(reason);
|
|
11381
11949
|
}
|
|
11382
11950
|
finish() {
|
|
@@ -11421,18 +11989,6 @@ var OpenAIStreamAccumulator = class {
|
|
|
11421
11989
|
});
|
|
11422
11990
|
}
|
|
11423
11991
|
};
|
|
11424
|
-
function mapOpenAIFinish(reason) {
|
|
11425
|
-
switch (reason) {
|
|
11426
|
-
case "tool_calls":
|
|
11427
|
-
return "tool_use";
|
|
11428
|
-
case "length":
|
|
11429
|
-
return "max_tokens";
|
|
11430
|
-
case "stop":
|
|
11431
|
-
return "end_turn";
|
|
11432
|
-
default:
|
|
11433
|
-
return "end_turn";
|
|
11434
|
-
}
|
|
11435
|
-
}
|
|
11436
11992
|
function applyReasoningRequest(body, effort, providerName) {
|
|
11437
11993
|
if (providerName === "openai") {
|
|
11438
11994
|
body.reasoning_effort = effort;
|
|
@@ -11530,19 +12086,76 @@ function assistantMessage(message) {
|
|
|
11530
12086
|
|
|
11531
12087
|
// src/internal/llm/pool-aware-client.ts
|
|
11532
12088
|
init_errors();
|
|
12089
|
+
|
|
12090
|
+
// src/internal/resilience/circuit-breaker.ts
|
|
12091
|
+
var DEFAULT_MAX_TIMEOUTS = 3;
|
|
12092
|
+
var DEFAULT_COOLDOWN_MS2 = 6e4;
|
|
12093
|
+
var CircuitBreaker = class {
|
|
12094
|
+
constructor(opts = {}) {
|
|
12095
|
+
this.opts = opts;
|
|
12096
|
+
}
|
|
12097
|
+
opts;
|
|
12098
|
+
states = /* @__PURE__ */ new Map();
|
|
12099
|
+
/** @returns true when the breaker is open and the call should be skipped. */
|
|
12100
|
+
shouldSkip(key) {
|
|
12101
|
+
const state2 = this.states.get(key);
|
|
12102
|
+
if (state2 === void 0) return false;
|
|
12103
|
+
if (state2.cooldownUntilMs === 0) return false;
|
|
12104
|
+
if (this.now() < state2.cooldownUntilMs) return true;
|
|
12105
|
+
state2.cooldownUntilMs = 0;
|
|
12106
|
+
state2.consecutiveTimeouts = 0;
|
|
12107
|
+
return false;
|
|
12108
|
+
}
|
|
12109
|
+
recordSuccess(key) {
|
|
12110
|
+
const state2 = this.states.get(key);
|
|
12111
|
+
if (state2 === void 0) return;
|
|
12112
|
+
state2.consecutiveTimeouts = 0;
|
|
12113
|
+
state2.cooldownUntilMs = 0;
|
|
12114
|
+
}
|
|
12115
|
+
recordTimeout(key) {
|
|
12116
|
+
const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
12117
|
+
state2.consecutiveTimeouts += 1;
|
|
12118
|
+
if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
|
|
12119
|
+
state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
|
|
12120
|
+
}
|
|
12121
|
+
this.states.set(key, state2);
|
|
12122
|
+
}
|
|
12123
|
+
/** @internal — tests inspect counter state. */
|
|
12124
|
+
inspect(key) {
|
|
12125
|
+
return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
12126
|
+
}
|
|
12127
|
+
now() {
|
|
12128
|
+
return this.opts.now?.() ?? Date.now();
|
|
12129
|
+
}
|
|
12130
|
+
};
|
|
12131
|
+
|
|
12132
|
+
// src/internal/llm/pool-aware-client.ts
|
|
12133
|
+
init_retry();
|
|
11533
12134
|
var PoolAwareLlmClient = class {
|
|
11534
|
-
constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
|
|
12135
|
+
constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
|
|
11535
12136
|
this.pool = pool;
|
|
11536
12137
|
this.buildClient = buildClient2;
|
|
11537
12138
|
this.waitForAvailableMs = waitForAvailableMs;
|
|
11538
12139
|
this.name = `pool-aware:${pool.provider}`;
|
|
12140
|
+
this.breaker = resilience.breaker ?? new CircuitBreaker();
|
|
12141
|
+
this.backoffBaseMs = resilience.backoffBaseMs;
|
|
12142
|
+
this.rng = resilience.rng;
|
|
11539
12143
|
}
|
|
11540
12144
|
pool;
|
|
11541
12145
|
buildClient;
|
|
11542
12146
|
waitForAvailableMs;
|
|
11543
12147
|
name;
|
|
12148
|
+
/** M2 #60 — provider-level circuit breaker (consecutive-failure). */
|
|
12149
|
+
breaker;
|
|
12150
|
+
backoffBaseMs;
|
|
12151
|
+
rng;
|
|
11544
12152
|
// 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.
|
|
11545
12153
|
async *stream(request, signal) {
|
|
12154
|
+
if (this.breaker.shouldSkip(this.pool.provider)) {
|
|
12155
|
+
throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
|
|
12156
|
+
code: "circuit_open"
|
|
12157
|
+
});
|
|
12158
|
+
}
|
|
11546
12159
|
let hasRetried429 = false;
|
|
11547
12160
|
while (true) {
|
|
11548
12161
|
if (signal.aborted) throw abortError2(signal);
|
|
@@ -11557,6 +12170,7 @@ var PoolAwareLlmClient = class {
|
|
|
11557
12170
|
}
|
|
11558
12171
|
}
|
|
11559
12172
|
if (entry === null) {
|
|
12173
|
+
this.breaker.recordTimeout(this.pool.provider);
|
|
11560
12174
|
throw new CredentialPoolExhaustedError(
|
|
11561
12175
|
`All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
|
|
11562
12176
|
{ provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
|
|
@@ -11566,10 +12180,19 @@ var PoolAwareLlmClient = class {
|
|
|
11566
12180
|
const realClient = this.buildClient(entry.accessToken);
|
|
11567
12181
|
const attempt = await tryFirstEvent(realClient, request, signal);
|
|
11568
12182
|
if (attempt.kind === "ok") {
|
|
12183
|
+
this.breaker.recordSuccess(this.pool.provider);
|
|
11569
12184
|
return yield* relayStream(attempt.generator, attempt.firstResult);
|
|
11570
12185
|
}
|
|
11571
12186
|
const decision = classifyAndDecide(attempt.error, hasRetried429);
|
|
11572
12187
|
if (decision === "retry") {
|
|
12188
|
+
await sleepWithAbort(
|
|
12189
|
+
computeBackoffMs({
|
|
12190
|
+
attempt: 0,
|
|
12191
|
+
...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
|
|
12192
|
+
...this.rng !== void 0 ? { rng: this.rng } : {}
|
|
12193
|
+
}),
|
|
12194
|
+
signal
|
|
12195
|
+
);
|
|
11573
12196
|
hasRetried429 = true;
|
|
11574
12197
|
continue;
|
|
11575
12198
|
}
|
|
@@ -11589,6 +12212,7 @@ var PoolAwareLlmClient = class {
|
|
|
11589
12212
|
hasRetried429 = false;
|
|
11590
12213
|
continue;
|
|
11591
12214
|
}
|
|
12215
|
+
this.breaker.recordTimeout(this.pool.provider);
|
|
11592
12216
|
throw attempt.error;
|
|
11593
12217
|
}
|
|
11594
12218
|
}
|
|
@@ -12023,9 +12647,28 @@ function selectTransport(profile, apiKey) {
|
|
|
12023
12647
|
|
|
12024
12648
|
// src/internal/mcp/client.ts
|
|
12025
12649
|
init_errors();
|
|
12026
|
-
function createMcpClient(name, config) {
|
|
12650
|
+
function createMcpClient(name, config, fetchImpl = fetch) {
|
|
12027
12651
|
if (isStdio(config)) return new StdioMcpClient(name, config);
|
|
12028
|
-
return new HttpMcpClient(name, config);
|
|
12652
|
+
return new HttpMcpClient(name, config, fetchImpl);
|
|
12653
|
+
}
|
|
12654
|
+
var DEFAULT_MCP_TIMEOUT_MS = 3e4;
|
|
12655
|
+
var MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
12656
|
+
var RECONNECT_BASE_MS = 250;
|
|
12657
|
+
var MAX_RECONNECT_ATTEMPTS = 2;
|
|
12658
|
+
function reconnectDelay(attempt) {
|
|
12659
|
+
const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
|
|
12660
|
+
const ms = Math.floor(Math.random() * ceiling);
|
|
12661
|
+
return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
12662
|
+
}
|
|
12663
|
+
function mcpTimeoutError(name, timeoutMs) {
|
|
12664
|
+
return new NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
|
|
12665
|
+
code: "mcp_timeout"
|
|
12666
|
+
});
|
|
12667
|
+
}
|
|
12668
|
+
function isAbortLike(cause) {
|
|
12669
|
+
if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
|
|
12670
|
+
const name = cause.name;
|
|
12671
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
12029
12672
|
}
|
|
12030
12673
|
async function rpcInitialize(request) {
|
|
12031
12674
|
await request("initialize", {
|
|
@@ -12071,32 +12714,107 @@ var StdioMcpClient = class extends BaseMcpClient {
|
|
|
12071
12714
|
name;
|
|
12072
12715
|
child;
|
|
12073
12716
|
nextId = 1;
|
|
12717
|
+
// #59 — pending requests carry a reject + timer so a silent server times out
|
|
12718
|
+
// (typed error), a late reply after timeout is a no-op, and close() settles them.
|
|
12074
12719
|
pending = /* @__PURE__ */ new Map();
|
|
12075
12720
|
buffer = "";
|
|
12076
|
-
|
|
12721
|
+
// M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
|
|
12722
|
+
// unexpectedly OR times out (not via close()); the next request re-spawns with
|
|
12723
|
+
// backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
|
|
12724
|
+
// concurrent request so parallel tool dispatch after a drop awaits one handshake
|
|
12725
|
+
// instead of racing (or spuriously failing with mcp_not_init).
|
|
12726
|
+
dropped = false;
|
|
12727
|
+
reconnectAttempts = 0;
|
|
12728
|
+
reconnectPromise;
|
|
12729
|
+
get timeoutMs() {
|
|
12730
|
+
return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
|
|
12731
|
+
}
|
|
12732
|
+
/** Spawn the server child and wire stdout/stderr/error/exit handlers.
|
|
12733
|
+
* Shared by `initialize()` and the M2 #59 reconnect path. */
|
|
12734
|
+
spawnChild() {
|
|
12077
12735
|
const resolvedCwd = resolveMcpCwd(this.config.cwd);
|
|
12078
12736
|
const child = spawn(this.config.command, this.config.args ?? [], {
|
|
12079
12737
|
cwd: resolvedCwd,
|
|
12080
|
-
|
|
12738
|
+
// #54 (F-H1) — a third-party MCP server binary must not inherit host
|
|
12739
|
+
// secrets. Scrub secret-like vars by default; `config.env` still wins.
|
|
12740
|
+
env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
|
|
12081
12741
|
});
|
|
12082
12742
|
this.child = child;
|
|
12083
12743
|
child.stdout.on("data", (chunk) => this.consume(chunk));
|
|
12084
12744
|
child.stderr.on("data", () => void 0);
|
|
12745
|
+
child.stdin.on("error", () => void 0);
|
|
12085
12746
|
child.on("error", () => {
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12747
|
+
this.rejectAllPending(
|
|
12748
|
+
new NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
|
|
12749
|
+
);
|
|
12750
|
+
});
|
|
12751
|
+
child.on("exit", () => {
|
|
12752
|
+
if (this.child !== child) return;
|
|
12753
|
+
this.child = void 0;
|
|
12754
|
+
this.dropped = true;
|
|
12755
|
+
this.rejectAllPending(
|
|
12756
|
+
new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
|
|
12757
|
+
);
|
|
12090
12758
|
});
|
|
12759
|
+
}
|
|
12760
|
+
async initialize() {
|
|
12761
|
+
this.spawnChild();
|
|
12091
12762
|
await super.initialize();
|
|
12092
12763
|
}
|
|
12764
|
+
/** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
|
|
12765
|
+
* full-jitter backoff) when the client was dropped; fail fast when never
|
|
12766
|
+
* initialized. Concurrent callers share ONE reconnect handshake. */
|
|
12767
|
+
ensureConnected() {
|
|
12768
|
+
if (this.child !== void 0) return Promise.resolve();
|
|
12769
|
+
if (!this.dropped) {
|
|
12770
|
+
return Promise.reject(
|
|
12771
|
+
new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
|
|
12772
|
+
);
|
|
12773
|
+
}
|
|
12774
|
+
this.reconnectPromise ??= this.reconnect().finally(() => {
|
|
12775
|
+
this.reconnectPromise = void 0;
|
|
12776
|
+
});
|
|
12777
|
+
return this.reconnectPromise;
|
|
12778
|
+
}
|
|
12779
|
+
async reconnect() {
|
|
12780
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
12781
|
+
throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
|
|
12782
|
+
}
|
|
12783
|
+
await reconnectDelay(this.reconnectAttempts);
|
|
12784
|
+
this.reconnectAttempts += 1;
|
|
12785
|
+
this.spawnChild();
|
|
12786
|
+
await super.initialize();
|
|
12787
|
+
this.dropped = false;
|
|
12788
|
+
this.reconnectAttempts = 0;
|
|
12789
|
+
}
|
|
12093
12790
|
async close() {
|
|
12094
|
-
|
|
12095
|
-
this.child
|
|
12791
|
+
this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
|
|
12792
|
+
const child = this.child;
|
|
12096
12793
|
this.child = void 0;
|
|
12794
|
+
this.dropped = false;
|
|
12795
|
+
child?.kill("SIGTERM");
|
|
12796
|
+
}
|
|
12797
|
+
/** Reject + clear every pending request (crash / close). @internal */
|
|
12798
|
+
rejectAllPending(error) {
|
|
12799
|
+
for (const entry of this.pending.values()) {
|
|
12800
|
+
clearTimeout(entry.timer);
|
|
12801
|
+
entry.reject(error);
|
|
12802
|
+
}
|
|
12803
|
+
this.pending.clear();
|
|
12097
12804
|
}
|
|
12098
12805
|
consume(chunk) {
|
|
12099
12806
|
this.buffer += chunk.toString("utf8");
|
|
12807
|
+
if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
|
|
12808
|
+
this.buffer = "";
|
|
12809
|
+
this.rejectAllPending(
|
|
12810
|
+
new NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
|
|
12811
|
+
code: "mcp_buffer_overflow"
|
|
12812
|
+
})
|
|
12813
|
+
);
|
|
12814
|
+
this.child?.kill("SIGKILL");
|
|
12815
|
+
this.child = void 0;
|
|
12816
|
+
return;
|
|
12817
|
+
}
|
|
12100
12818
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
12101
12819
|
while (newlineIndex !== -1) {
|
|
12102
12820
|
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
@@ -12113,23 +12831,47 @@ var StdioMcpClient = class extends BaseMcpClient {
|
|
|
12113
12831
|
return;
|
|
12114
12832
|
}
|
|
12115
12833
|
if (typeof message.id !== "number") return;
|
|
12116
|
-
const
|
|
12117
|
-
if (
|
|
12834
|
+
const entry = this.pending.get(message.id);
|
|
12835
|
+
if (entry === void 0) return;
|
|
12118
12836
|
this.pending.delete(message.id);
|
|
12119
|
-
|
|
12837
|
+
clearTimeout(entry.timer);
|
|
12838
|
+
entry.resolve(message);
|
|
12120
12839
|
}
|
|
12121
12840
|
request(method, params) {
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12841
|
+
const child = this.child;
|
|
12842
|
+
if (child !== void 0) return this.send(child, method, params);
|
|
12843
|
+
if (this.dropped) return this.reconnectAndRequest(method, params);
|
|
12844
|
+
return Promise.reject(
|
|
12845
|
+
new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
|
|
12846
|
+
);
|
|
12847
|
+
}
|
|
12848
|
+
/** M2 #59 — reconnect a dropped client, then send. Separate async path so the
|
|
12849
|
+
* happy path above never pays an extra microtask tick. */
|
|
12850
|
+
async reconnectAndRequest(method, params) {
|
|
12851
|
+
await this.ensureConnected();
|
|
12852
|
+
const child = this.child;
|
|
12853
|
+
if (child === void 0) {
|
|
12854
|
+
throw new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
|
|
12126
12855
|
}
|
|
12856
|
+
return this.send(child, method, params);
|
|
12857
|
+
}
|
|
12858
|
+
send(child, method, params) {
|
|
12127
12859
|
const id = this.nextId++;
|
|
12128
12860
|
const payload = { jsonrpc: "2.0", id, method, params };
|
|
12129
|
-
|
|
12861
|
+
child.stdin.write(`${JSON.stringify(payload)}
|
|
12130
12862
|
`);
|
|
12131
|
-
return new Promise((resolve3) => {
|
|
12132
|
-
|
|
12863
|
+
return new Promise((resolve3, reject) => {
|
|
12864
|
+
const timer = setTimeout(() => {
|
|
12865
|
+
this.pending.delete(id);
|
|
12866
|
+
reject(mcpTimeoutError(this.name, this.timeoutMs));
|
|
12867
|
+
this.child?.kill("SIGKILL");
|
|
12868
|
+
this.child = void 0;
|
|
12869
|
+
this.dropped = true;
|
|
12870
|
+
this.rejectAllPending(
|
|
12871
|
+
new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
|
|
12872
|
+
);
|
|
12873
|
+
}, this.timeoutMs);
|
|
12874
|
+
this.pending.set(id, { resolve: resolve3, reject, timer });
|
|
12133
12875
|
});
|
|
12134
12876
|
}
|
|
12135
12877
|
};
|
|
@@ -12155,11 +12897,20 @@ var HttpMcpClient = class extends BaseMcpClient {
|
|
|
12155
12897
|
accept: "application/json",
|
|
12156
12898
|
...this.config.headers ?? {}
|
|
12157
12899
|
};
|
|
12158
|
-
const
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12900
|
+
const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
|
|
12901
|
+
let response;
|
|
12902
|
+
try {
|
|
12903
|
+
response = await this.fetchImpl(this.config.url, {
|
|
12904
|
+
method: "POST",
|
|
12905
|
+
headers,
|
|
12906
|
+
body: JSON.stringify(payload),
|
|
12907
|
+
// #59 — bound the request; a non-responding endpoint aborts here.
|
|
12908
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
12909
|
+
});
|
|
12910
|
+
} catch (cause) {
|
|
12911
|
+
if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
|
|
12912
|
+
throw cause;
|
|
12913
|
+
}
|
|
12163
12914
|
if (!response.ok) {
|
|
12164
12915
|
throw new NetworkError(`MCP ${this.name} returned ${response.status}`, {
|
|
12165
12916
|
code: "mcp_http_error"
|
|
@@ -12274,11 +13025,33 @@ function resolveRunProvider(options) {
|
|
|
12274
13025
|
);
|
|
12275
13026
|
}
|
|
12276
13027
|
const parsedModel = parseModelId(options.model?.id);
|
|
12277
|
-
const
|
|
12278
|
-
const
|
|
12279
|
-
const
|
|
13028
|
+
const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
|
|
13029
|
+
const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
|
|
13030
|
+
const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
|
|
13031
|
+
const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
|
|
12280
13032
|
return { primary, effectiveModelId };
|
|
12281
13033
|
}
|
|
13034
|
+
function inferProviderFromApiKey(apiKey) {
|
|
13035
|
+
if (apiKey === void 0 || apiKey.length === 0) return void 0;
|
|
13036
|
+
const byPrefix = [
|
|
13037
|
+
{ provider: "openrouter", prefix: "sk-or-" },
|
|
13038
|
+
{ provider: "anthropic", prefix: "sk-ant-" },
|
|
13039
|
+
{ provider: "openai", prefix: "sk-" }
|
|
13040
|
+
];
|
|
13041
|
+
for (const { provider, prefix } of byPrefix) {
|
|
13042
|
+
if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
|
|
13043
|
+
return provider;
|
|
13044
|
+
}
|
|
13045
|
+
}
|
|
13046
|
+
return void 0;
|
|
13047
|
+
}
|
|
13048
|
+
function mergeExplicitApiKey(pools, primary, apiKey) {
|
|
13049
|
+
if (apiKey === void 0 || apiKey.length === 0) return pools;
|
|
13050
|
+
if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
|
|
13051
|
+
const existing = pools?.[primary];
|
|
13052
|
+
if (existing !== void 0 && existing.length > 0) return pools;
|
|
13053
|
+
return { ...pools ?? {}, [primary]: [apiKey] };
|
|
13054
|
+
}
|
|
12282
13055
|
function buildLoopInputs(options, runId, userText) {
|
|
12283
13056
|
const maxIterations = options.sendOptions.maxIterations;
|
|
12284
13057
|
if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
|
|
@@ -12289,7 +13062,11 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
12289
13062
|
}
|
|
12290
13063
|
const { primary, effectiveModelId } = resolveRunProvider(options);
|
|
12291
13064
|
const fallback = options.agentOptions.providers?.fallback;
|
|
12292
|
-
const apiKeys =
|
|
13065
|
+
const apiKeys = mergeExplicitApiKey(
|
|
13066
|
+
options.agentOptions.providers?.apiKeys,
|
|
13067
|
+
primary,
|
|
13068
|
+
options.agentOptions.apiKey
|
|
13069
|
+
);
|
|
12293
13070
|
const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
|
|
12294
13071
|
const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
|
|
12295
13072
|
const chain = resolveProviderChain({
|
|
@@ -12333,6 +13110,10 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
12333
13110
|
// D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
|
|
12334
13111
|
// can attach it to the LLM `fetch({ signal })` call.
|
|
12335
13112
|
...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
|
|
13113
|
+
// #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
|
|
13114
|
+
// can enable them via SendOptions (not only internal AgentLoopInputs).
|
|
13115
|
+
...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
|
|
13116
|
+
...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
|
|
12336
13117
|
// M1-2: per-send iteration ceiling (validated above). The loop reads
|
|
12337
13118
|
// inputs.maxIterations (default 8 when unset).
|
|
12338
13119
|
...maxIterations !== void 0 ? { maxIterations } : {},
|
|
@@ -12649,7 +13430,12 @@ async function runActiveMemory(args) {
|
|
|
12649
13430
|
hits: []
|
|
12650
13431
|
});
|
|
12651
13432
|
}
|
|
12652
|
-
const
|
|
13433
|
+
const tenantCtx = {
|
|
13434
|
+
namespace: args.namespace,
|
|
13435
|
+
userId: args.userId,
|
|
13436
|
+
scope: args.scope
|
|
13437
|
+
};
|
|
13438
|
+
const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
|
|
12653
13439
|
if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
|
|
12654
13440
|
const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
|
|
12655
13441
|
if (query.trim().length === 0) {
|
|
@@ -12734,7 +13520,12 @@ function notifyBreaker(breaker, key, status) {
|
|
|
12734
13520
|
else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key);
|
|
12735
13521
|
}
|
|
12736
13522
|
async function finalize(args, queryMode, result) {
|
|
12737
|
-
|
|
13523
|
+
const tenantCtx = {
|
|
13524
|
+
namespace: args.namespace,
|
|
13525
|
+
userId: args.userId,
|
|
13526
|
+
scope: args.scope
|
|
13527
|
+
};
|
|
13528
|
+
args.cache?.set(args.userText, queryMode, result, tenantCtx);
|
|
12738
13529
|
if (args.persistTranscripts === true && args.cwd !== void 0) {
|
|
12739
13530
|
await persistActiveMemoryTranscript(args.cwd, {
|
|
12740
13531
|
runId: args.runId ?? `run-${Date.now()}`,
|
|
@@ -13336,48 +14127,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
|
|
|
13336
14127
|
gemini: geminiMemoryEmbeddingProviderAdapter
|
|
13337
14128
|
};
|
|
13338
14129
|
|
|
13339
|
-
// src/internal/memory/circuit-breaker.ts
|
|
13340
|
-
var DEFAULT_MAX_TIMEOUTS = 3;
|
|
13341
|
-
var DEFAULT_COOLDOWN_MS2 = 6e4;
|
|
13342
|
-
var CircuitBreaker = class {
|
|
13343
|
-
constructor(opts = {}) {
|
|
13344
|
-
this.opts = opts;
|
|
13345
|
-
}
|
|
13346
|
-
opts;
|
|
13347
|
-
states = /* @__PURE__ */ new Map();
|
|
13348
|
-
/** @returns true when the breaker is open and the call should be skipped. */
|
|
13349
|
-
shouldSkip(key) {
|
|
13350
|
-
const state2 = this.states.get(key);
|
|
13351
|
-
if (state2 === void 0) return false;
|
|
13352
|
-
if (state2.cooldownUntilMs === 0) return false;
|
|
13353
|
-
if (this.now() < state2.cooldownUntilMs) return true;
|
|
13354
|
-
state2.cooldownUntilMs = 0;
|
|
13355
|
-
state2.consecutiveTimeouts = 0;
|
|
13356
|
-
return false;
|
|
13357
|
-
}
|
|
13358
|
-
recordSuccess(key) {
|
|
13359
|
-
const state2 = this.states.get(key);
|
|
13360
|
-
if (state2 === void 0) return;
|
|
13361
|
-
state2.consecutiveTimeouts = 0;
|
|
13362
|
-
state2.cooldownUntilMs = 0;
|
|
13363
|
-
}
|
|
13364
|
-
recordTimeout(key) {
|
|
13365
|
-
const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
13366
|
-
state2.consecutiveTimeouts += 1;
|
|
13367
|
-
if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
|
|
13368
|
-
state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
|
|
13369
|
-
}
|
|
13370
|
-
this.states.set(key, state2);
|
|
13371
|
-
}
|
|
13372
|
-
/** @internal — tests inspect counter state. */
|
|
13373
|
-
inspect(key) {
|
|
13374
|
-
return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
13375
|
-
}
|
|
13376
|
-
now() {
|
|
13377
|
-
return this.opts.now?.() ?? Date.now();
|
|
13378
|
-
}
|
|
13379
|
-
};
|
|
13380
|
-
|
|
13381
14130
|
// src/internal/persistence/fts5-sanitize.ts
|
|
13382
14131
|
var PHRASE_OPEN = "";
|
|
13383
14132
|
var PHRASE_CLOSE = "";
|
|
@@ -16611,7 +17360,9 @@ var SandboxBackend = class {
|
|
|
16611
17360
|
this.config = {
|
|
16612
17361
|
workDir: config.workDir ?? "/tmp",
|
|
16613
17362
|
timeoutMs: config.timeoutMs ?? 3e4,
|
|
16614
|
-
maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024
|
|
17363
|
+
maxOutputBytes: config.maxOutputBytes ?? 5 * 1024 * 1024,
|
|
17364
|
+
// #54 — preserve the env policy so backends can scrub secrets.
|
|
17365
|
+
env: config.env ?? "inherit-scrubbed"
|
|
16615
17366
|
};
|
|
16616
17367
|
}
|
|
16617
17368
|
async readFile(path) {
|
|
@@ -16681,7 +17432,9 @@ var LocalSandbox = class extends SandboxBackend {
|
|
|
16681
17432
|
cwd: this.config.workDir,
|
|
16682
17433
|
timeout,
|
|
16683
17434
|
maxBuffer: max,
|
|
16684
|
-
encoding: "utf-8"
|
|
17435
|
+
encoding: "utf-8",
|
|
17436
|
+
// #54 — scrub secret-like host env vars from the child by default.
|
|
17437
|
+
env: resolveChildEnv({ policy: this.config.env })
|
|
16685
17438
|
},
|
|
16686
17439
|
(error, stdout, stderr) => {
|
|
16687
17440
|
resolve3(this.buildResult(error, stdout ?? "", stderr ?? ""));
|