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