@theokit/sdk 2.15.2 → 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 +65 -0
- package/dist/a2a/index.cjs +875 -198
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +876 -199
- 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 +840 -187
- 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 +840 -187
- 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 +846 -189
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +846 -189
- package/dist/eval.js.map +1 -1
- package/dist/event-bus.d.ts +3 -0
- package/dist/index.cjs +977 -215
- 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 +977 -217
- 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 +4 -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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID, randomBytes } from 'crypto';
|
|
2
2
|
import { existsSync, rmSync, mkdirSync, renameSync, readFileSync, realpathSync, lstatSync, readlinkSync, readdirSync } from 'fs';
|
|
3
3
|
import { join, dirname, relative, resolve, sep, isAbsolute } from 'path';
|
|
4
|
-
import { readFile, stat, rm, readdir, mkdir,
|
|
4
|
+
import { readFile, stat, rm, readdir, mkdir, rename, open, unlink, statfs, access, appendFile } from 'fs/promises';
|
|
5
5
|
import { z, toJSONSchema } from 'zod';
|
|
6
6
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -1198,6 +1198,16 @@ var init_credential_pool_types = __esm({
|
|
|
1198
1198
|
});
|
|
1199
1199
|
|
|
1200
1200
|
// src/internal/llm/retry.ts
|
|
1201
|
+
function computeBackoffMs(opts) {
|
|
1202
|
+
const base = opts.baseMs ?? DEFAULT_BASE_MS;
|
|
1203
|
+
const cap2 = opts.capMs ?? DEFAULT_CAP_MS;
|
|
1204
|
+
const rng = opts.rng ?? Math.random;
|
|
1205
|
+
if (opts.retryAfterMs !== void 0 && opts.retryAfterMs >= 0) {
|
|
1206
|
+
return Math.max(base, Math.min(cap2, opts.retryAfterMs));
|
|
1207
|
+
}
|
|
1208
|
+
const ceiling = Math.min(cap2, base * 2 ** opts.attempt);
|
|
1209
|
+
return Math.floor(rng() * ceiling);
|
|
1210
|
+
}
|
|
1201
1211
|
function sleepWithAbort(ms, signal) {
|
|
1202
1212
|
if (ms <= 0 || signal.aborted) return Promise.resolve();
|
|
1203
1213
|
return new Promise((resolve3) => {
|
|
@@ -1212,8 +1222,11 @@ function sleepWithAbort(ms, signal) {
|
|
|
1212
1222
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1213
1223
|
});
|
|
1214
1224
|
}
|
|
1225
|
+
var DEFAULT_BASE_MS, DEFAULT_CAP_MS;
|
|
1215
1226
|
var init_retry = __esm({
|
|
1216
1227
|
"src/internal/llm/retry.ts"() {
|
|
1228
|
+
DEFAULT_BASE_MS = 500;
|
|
1229
|
+
DEFAULT_CAP_MS = 32e3;
|
|
1217
1230
|
}
|
|
1218
1231
|
});
|
|
1219
1232
|
|
|
@@ -5280,7 +5293,7 @@ function abortRun(name, runId, startedAt, stepResults, signal) {
|
|
|
5280
5293
|
}
|
|
5281
5294
|
async function runStepsLoop(params) {
|
|
5282
5295
|
const { options, steps, ctx, runId, startedAt, signal } = params;
|
|
5283
|
-
const stepResults = [];
|
|
5296
|
+
const stepResults = [...params.initialStepResults ?? []];
|
|
5284
5297
|
let acc = params.input;
|
|
5285
5298
|
for (const step of steps) {
|
|
5286
5299
|
if (signal.aborted) return abortRun(options.name, runId, startedAt, stepResults, signal);
|
|
@@ -5339,7 +5352,8 @@ async function executeWorkflow(options, steps, input, runOpts) {
|
|
|
5339
5352
|
ctx,
|
|
5340
5353
|
runId,
|
|
5341
5354
|
startedAt,
|
|
5342
|
-
signal
|
|
5355
|
+
signal,
|
|
5356
|
+
...runOpts?.initialStepResults !== void 0 ? { initialStepResults: runOpts.initialStepResults } : {}
|
|
5343
5357
|
});
|
|
5344
5358
|
} finally {
|
|
5345
5359
|
runSpan.end();
|
|
@@ -5422,7 +5436,9 @@ async function resumeWorkflow(opts) {
|
|
|
5422
5436
|
await store.delete(opts.runId);
|
|
5423
5437
|
return executeWorkflow(options, remainingSteps, resumeInput, {
|
|
5424
5438
|
signal: opts.signal,
|
|
5425
|
-
runId: opts.runId
|
|
5439
|
+
runId: opts.runId,
|
|
5440
|
+
// M3 #62 — restore prior step outputs so the resumed run is not lossy (internal seam).
|
|
5441
|
+
initialStepResults: snapshot.stepResults
|
|
5426
5442
|
});
|
|
5427
5443
|
}
|
|
5428
5444
|
var init_executor = __esm({
|
|
@@ -7053,13 +7069,15 @@ function buildGitInfo() {
|
|
|
7053
7069
|
init_errors();
|
|
7054
7070
|
|
|
7055
7071
|
// src/internal/llm/sse.ts
|
|
7056
|
-
|
|
7072
|
+
init_errors();
|
|
7073
|
+
var DEFAULT_SSE_IDLE_MS = 6e4;
|
|
7074
|
+
async function* parseSseStream(body, signal, idleTimeoutMs = DEFAULT_SSE_IDLE_MS) {
|
|
7057
7075
|
if (body === null) return;
|
|
7058
7076
|
const reader = body.getReader();
|
|
7059
7077
|
const decoder = new TextDecoder("utf-8");
|
|
7060
7078
|
const state4 = { buffer: "", event: "message", data: "" };
|
|
7061
7079
|
try {
|
|
7062
|
-
for await (const chunk of readChunks(reader, signal)) {
|
|
7080
|
+
for await (const chunk of readChunks(reader, signal, idleTimeoutMs)) {
|
|
7063
7081
|
state4.buffer += decoder.decode(chunk, { stream: true });
|
|
7064
7082
|
for (const record of drainCompleteRecords(state4)) yield record;
|
|
7065
7083
|
}
|
|
@@ -7069,14 +7087,38 @@ async function* parseSseStream(body, signal) {
|
|
|
7069
7087
|
releaseReader(reader);
|
|
7070
7088
|
}
|
|
7071
7089
|
}
|
|
7072
|
-
async function* readChunks(reader, signal) {
|
|
7090
|
+
async function* readChunks(reader, signal, idleTimeoutMs) {
|
|
7073
7091
|
while (true) {
|
|
7074
7092
|
if (signal.aborted) return;
|
|
7075
|
-
const { value, done } = await reader
|
|
7093
|
+
const { value, done } = await readWithIdleTimeout(reader, idleTimeoutMs);
|
|
7076
7094
|
if (done) return;
|
|
7077
7095
|
if (value !== void 0) yield value;
|
|
7078
7096
|
}
|
|
7079
7097
|
}
|
|
7098
|
+
function readWithIdleTimeout(reader, idleTimeoutMs) {
|
|
7099
|
+
if (idleTimeoutMs <= 0) return reader.read();
|
|
7100
|
+
return new Promise(
|
|
7101
|
+
(resolve3, reject) => {
|
|
7102
|
+
const timer = setTimeout(() => {
|
|
7103
|
+
reject(
|
|
7104
|
+
new NetworkError(`SSE stream idle for ${idleTimeoutMs}ms \u2014 upstream stalled`, {
|
|
7105
|
+
code: "stream_idle_timeout"
|
|
7106
|
+
})
|
|
7107
|
+
);
|
|
7108
|
+
}, idleTimeoutMs);
|
|
7109
|
+
reader.read().then(
|
|
7110
|
+
(result) => {
|
|
7111
|
+
clearTimeout(timer);
|
|
7112
|
+
resolve3(result);
|
|
7113
|
+
},
|
|
7114
|
+
(err) => {
|
|
7115
|
+
clearTimeout(timer);
|
|
7116
|
+
reject(err);
|
|
7117
|
+
}
|
|
7118
|
+
);
|
|
7119
|
+
}
|
|
7120
|
+
);
|
|
7121
|
+
}
|
|
7080
7122
|
async function cancelReaderQuietly(reader) {
|
|
7081
7123
|
try {
|
|
7082
7124
|
await reader.cancel();
|
|
@@ -7770,6 +7812,9 @@ var PersonalityStore = class {
|
|
|
7770
7812
|
}
|
|
7771
7813
|
};
|
|
7772
7814
|
|
|
7815
|
+
// src/internal/plugins/manager.ts
|
|
7816
|
+
init_errors();
|
|
7817
|
+
|
|
7773
7818
|
// src/internal/plugins/context.ts
|
|
7774
7819
|
function createPluginContext() {
|
|
7775
7820
|
const registrations = {
|
|
@@ -7831,6 +7876,9 @@ var PluginManager = class {
|
|
|
7831
7876
|
memoryProviders: []
|
|
7832
7877
|
};
|
|
7833
7878
|
#initialized = false;
|
|
7879
|
+
// #68 — registrations of plugins added post-init via `register()`, keyed by
|
|
7880
|
+
// plugin name so a re-register REPLACES (not appends) the prior hooks.
|
|
7881
|
+
#byName = /* @__PURE__ */ new Map();
|
|
7834
7882
|
async initialize(plugins) {
|
|
7835
7883
|
if (this.#initialized) {
|
|
7836
7884
|
throw new Error("PluginManager.initialize called twice \u2014 register only once per process");
|
|
@@ -7848,6 +7896,36 @@ var PluginManager = class {
|
|
|
7848
7896
|
await this.#dispatchPlugin(plugin);
|
|
7849
7897
|
}
|
|
7850
7898
|
}
|
|
7899
|
+
/**
|
|
7900
|
+
* #68 — register a single `general` plugin AFTER `initialize()` has run.
|
|
7901
|
+
*
|
|
7902
|
+
* The bulk `initialize()` is single-shot (one call per process); late
|
|
7903
|
+
* registration is a distinct, named operation used by adapters that install
|
|
7904
|
+
* a plugin per-session/per-request (e.g. the ACP permission veto, which is
|
|
7905
|
+
* installed once the permission mode + connection are known — after the
|
|
7906
|
+
* agent's own plugins were already initialized).
|
|
7907
|
+
*
|
|
7908
|
+
* Idempotent by plugin NAME: re-registering a plugin with the same name
|
|
7909
|
+
* REPLACES its prior hooks/tools instead of appending duplicates (the ACP
|
|
7910
|
+
* permission plugin is re-installed on every prompt).
|
|
7911
|
+
*
|
|
7912
|
+
* Only `general` plugins may be registered late — model-provider / memory
|
|
7913
|
+
* plugins are resolved during the bulk init and cannot be added afterwards.
|
|
7914
|
+
*/
|
|
7915
|
+
async register(plugin) {
|
|
7916
|
+
if (plugin.kind !== "general") {
|
|
7917
|
+
throw new ConfigurationError(
|
|
7918
|
+
`late register supports general plugins only (got "${plugin.kind}" for "${plugin.name}")`,
|
|
7919
|
+
{ code: "plugin_late_register_kind" }
|
|
7920
|
+
);
|
|
7921
|
+
}
|
|
7922
|
+
const prior = this.#byName.get(plugin.name);
|
|
7923
|
+
if (prior !== void 0) this.#unmerge(prior);
|
|
7924
|
+
const { ctx, registrations } = createPluginContext();
|
|
7925
|
+
await plugin.register(ctx);
|
|
7926
|
+
this.#byName.set(plugin.name, registrations);
|
|
7927
|
+
this.#merge(registrations);
|
|
7928
|
+
}
|
|
7851
7929
|
get aggregated() {
|
|
7852
7930
|
return this.#aggregated;
|
|
7853
7931
|
}
|
|
@@ -7928,6 +8006,64 @@ var PluginManager = class {
|
|
|
7928
8006
|
}
|
|
7929
8007
|
}
|
|
7930
8008
|
}
|
|
8009
|
+
// #65 — the previously-dead hooks, now wired. Fire-and-forget hooks run
|
|
8010
|
+
// in order (per-handler errors logged, never thrown); transform hooks fold
|
|
8011
|
+
// over the payload (a handler returning a value replaces it).
|
|
8012
|
+
/** @internal */
|
|
8013
|
+
async #runFireAndForget(name, ctx) {
|
|
8014
|
+
for (const h of this.#aggregated.hooks.get(name) ?? []) {
|
|
8015
|
+
try {
|
|
8016
|
+
await h(ctx);
|
|
8017
|
+
} catch (err) {
|
|
8018
|
+
process.stderr.write(
|
|
8019
|
+
`[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
|
|
8020
|
+
`
|
|
8021
|
+
);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
/** @internal — fold: each handler may return a replacement payload; a throw keeps the prior value. */
|
|
8026
|
+
async #runTransform(name, payload, ctx) {
|
|
8027
|
+
let current = payload;
|
|
8028
|
+
for (const h of this.#aggregated.hooks.get(name) ?? []) {
|
|
8029
|
+
try {
|
|
8030
|
+
const out = await h(current, ctx);
|
|
8031
|
+
if (out !== void 0) current = out;
|
|
8032
|
+
} catch (err) {
|
|
8033
|
+
process.stderr.write(
|
|
8034
|
+
`[theokit-sdk] ${name} hook failed: ${err instanceof Error ? err.message : String(err)}
|
|
8035
|
+
`
|
|
8036
|
+
);
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
return current;
|
|
8040
|
+
}
|
|
8041
|
+
/** #65 — fired after a tool call completes. @internal */
|
|
8042
|
+
runPostToolCallHooks(ctx) {
|
|
8043
|
+
return this.#runFireAndForget("post_tool_call", ctx);
|
|
8044
|
+
}
|
|
8045
|
+
/** #65 — fired before / after each LLM turn. @internal */
|
|
8046
|
+
runPreLlmCallHooks(ctx) {
|
|
8047
|
+
return this.#runFireAndForget("pre_llm_call", ctx);
|
|
8048
|
+
}
|
|
8049
|
+
runPostLlmCallHooks(ctx) {
|
|
8050
|
+
return this.#runFireAndForget("post_llm_call", ctx);
|
|
8051
|
+
}
|
|
8052
|
+
/** #65 — fired at run start / end. @internal */
|
|
8053
|
+
runOnSessionStartHooks(ctx) {
|
|
8054
|
+
return this.#runFireAndForget("on_session_start", ctx);
|
|
8055
|
+
}
|
|
8056
|
+
runOnSessionEndHooks(ctx) {
|
|
8057
|
+
return this.#runFireAndForget("on_session_end", ctx);
|
|
8058
|
+
}
|
|
8059
|
+
/** #65/#57 — transform tool results before they reach the LLM (the #57 seam). @internal */
|
|
8060
|
+
runTransformToolResultHooks(results, ctx) {
|
|
8061
|
+
return this.#runTransform("transform_tool_result", results, ctx);
|
|
8062
|
+
}
|
|
8063
|
+
/** #65 — transform the LLM output text before it is consumed. @internal */
|
|
8064
|
+
runTransformLlmOutputHooks(output, ctx) {
|
|
8065
|
+
return this.#runTransform("transform_llm_output", output, ctx);
|
|
8066
|
+
}
|
|
7931
8067
|
async #dispatchPlugin(plugin) {
|
|
7932
8068
|
if (plugin.kind === "general") {
|
|
7933
8069
|
const { ctx, registrations } = createPluginContext();
|
|
@@ -7955,7 +8091,29 @@ var PluginManager = class {
|
|
|
7955
8091
|
}
|
|
7956
8092
|
this.#aggregated.injected.push(...r.injected);
|
|
7957
8093
|
}
|
|
8094
|
+
/**
|
|
8095
|
+
* #68 — inverse of #merge: remove a prior registration's contributions from
|
|
8096
|
+
* the aggregated view by object identity. Used by `register()` to replace a
|
|
8097
|
+
* same-named plugin's hooks/tools instead of accumulating duplicates.
|
|
8098
|
+
*/
|
|
8099
|
+
#unmerge(r) {
|
|
8100
|
+
removeAll(this.#aggregated.tools, r.tools);
|
|
8101
|
+
removeAll(this.#aggregated.commands, r.commands);
|
|
8102
|
+
removeAll(this.#aggregated.injected, r.injected);
|
|
8103
|
+
for (const [hook, handlers] of r.hooks.entries()) {
|
|
8104
|
+
const existing = this.#aggregated.hooks.get(hook);
|
|
8105
|
+
if (existing === void 0) continue;
|
|
8106
|
+
removeAll(existing, handlers);
|
|
8107
|
+
if (existing.length === 0) this.#aggregated.hooks.delete(hook);
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
7958
8110
|
};
|
|
8111
|
+
function removeAll(arr, toRemove) {
|
|
8112
|
+
for (const item of toRemove) {
|
|
8113
|
+
const idx = arr.indexOf(item);
|
|
8114
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
7959
8117
|
|
|
7960
8118
|
// src/internal/telemetry/span-names.ts
|
|
7961
8119
|
var SPAN_NAMES = {
|
|
@@ -7963,7 +8121,12 @@ var SPAN_NAMES = {
|
|
|
7963
8121
|
AGENT_SEND: "agent.send",
|
|
7964
8122
|
MEMORY_RECALL: "memory.recall"};
|
|
7965
8123
|
var HISTOGRAM_NAMES = {
|
|
7966
|
-
MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms"
|
|
8124
|
+
MEMORY_RECALL_DURATION_MS: "theokit_memory_recall_duration_ms",
|
|
8125
|
+
TOOL_CALL_DURATION_MS: "theokit_tool_call_duration_ms",
|
|
8126
|
+
LLM_CALL_DURATION_MS: "theokit_llm_call_duration_ms",
|
|
8127
|
+
LLM_TOKENS: "theokit_llm_tokens",
|
|
8128
|
+
/** M3 #66 — count of finishes where the provider omitted usage (silent undercount). */
|
|
8129
|
+
LLM_USAGE_MISSING: "theokit_llm_usage_missing"
|
|
7967
8130
|
};
|
|
7968
8131
|
|
|
7969
8132
|
// src/internal/telemetry/tracer.ts
|
|
@@ -8301,7 +8464,25 @@ function createTelemetry(settings) {
|
|
|
8301
8464
|
enabled: true,
|
|
8302
8465
|
includeContent: settings.includeContent === true,
|
|
8303
8466
|
startSpan: startNewSpan,
|
|
8304
|
-
|
|
8467
|
+
// M3 #64 — actually nest the child under its parent instead of discarding it.
|
|
8468
|
+
// The parent's SpanContext is set on a fresh OTel context so the child links
|
|
8469
|
+
// to it (traceId + parentSpanId), reconstructing the causal trace tree. Falls
|
|
8470
|
+
// back to a root span when the parent has no valid span id (telemetry off /
|
|
8471
|
+
// NOOP), preserving the pre-M3 behavior for parentless callers.
|
|
8472
|
+
startChildSpan: (parent, name, attrs) => {
|
|
8473
|
+
const redactedAttrs = attrs === void 0 ? void 0 : redactAttrs(attrs);
|
|
8474
|
+
const opts = redactedAttrs ? { attributes: redactedAttrs } : void 0;
|
|
8475
|
+
const pctx = safe(() => parent?.spanContext(), void 0);
|
|
8476
|
+
const span = safe(() => {
|
|
8477
|
+
if (pctx !== void 0 && pctx.spanId !== "0".repeat(16)) {
|
|
8478
|
+
const childCtx = otel.trace.setSpanContext(otel.context.active(), pctx);
|
|
8479
|
+
return tracer.startSpan(name, opts, childCtx);
|
|
8480
|
+
}
|
|
8481
|
+
return tracer.startSpan(name, opts);
|
|
8482
|
+
}, NOOP_SPAN);
|
|
8483
|
+
if (span !== NOOP_SPAN) openSpans.add(span);
|
|
8484
|
+
return wrapSpan(span, openSpans);
|
|
8485
|
+
},
|
|
8305
8486
|
recordHistogram,
|
|
8306
8487
|
endAll: () => {
|
|
8307
8488
|
for (const span of openSpans) safe(() => span.end(), void 0);
|
|
@@ -8337,12 +8518,62 @@ function redactAttrs(attrs) {
|
|
|
8337
8518
|
}
|
|
8338
8519
|
return out;
|
|
8339
8520
|
}
|
|
8521
|
+
|
|
8522
|
+
// src/internal/runtime/lifecycle/env-policy.ts
|
|
8523
|
+
var SECRET_PATTERNS = [
|
|
8524
|
+
/KEY/i,
|
|
8525
|
+
/SECRET/i,
|
|
8526
|
+
/TOKEN/i,
|
|
8527
|
+
/PASSWORD/i,
|
|
8528
|
+
/PASSWD/i,
|
|
8529
|
+
/PASSPHRASE/i,
|
|
8530
|
+
/[_-]PWD/i,
|
|
8531
|
+
/CREDENTIAL/i,
|
|
8532
|
+
/PRIVATE/i,
|
|
8533
|
+
/_AUTH/i
|
|
8534
|
+
];
|
|
8535
|
+
var CORE_VARS = [
|
|
8536
|
+
"PATH",
|
|
8537
|
+
"HOME",
|
|
8538
|
+
"SHELL",
|
|
8539
|
+
"LANG",
|
|
8540
|
+
"LC_ALL",
|
|
8541
|
+
"LC_CTYPE",
|
|
8542
|
+
"TMPDIR",
|
|
8543
|
+
"TMP",
|
|
8544
|
+
"TEMP",
|
|
8545
|
+
"USER",
|
|
8546
|
+
"LOGNAME"
|
|
8547
|
+
];
|
|
8548
|
+
function isSecretName(name) {
|
|
8549
|
+
return SECRET_PATTERNS.some((re) => re.test(name));
|
|
8550
|
+
}
|
|
8551
|
+
function inheritsUnderPolicy(name, policy) {
|
|
8552
|
+
if (policy === "all") return true;
|
|
8553
|
+
if (policy === "core") return CORE_VARS.includes(name);
|
|
8554
|
+
return !isSecretName(name);
|
|
8555
|
+
}
|
|
8556
|
+
function resolveChildEnv(options = {}) {
|
|
8557
|
+
const parent = options.parent ?? process.env;
|
|
8558
|
+
const policy = options.policy ?? "inherit-scrubbed";
|
|
8559
|
+
const base = {};
|
|
8560
|
+
for (const [name, value] of Object.entries(parent)) {
|
|
8561
|
+
if (value !== void 0 && inheritsUnderPolicy(name, policy)) base[name] = value;
|
|
8562
|
+
}
|
|
8563
|
+
for (const [name, value] of Object.entries(options.overrides ?? {})) {
|
|
8564
|
+
base[name] = value;
|
|
8565
|
+
}
|
|
8566
|
+
return base;
|
|
8567
|
+
}
|
|
8568
|
+
|
|
8569
|
+
// src/internal/runtime/lifecycle/spawn-collect.ts
|
|
8340
8570
|
function spawnAndCollect(options) {
|
|
8341
8571
|
return new Promise((resolve3) => {
|
|
8342
8572
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
8343
8573
|
const spawnOptions = {
|
|
8344
8574
|
cwd: options.cwd,
|
|
8345
|
-
|
|
8575
|
+
// #54 — scrub secret-like parent env by default; `options.env` still wins.
|
|
8576
|
+
env: resolveChildEnv({ policy: options.envPolicy, overrides: options.env })
|
|
8346
8577
|
};
|
|
8347
8578
|
const child = spawn(options.command, options.args ?? [], spawnOptions);
|
|
8348
8579
|
let stdout = "";
|
|
@@ -8524,15 +8755,24 @@ function warnMalformed(agentId, line) {
|
|
|
8524
8755
|
`
|
|
8525
8756
|
);
|
|
8526
8757
|
}
|
|
8758
|
+
function hydrateSessionLine(parsed) {
|
|
8759
|
+
if (typeof parsed.text !== "string" || parsed.role === void 0) return void 0;
|
|
8760
|
+
if (parsed.role === "user" || parsed.role === "assistant") {
|
|
8761
|
+
return { role: parsed.role, text: parsed.text };
|
|
8762
|
+
}
|
|
8763
|
+
if (parsed.role === "tool_call" || parsed.role === "tool_result") {
|
|
8764
|
+
const label = parsed.role === "tool_call" ? "tool call" : "tool result";
|
|
8765
|
+
return { role: "assistant", text: `[${label}] ${parsed.text}` };
|
|
8766
|
+
}
|
|
8767
|
+
return void 0;
|
|
8768
|
+
}
|
|
8527
8769
|
async function readSessionFile(cwd, agentId) {
|
|
8528
8770
|
const lines = await readJsonlLines(cwd, agentId);
|
|
8529
8771
|
const messages = [];
|
|
8530
8772
|
for (const line of lines) {
|
|
8531
8773
|
try {
|
|
8532
|
-
const
|
|
8533
|
-
if (
|
|
8534
|
-
messages.push({ role: parsed.role, text: parsed.text });
|
|
8535
|
-
}
|
|
8774
|
+
const msg = hydrateSessionLine(JSON.parse(line));
|
|
8775
|
+
if (msg !== void 0) messages.push(msg);
|
|
8536
8776
|
} catch {
|
|
8537
8777
|
warnMalformed(agentId, line);
|
|
8538
8778
|
}
|
|
@@ -8559,28 +8799,78 @@ async function readAllPersistedMessages(cwd, agentId) {
|
|
|
8559
8799
|
return messages;
|
|
8560
8800
|
}
|
|
8561
8801
|
async function appendAnyPersistedMessage(cwd, agentId, record) {
|
|
8562
|
-
|
|
8563
|
-
await mkdir(dirname(path), { recursive: true });
|
|
8564
|
-
await appendFile(path, `${redactSecrets(JSON.stringify(record))}
|
|
8565
|
-
`, "utf8");
|
|
8802
|
+
await appendPersistedMessages(cwd, agentId, [record]);
|
|
8566
8803
|
}
|
|
8567
|
-
async function
|
|
8804
|
+
async function appendPersistedMessages(cwd, agentId, records) {
|
|
8805
|
+
if (records.length === 0) return;
|
|
8568
8806
|
const path = sessionFilePath(cwd, agentId);
|
|
8569
|
-
|
|
8807
|
+
const payload = records.map((r) => `${redactSecrets(JSON.stringify(r))}
|
|
8808
|
+
`).join("");
|
|
8809
|
+
const dir = dirname(path);
|
|
8810
|
+
let written = false;
|
|
8811
|
+
const attempt = async () => {
|
|
8812
|
+
await mkdir(dir, { recursive: true });
|
|
8813
|
+
await withFileLock(path, async () => {
|
|
8814
|
+
await appendFile(path, payload, "utf8");
|
|
8815
|
+
written = true;
|
|
8816
|
+
});
|
|
8817
|
+
};
|
|
8570
8818
|
try {
|
|
8571
|
-
|
|
8572
|
-
} catch {
|
|
8573
|
-
|
|
8819
|
+
await attempt();
|
|
8820
|
+
} catch (cause) {
|
|
8821
|
+
if (written || cause.code !== "ENOENT") throw cause;
|
|
8822
|
+
await attempt();
|
|
8574
8823
|
}
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8824
|
+
}
|
|
8825
|
+
async function rewriteLockedSession(path, transform) {
|
|
8826
|
+
await withFileLock(path, async () => {
|
|
8827
|
+
let raw;
|
|
8828
|
+
try {
|
|
8829
|
+
raw = await readFile(path, "utf8");
|
|
8830
|
+
} catch {
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8833
|
+
const lines = raw.split("\n").filter((line) => line.length > 0);
|
|
8834
|
+
const next = transform(lines);
|
|
8835
|
+
if (next === void 0) return;
|
|
8836
|
+
await replaceFileAtomic(path, next);
|
|
8837
|
+
});
|
|
8838
|
+
}
|
|
8839
|
+
async function compactSessionFile(cwd, agentId, maxTurns) {
|
|
8840
|
+
const path = sessionFilePath(cwd, agentId);
|
|
8841
|
+
if (!existsSync(path)) return;
|
|
8842
|
+
await rewriteLockedSession(
|
|
8843
|
+
path,
|
|
8844
|
+
(lines) => lines.length <= maxTurns * 2 ? void 0 : `${lines.slice(-maxTurns).join("\n")}
|
|
8845
|
+
`
|
|
8846
|
+
);
|
|
8847
|
+
}
|
|
8848
|
+
async function truncateSessionTo(cwd, agentId, keepCount) {
|
|
8849
|
+
const path = sessionFilePath(cwd, agentId);
|
|
8850
|
+
if (!existsSync(path)) return 0;
|
|
8851
|
+
let kept = 0;
|
|
8852
|
+
await rewriteLockedSession(path, (lines) => {
|
|
8853
|
+
const keep = Math.max(0, Math.min(keepCount, lines.length));
|
|
8854
|
+
kept = keep;
|
|
8855
|
+
if (keep === lines.length) return void 0;
|
|
8856
|
+
return keep === 0 ? "" : `${lines.slice(0, keep).join("\n")}
|
|
8578
8857
|
`;
|
|
8579
|
-
|
|
8858
|
+
});
|
|
8859
|
+
return kept;
|
|
8580
8860
|
}
|
|
8581
8861
|
|
|
8582
8862
|
// src/internal/persistence/conversation-storage-fs.ts
|
|
8583
8863
|
init_security();
|
|
8864
|
+
|
|
8865
|
+
// src/internal/persistence/pagination.ts
|
|
8866
|
+
function paginate(items, opts) {
|
|
8867
|
+
if (opts === void 0 || opts.offset === void 0 && opts.limit === void 0) return items;
|
|
8868
|
+
const start = Math.max(0, opts.offset ?? 0);
|
|
8869
|
+
const end = opts.limit === void 0 ? items.length : start + Math.max(0, opts.limit);
|
|
8870
|
+
return items.slice(start, end);
|
|
8871
|
+
}
|
|
8872
|
+
|
|
8873
|
+
// src/internal/persistence/conversation-storage-fs.ts
|
|
8584
8874
|
var FileSystemConversationStorage = class {
|
|
8585
8875
|
#root;
|
|
8586
8876
|
constructor(opts = {}) {
|
|
@@ -8590,23 +8880,31 @@ var FileSystemConversationStorage = class {
|
|
|
8590
8880
|
get root() {
|
|
8591
8881
|
return this.#root;
|
|
8592
8882
|
}
|
|
8593
|
-
async getMessages(conversationId) {
|
|
8883
|
+
async getMessages(conversationId, opts) {
|
|
8594
8884
|
const records = await readAllPersistedMessages(this.#root, conversationId);
|
|
8595
|
-
|
|
8885
|
+
const all = records.map(toStoredMessage);
|
|
8886
|
+
return paginate(all, opts);
|
|
8596
8887
|
}
|
|
8597
8888
|
async appendMessage(conversationId, message) {
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8889
|
+
await appendAnyPersistedMessage(this.#root, conversationId, toRecord(message));
|
|
8890
|
+
}
|
|
8891
|
+
async appendMessages(conversationId, messages) {
|
|
8892
|
+
await appendPersistedMessages(this.#root, conversationId, messages.map(toRecord));
|
|
8893
|
+
}
|
|
8894
|
+
async truncateConversation(conversationId, keepCount) {
|
|
8895
|
+
return truncateSessionTo(this.#root, conversationId, keepCount);
|
|
8604
8896
|
}
|
|
8605
8897
|
async deleteConversation(conversationId) {
|
|
8606
8898
|
const safe2 = sanitizeIdentifier(conversationId, { maxLen: 128 });
|
|
8607
8899
|
const dirPath = safePathJoin(this.#root, ".theokit", "agents", safe2);
|
|
8608
8900
|
await rm(dirPath, { recursive: true, force: true });
|
|
8609
8901
|
}
|
|
8902
|
+
async deleteScope(prefix) {
|
|
8903
|
+
const ids = await this.listConversationIds();
|
|
8904
|
+
const matching = ids.filter((id) => id.startsWith(prefix));
|
|
8905
|
+
for (const id of matching) await this.deleteConversation(id);
|
|
8906
|
+
return matching.length;
|
|
8907
|
+
}
|
|
8610
8908
|
async listConversationIds(opts = {}) {
|
|
8611
8909
|
const agentsRoot = safePathJoin(this.#root, ".theokit", "agents");
|
|
8612
8910
|
let entries;
|
|
@@ -8632,6 +8930,9 @@ function toStoredMessage(record) {
|
|
|
8632
8930
|
at: record.at
|
|
8633
8931
|
};
|
|
8634
8932
|
}
|
|
8933
|
+
function toRecord(message) {
|
|
8934
|
+
return { role: message.role, text: message.content, at: message.at ?? Date.now() };
|
|
8935
|
+
}
|
|
8635
8936
|
|
|
8636
8937
|
// src/internal/runtime/session/agent-session.ts
|
|
8637
8938
|
var DEFAULT_MAX_TURNS = 200;
|
|
@@ -8717,12 +9018,19 @@ async function readPersistedForCache(adapter, agentId) {
|
|
|
8717
9018
|
const records = await adapter.getMessages(agentId);
|
|
8718
9019
|
const out = [];
|
|
8719
9020
|
for (const r of records) {
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
}
|
|
9021
|
+
const folded = foldStoredToSession(r);
|
|
9022
|
+
if (folded !== void 0) out.push(folded);
|
|
8723
9023
|
}
|
|
8724
9024
|
return out;
|
|
8725
9025
|
}
|
|
9026
|
+
function foldStoredToSession(r) {
|
|
9027
|
+
if (r.role === "user" || r.role === "assistant") return { role: r.role, text: r.content };
|
|
9028
|
+
if (r.role === "tool_call" || r.role === "tool_result") {
|
|
9029
|
+
const label = r.role === "tool_call" ? "tool call" : "tool result";
|
|
9030
|
+
return { role: "assistant", text: `[${label}] ${r.content}` };
|
|
9031
|
+
}
|
|
9032
|
+
return void 0;
|
|
9033
|
+
}
|
|
8726
9034
|
async function flushSessionWrites() {
|
|
8727
9035
|
while (pendingAppends.size > 0) {
|
|
8728
9036
|
const all = Array.from(pendingAppends.values());
|
|
@@ -11074,11 +11382,32 @@ function reasoningEffortFromParams(params) {
|
|
|
11074
11382
|
const thinking = params?.find((p) => p.id === "thinking");
|
|
11075
11383
|
return thinking !== void 0 && thinking.value.length > 0 ? thinking.value : void 0;
|
|
11076
11384
|
}
|
|
11385
|
+
function emitLlmMetrics(inputs, result, startAt) {
|
|
11386
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_CALL_DURATION_MS, Date.now() - startAt, {
|
|
11387
|
+
provider: inputs.llm.name
|
|
11388
|
+
});
|
|
11389
|
+
if (result.inputTokens === void 0 && result.outputTokens === void 0) {
|
|
11390
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.LLM_USAGE_MISSING, 1, {
|
|
11391
|
+
provider: inputs.llm.name
|
|
11392
|
+
});
|
|
11393
|
+
process.stderr.write(
|
|
11394
|
+
`[theokit-sdk] llm usage missing from ${inputs.llm.name} finish \u2014 budget may undercount
|
|
11395
|
+
`
|
|
11396
|
+
);
|
|
11397
|
+
return;
|
|
11398
|
+
}
|
|
11399
|
+
inputs.telemetry?.recordHistogram(
|
|
11400
|
+
HISTOGRAM_NAMES.LLM_TOKENS,
|
|
11401
|
+
(result.inputTokens ?? 0) + (result.outputTokens ?? 0),
|
|
11402
|
+
{ provider: inputs.llm.name }
|
|
11403
|
+
);
|
|
11404
|
+
}
|
|
11077
11405
|
async function streamLlmTurn(inputs, ctx) {
|
|
11078
|
-
const llmSpan = inputs.telemetry?.
|
|
11406
|
+
const llmSpan = inputs.telemetry?.startChildSpan(ctx.sendSpan, "llm.call", {
|
|
11079
11407
|
"model.id": inputs.model.id ?? "auto",
|
|
11080
11408
|
provider: inputs.llm.name
|
|
11081
11409
|
});
|
|
11410
|
+
const startAt = Date.now();
|
|
11082
11411
|
const signal = inputs.signal ?? new AbortController().signal;
|
|
11083
11412
|
const generator = inputs.llm.stream(
|
|
11084
11413
|
{
|
|
@@ -11121,6 +11450,7 @@ async function streamLlmTurn(inputs, ctx) {
|
|
|
11121
11450
|
inputTokens: result.inputTokens ?? 0,
|
|
11122
11451
|
outputTokens: result.outputTokens ?? 0
|
|
11123
11452
|
});
|
|
11453
|
+
emitLlmMetrics(inputs, result, startAt);
|
|
11124
11454
|
llmSpan?.end();
|
|
11125
11455
|
const stripped = stripThinkBlocks(collected.accumulatedText);
|
|
11126
11456
|
return {
|
|
@@ -11350,21 +11680,21 @@ async function executeTool(inputs, resolved, call) {
|
|
|
11350
11680
|
}
|
|
11351
11681
|
if (resolved.origin === "shell") return runShellTool(inputs, call);
|
|
11352
11682
|
if (resolved.origin === "memory") return runMemoryTool(resolved, call);
|
|
11353
|
-
if (resolved.origin === "custom") return runCustomTool(resolved, call);
|
|
11683
|
+
if (resolved.origin === "custom") return runCustomTool(resolved, call, inputs.signal);
|
|
11354
11684
|
return runMcpTool(inputs, resolved, call);
|
|
11355
11685
|
}
|
|
11356
11686
|
async function runMemoryTool(resolved, call) {
|
|
11357
11687
|
return runHandlerTool("memory", resolved.memoryHandler, call);
|
|
11358
11688
|
}
|
|
11359
|
-
async function runCustomTool(resolved, call) {
|
|
11360
|
-
return runHandlerTool("custom", resolved.customHandler, call);
|
|
11689
|
+
async function runCustomTool(resolved, call, signal) {
|
|
11690
|
+
return runHandlerTool("custom", resolved.customHandler, call, signal);
|
|
11361
11691
|
}
|
|
11362
|
-
async function runHandlerTool(kind, handler, call) {
|
|
11692
|
+
async function runHandlerTool(kind, handler, call, signal) {
|
|
11363
11693
|
if (handler === void 0) {
|
|
11364
11694
|
return { stdout: "", stderr: `${kind} tool ${call.name} has no handler`, exitCode: 127 };
|
|
11365
11695
|
}
|
|
11366
11696
|
try {
|
|
11367
|
-
const stdout = await handler(call.input);
|
|
11697
|
+
const stdout = await handler(call.input, { signal });
|
|
11368
11698
|
return { stdout, stderr: "", exitCode: 0 };
|
|
11369
11699
|
} catch (cause) {
|
|
11370
11700
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -11410,22 +11740,66 @@ ${result.stderr}`.trim();
|
|
|
11410
11740
|
return result.stdout.trim();
|
|
11411
11741
|
}
|
|
11412
11742
|
|
|
11743
|
+
// src/internal/agent-loop/tool-timeout.ts
|
|
11744
|
+
var TOOL_ABORTED_EXIT = 124;
|
|
11745
|
+
function abortedResult(signal) {
|
|
11746
|
+
const reason = signal.reason;
|
|
11747
|
+
const timedOut = reason?.name === "TimeoutError";
|
|
11748
|
+
return {
|
|
11749
|
+
stdout: "",
|
|
11750
|
+
stderr: timedOut ? "tool execution timed out" : "tool execution aborted",
|
|
11751
|
+
exitCode: TOOL_ABORTED_EXIT
|
|
11752
|
+
};
|
|
11753
|
+
}
|
|
11754
|
+
function raceToolExecution(exec, opts) {
|
|
11755
|
+
const { signal, timeoutMs } = opts;
|
|
11756
|
+
if (signal === void 0 && timeoutMs === void 0) return exec;
|
|
11757
|
+
const signals = [];
|
|
11758
|
+
if (signal !== void 0) signals.push(signal);
|
|
11759
|
+
if (timeoutMs !== void 0) signals.push(AbortSignal.timeout(timeoutMs));
|
|
11760
|
+
const merged = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
11761
|
+
if (merged.aborted) return Promise.resolve(abortedResult(merged));
|
|
11762
|
+
return new Promise((resolve3, reject) => {
|
|
11763
|
+
let settled = false;
|
|
11764
|
+
const onAbort = () => {
|
|
11765
|
+
if (settled) return;
|
|
11766
|
+
settled = true;
|
|
11767
|
+
resolve3(abortedResult(merged));
|
|
11768
|
+
};
|
|
11769
|
+
merged.addEventListener("abort", onAbort, { once: true });
|
|
11770
|
+
exec.then(
|
|
11771
|
+
(r) => {
|
|
11772
|
+
if (settled) return;
|
|
11773
|
+
settled = true;
|
|
11774
|
+
merged.removeEventListener("abort", onAbort);
|
|
11775
|
+
resolve3(r);
|
|
11776
|
+
},
|
|
11777
|
+
(e) => {
|
|
11778
|
+
if (settled) return;
|
|
11779
|
+
settled = true;
|
|
11780
|
+
merged.removeEventListener("abort", onAbort);
|
|
11781
|
+
reject(e);
|
|
11782
|
+
}
|
|
11783
|
+
);
|
|
11784
|
+
});
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11413
11787
|
// src/internal/agent-loop/tool-dispatch.ts
|
|
11414
|
-
async function dispatchTools(inputs, tools, toolCalls, events) {
|
|
11788
|
+
async function dispatchTools(inputs, tools, toolCalls, events, parentSpan) {
|
|
11415
11789
|
const maxConcurrent = inputs.maxConcurrentTools ?? 4;
|
|
11416
11790
|
return mapWithConcurrency(
|
|
11417
11791
|
toolCalls,
|
|
11418
11792
|
maxConcurrent,
|
|
11419
|
-
(call) => dispatchSingleCall(inputs, tools, call, events)
|
|
11793
|
+
(call) => dispatchSingleCall(inputs, tools, call, events, parentSpan)
|
|
11420
11794
|
);
|
|
11421
11795
|
}
|
|
11422
|
-
async function dispatchSingleCall(inputs, tools, call, events) {
|
|
11796
|
+
async function dispatchSingleCall(inputs, tools, call, events, parentSpan) {
|
|
11423
11797
|
const { call: workingCall, repairs } = applyRepairAndExtractCall(tools, call);
|
|
11424
11798
|
const callId = generateCallId();
|
|
11425
11799
|
const forkVeto = vetoFromForkWhitelist(inputs, workingCall, callId, events);
|
|
11426
11800
|
if (forkVeto !== void 0) return forkVeto;
|
|
11427
11801
|
const resolved = tools.find((tool) => tool.name === workingCall.name);
|
|
11428
|
-
const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs);
|
|
11802
|
+
const toolSpan = startToolCallSpan(inputs, workingCall, resolved, callId, repairs, parentSpan);
|
|
11429
11803
|
events.push(buildToolUseRunning(inputs, callId, workingCall));
|
|
11430
11804
|
const pluginVeto = await vetoFromPluginPreHook(inputs, workingCall, callId, events);
|
|
11431
11805
|
if (pluginVeto !== void 0) {
|
|
@@ -11442,6 +11816,13 @@ async function dispatchSingleCall(inputs, tools, call, events) {
|
|
|
11442
11816
|
return fileVeto;
|
|
11443
11817
|
}
|
|
11444
11818
|
const result = await runToolWithLifecycle(inputs, resolved, workingCall, callId);
|
|
11819
|
+
await inputs.pluginManager?.runPostToolCallHooks({
|
|
11820
|
+
name: workingCall.name,
|
|
11821
|
+
args: workingCall.input,
|
|
11822
|
+
result: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode },
|
|
11823
|
+
agentId: inputs.agentId,
|
|
11824
|
+
runId: inputs.runId
|
|
11825
|
+
});
|
|
11445
11826
|
return finalizeSpanAndPostHook(inputs, workingCall, callId, result, events, toolSpan);
|
|
11446
11827
|
}
|
|
11447
11828
|
function applyRepairAndExtractCall(tools, call) {
|
|
@@ -11474,8 +11855,8 @@ function vetoFromForkWhitelist(inputs, call, callId, events) {
|
|
|
11474
11855
|
content: `Tool blocked by fork whitelist: ${whitelistDecision.reason}`
|
|
11475
11856
|
};
|
|
11476
11857
|
}
|
|
11477
|
-
function startToolCallSpan(inputs, call, resolved, callId, repairs) {
|
|
11478
|
-
const toolSpan = inputs.telemetry?.
|
|
11858
|
+
function startToolCallSpan(inputs, call, resolved, callId, repairs, parentSpan) {
|
|
11859
|
+
const toolSpan = inputs.telemetry?.startChildSpan(parentSpan, "tool.call", {
|
|
11479
11860
|
"tool.name": call.name,
|
|
11480
11861
|
"tool.origin": resolved?.origin ?? "unknown",
|
|
11481
11862
|
callId
|
|
@@ -11539,8 +11920,14 @@ async function runToolWithLifecycle(inputs, resolved, call, callId) {
|
|
|
11539
11920
|
conversationId: inputs.agentId,
|
|
11540
11921
|
callId
|
|
11541
11922
|
});
|
|
11542
|
-
const result = await executeTool(inputs, resolved, call)
|
|
11923
|
+
const result = await raceToolExecution(executeTool(inputs, resolved, call), {
|
|
11924
|
+
signal: inputs.signal,
|
|
11925
|
+
timeoutMs: inputs.perToolTimeoutMs
|
|
11926
|
+
});
|
|
11543
11927
|
const durationMs = Date.now() - startAt;
|
|
11928
|
+
inputs.telemetry?.recordHistogram(HISTOGRAM_NAMES.TOOL_CALL_DURATION_MS, durationMs, {
|
|
11929
|
+
"tool.name": call.name
|
|
11930
|
+
});
|
|
11544
11931
|
if (result.exitCode !== void 0 && result.exitCode !== 0 && result.exitCode !== null) {
|
|
11545
11932
|
await safeEmitToolHook(inputs.onToolError, {
|
|
11546
11933
|
toolName: call.name,
|
|
@@ -11639,6 +12026,35 @@ function buildToolUseCompleted(inputs, callId, call, result) {
|
|
|
11639
12026
|
};
|
|
11640
12027
|
}
|
|
11641
12028
|
|
|
12029
|
+
// src/internal/agent-loop/tool-result-guard.ts
|
|
12030
|
+
var OPEN = "<untrusted-tool-output>";
|
|
12031
|
+
var CLOSE = "</untrusted-tool-output>";
|
|
12032
|
+
var PII_PATTERNS = [
|
|
12033
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
12034
|
+
// email
|
|
12035
|
+
/\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g
|
|
12036
|
+
// phone
|
|
12037
|
+
];
|
|
12038
|
+
function guardText(content, opts) {
|
|
12039
|
+
let out = content;
|
|
12040
|
+
if (opts.redactPii === true) {
|
|
12041
|
+
for (const re of PII_PATTERNS) out = out.replace(re, "[REDACTED]");
|
|
12042
|
+
}
|
|
12043
|
+
if (opts.delimit === true) {
|
|
12044
|
+
const safe2 = out.split(CLOSE).join("</ untrusted-tool-output>");
|
|
12045
|
+
out = `${OPEN}
|
|
12046
|
+
${safe2}
|
|
12047
|
+
${CLOSE}`;
|
|
12048
|
+
}
|
|
12049
|
+
return out;
|
|
12050
|
+
}
|
|
12051
|
+
function applyToolResultGuard(parts, opts) {
|
|
12052
|
+
if (opts.delimit !== true && opts.redactPii !== true) return parts;
|
|
12053
|
+
return parts.map(
|
|
12054
|
+
(p) => p.type === "tool_result" ? { ...p, content: guardText(p.content, opts) } : p
|
|
12055
|
+
);
|
|
12056
|
+
}
|
|
12057
|
+
|
|
11642
12058
|
// src/internal/budget/pricing-data.json
|
|
11643
12059
|
var pricing_data_default = {
|
|
11644
12060
|
_meta: {
|
|
@@ -11989,6 +12405,11 @@ async function runAgentLoop(inputs) {
|
|
|
11989
12405
|
try {
|
|
11990
12406
|
const ctx = await initLoopContext(inputs);
|
|
11991
12407
|
ctxRef = ctx;
|
|
12408
|
+
ctx.sendSpan = sendSpan;
|
|
12409
|
+
await inputs.pluginManager?.runOnSessionStartHooks({
|
|
12410
|
+
agentId: inputs.agentId,
|
|
12411
|
+
runId: inputs.runId
|
|
12412
|
+
});
|
|
11992
12413
|
const budget = inputs.budget ?? new IterationBudget({ maxIterations: inputs.maxIterations ?? 8 });
|
|
11993
12414
|
let lastTurnDecision;
|
|
11994
12415
|
while (budget.shouldContinue()) {
|
|
@@ -12016,6 +12437,7 @@ async function runAgentLoop(inputs) {
|
|
|
12016
12437
|
}
|
|
12017
12438
|
budget.consume();
|
|
12018
12439
|
inputs.budgetTracker?.nextIteration?.();
|
|
12440
|
+
if (inputs.signal?.aborted === true) break;
|
|
12019
12441
|
}
|
|
12020
12442
|
if (lastTurnDecision === "continue" && budget.shouldContinue() === false) {
|
|
12021
12443
|
ctx.stoppedAtIterationLimit = true;
|
|
@@ -12055,6 +12477,10 @@ async function runAgentLoop(inputs) {
|
|
|
12055
12477
|
...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
|
|
12056
12478
|
};
|
|
12057
12479
|
} finally {
|
|
12480
|
+
await inputs.pluginManager?.runOnSessionEndHooks({
|
|
12481
|
+
agentId: inputs.agentId,
|
|
12482
|
+
runId: inputs.runId
|
|
12483
|
+
});
|
|
12058
12484
|
if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
|
|
12059
12485
|
try {
|
|
12060
12486
|
await inputs.memoryProvider.dispose(ctxRef.memoryProviderHandle);
|
|
@@ -12158,7 +12584,10 @@ async function finishOrReflect(inputs, ctx, llmOutput) {
|
|
|
12158
12584
|
return "done";
|
|
12159
12585
|
}
|
|
12160
12586
|
async function runIteration(inputs, ctx) {
|
|
12587
|
+
const hookCtx = { agentId: inputs.agentId, runId: inputs.runId };
|
|
12588
|
+
await inputs.pluginManager?.runPreLlmCallHooks(hookCtx);
|
|
12161
12589
|
const llmOutput = await streamLlmTurn(inputs, ctx);
|
|
12590
|
+
await inputs.pluginManager?.runPostLlmCallHooks(hookCtx);
|
|
12162
12591
|
accumulateUsage(ctx.usage, llmOutput);
|
|
12163
12592
|
if (inputs.budgetTracker !== void 0) {
|
|
12164
12593
|
const modelId = inputs.model.id ?? "auto";
|
|
@@ -12184,6 +12613,13 @@ async function runIteration(inputs, ctx) {
|
|
|
12184
12613
|
}
|
|
12185
12614
|
return continueOrTerminate(inputs, ctx, llmOutput);
|
|
12186
12615
|
}
|
|
12616
|
+
async function transformLlmOutputText(inputs, text, ctx) {
|
|
12617
|
+
return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformLlmOutputHooks(text, ctx) : text;
|
|
12618
|
+
}
|
|
12619
|
+
async function guardAndTransformToolResults(inputs, raw, ctx) {
|
|
12620
|
+
const guarded = inputs.toolResultGuard !== void 0 ? applyToolResultGuard(raw, inputs.toolResultGuard) : raw;
|
|
12621
|
+
return inputs.pluginManager !== void 0 ? inputs.pluginManager.runTransformToolResultHooks(guarded, ctx) : guarded;
|
|
12622
|
+
}
|
|
12187
12623
|
async function continueOrTerminate(inputs, ctx, llmOutput) {
|
|
12188
12624
|
if (llmOutput.errored) return "error";
|
|
12189
12625
|
if (llmOutput.text.length > 0) {
|
|
@@ -12192,8 +12628,18 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
|
|
|
12192
12628
|
if (llmOutput.stopReason !== "tool_use" || llmOutput.toolCalls.length === 0) {
|
|
12193
12629
|
return finishOrReflect(inputs, ctx, llmOutput);
|
|
12194
12630
|
}
|
|
12195
|
-
|
|
12196
|
-
const
|
|
12631
|
+
const tCtx = { agentId: inputs.agentId, runId: inputs.runId };
|
|
12632
|
+
const outText = await transformLlmOutputText(inputs, llmOutput.text, tCtx);
|
|
12633
|
+
ctx.messages.push(buildAssistantTurn(outText, llmOutput.toolCalls));
|
|
12634
|
+
const rawResults = await dispatchTools(
|
|
12635
|
+
inputs,
|
|
12636
|
+
ctx.tools,
|
|
12637
|
+
llmOutput.toolCalls,
|
|
12638
|
+
ctx.events,
|
|
12639
|
+
ctx.sendSpan
|
|
12640
|
+
// M3 #64 — nest tool.call spans under agent.send
|
|
12641
|
+
);
|
|
12642
|
+
const toolResults = await guardAndTransformToolResults(inputs, rawResults, tCtx);
|
|
12197
12643
|
ctx.messages.push({ role: "user", content: toolResults });
|
|
12198
12644
|
if (inputs.onStep !== void 0) {
|
|
12199
12645
|
const cb = inputs.onStep;
|
|
@@ -12638,6 +13084,56 @@ function mapAnthropicStatusToCode(status, body) {
|
|
|
12638
13084
|
function formatMessage(status, code) {
|
|
12639
13085
|
return `Anthropic API error: ${code} (HTTP ${status})`;
|
|
12640
13086
|
}
|
|
13087
|
+
var cachedJsonrepair;
|
|
13088
|
+
function loadJsonrepair() {
|
|
13089
|
+
if (cachedJsonrepair === void 0) {
|
|
13090
|
+
const req = createRequire(import.meta.url);
|
|
13091
|
+
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
13092
|
+
}
|
|
13093
|
+
return cachedJsonrepair;
|
|
13094
|
+
}
|
|
13095
|
+
function isPlainObject(v) {
|
|
13096
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
13097
|
+
}
|
|
13098
|
+
function toFiniteNumber(raw) {
|
|
13099
|
+
if (raw === "") return void 0;
|
|
13100
|
+
const n = Number(raw);
|
|
13101
|
+
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
13102
|
+
}
|
|
13103
|
+
function tryJson(raw, repair) {
|
|
13104
|
+
const t = raw.trimStart();
|
|
13105
|
+
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
13106
|
+
try {
|
|
13107
|
+
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
13108
|
+
} catch {
|
|
13109
|
+
return void 0;
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
function heuristicCoerce(raw, repairJson) {
|
|
13113
|
+
if (raw === "true") return true;
|
|
13114
|
+
if (raw === "false") return false;
|
|
13115
|
+
if (raw === "null") return null;
|
|
13116
|
+
const n = toFiniteNumber(raw);
|
|
13117
|
+
if (n !== void 0) return n;
|
|
13118
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
13119
|
+
return json === void 0 ? raw : json;
|
|
13120
|
+
}
|
|
13121
|
+
function coerceCandidates(raw, repairJson) {
|
|
13122
|
+
const out = [];
|
|
13123
|
+
if (raw === "true") out.push(true);
|
|
13124
|
+
else if (raw === "false") out.push(false);
|
|
13125
|
+
else if (raw === "null") out.push(null);
|
|
13126
|
+
const n = toFiniteNumber(raw);
|
|
13127
|
+
if (n !== void 0) out.push(n);
|
|
13128
|
+
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
13129
|
+
if (json !== void 0) out.push(json);
|
|
13130
|
+
out.push(raw);
|
|
13131
|
+
return out;
|
|
13132
|
+
}
|
|
13133
|
+
function objectShape(schema) {
|
|
13134
|
+
const shape = schema?.shape;
|
|
13135
|
+
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
13136
|
+
}
|
|
12641
13137
|
|
|
12642
13138
|
// src/internal/llm/finish.ts
|
|
12643
13139
|
function collapseSystemText(system) {
|
|
@@ -12650,9 +13146,21 @@ function parseToolArguments(buffered) {
|
|
|
12650
13146
|
try {
|
|
12651
13147
|
return JSON.parse(buffered);
|
|
12652
13148
|
} catch {
|
|
13149
|
+
const repaired = tryJson(buffered, true);
|
|
13150
|
+
if (isPlainObject(repaired)) return repaired;
|
|
12653
13151
|
return { raw: buffered };
|
|
12654
13152
|
}
|
|
12655
13153
|
}
|
|
13154
|
+
function mapOpenAIFinish(reason) {
|
|
13155
|
+
switch (reason) {
|
|
13156
|
+
case "tool_calls":
|
|
13157
|
+
return "tool_use";
|
|
13158
|
+
case "length":
|
|
13159
|
+
return "max_tokens";
|
|
13160
|
+
default:
|
|
13161
|
+
return "end_turn";
|
|
13162
|
+
}
|
|
13163
|
+
}
|
|
12656
13164
|
function makeLlmFinish(state4) {
|
|
12657
13165
|
const finish = {
|
|
12658
13166
|
stopReason: state4.stopReason,
|
|
@@ -13534,56 +14042,9 @@ function toOllamaTools(tools) {
|
|
|
13534
14042
|
}
|
|
13535
14043
|
}));
|
|
13536
14044
|
}
|
|
13537
|
-
|
|
13538
|
-
|
|
13539
|
-
|
|
13540
|
-
const req = createRequire(import.meta.url);
|
|
13541
|
-
cachedJsonrepair = req("jsonrepair").jsonrepair;
|
|
13542
|
-
}
|
|
13543
|
-
return cachedJsonrepair;
|
|
13544
|
-
}
|
|
13545
|
-
function isPlainObject(v) {
|
|
13546
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
13547
|
-
}
|
|
13548
|
-
function toFiniteNumber(raw) {
|
|
13549
|
-
if (raw === "") return void 0;
|
|
13550
|
-
const n = Number(raw);
|
|
13551
|
-
return Number.isFinite(n) && String(n) === raw ? n : void 0;
|
|
13552
|
-
}
|
|
13553
|
-
function tryJson(raw, repair) {
|
|
13554
|
-
const t = raw.trimStart();
|
|
13555
|
-
if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
|
|
13556
|
-
try {
|
|
13557
|
-
return JSON.parse(repair ? loadJsonrepair()(t) : t);
|
|
13558
|
-
} catch {
|
|
13559
|
-
return void 0;
|
|
13560
|
-
}
|
|
13561
|
-
}
|
|
13562
|
-
function heuristicCoerce(raw, repairJson) {
|
|
13563
|
-
if (raw === "true") return true;
|
|
13564
|
-
if (raw === "false") return false;
|
|
13565
|
-
if (raw === "null") return null;
|
|
13566
|
-
const n = toFiniteNumber(raw);
|
|
13567
|
-
if (n !== void 0) return n;
|
|
13568
|
-
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
13569
|
-
return json === void 0 ? raw : json;
|
|
13570
|
-
}
|
|
13571
|
-
function coerceCandidates(raw, repairJson) {
|
|
13572
|
-
const out = [];
|
|
13573
|
-
if (raw === "true") out.push(true);
|
|
13574
|
-
else if (raw === "false") out.push(false);
|
|
13575
|
-
else if (raw === "null") out.push(null);
|
|
13576
|
-
const n = toFiniteNumber(raw);
|
|
13577
|
-
if (n !== void 0) out.push(n);
|
|
13578
|
-
const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
|
|
13579
|
-
if (json !== void 0) out.push(json);
|
|
13580
|
-
out.push(raw);
|
|
13581
|
-
return out;
|
|
13582
|
-
}
|
|
13583
|
-
function objectShape(schema) {
|
|
13584
|
-
const shape = schema?.shape;
|
|
13585
|
-
return shape !== null && typeof shape === "object" ? shape : void 0;
|
|
13586
|
-
}
|
|
14045
|
+
|
|
14046
|
+
// src/internal/llm/openai.ts
|
|
14047
|
+
init_errors();
|
|
13587
14048
|
|
|
13588
14049
|
// src/sanitize/sanitize-tool-input.ts
|
|
13589
14050
|
function applyTrim(key2, value, ctx) {
|
|
@@ -13828,8 +14289,12 @@ var OpenAIClient = class {
|
|
|
13828
14289
|
// model was actually given. Empty set (no tools) recovers nothing.
|
|
13829
14290
|
new Set(request.tools?.map((tool) => tool.name) ?? [])
|
|
13830
14291
|
);
|
|
14292
|
+
let sawDone = false;
|
|
13831
14293
|
for await (const record of parseSseStream(response.body, signal)) {
|
|
13832
|
-
if (record.data === "[DONE]")
|
|
14294
|
+
if (record.data === "[DONE]") {
|
|
14295
|
+
sawDone = true;
|
|
14296
|
+
break;
|
|
14297
|
+
}
|
|
13833
14298
|
let chunk;
|
|
13834
14299
|
try {
|
|
13835
14300
|
chunk = JSON.parse(record.data);
|
|
@@ -13849,6 +14314,11 @@ var OpenAIClient = class {
|
|
|
13849
14314
|
const events = accumulator.consume(chunk);
|
|
13850
14315
|
for (const event of events) yield event;
|
|
13851
14316
|
}
|
|
14317
|
+
if (!sawDone && !accumulator.finishReasonSeen) {
|
|
14318
|
+
throw new NetworkError("SSE stream truncated (no finish_reason / [DONE])", {
|
|
14319
|
+
code: "stream_truncated"
|
|
14320
|
+
});
|
|
14321
|
+
}
|
|
13852
14322
|
const drainEvent = accumulator.finalizeHeldText();
|
|
13853
14323
|
if (drainEvent !== void 0) yield drainEvent;
|
|
13854
14324
|
return accumulator.finish();
|
|
@@ -13948,8 +14418,15 @@ var OpenAIStreamAccumulator = class {
|
|
|
13948
14418
|
this.toolCalls.set(call.index, existing);
|
|
13949
14419
|
}
|
|
13950
14420
|
}
|
|
14421
|
+
/** M2 #61 — true once any chunk carried a non-null `finish_reason` (else a
|
|
14422
|
+
* stream ending without `[DONE]` is a truncation, not a clean end). */
|
|
14423
|
+
sawFinishReason = false;
|
|
14424
|
+
get finishReasonSeen() {
|
|
14425
|
+
return this.sawFinishReason;
|
|
14426
|
+
}
|
|
13951
14427
|
applyFinishReason(reason) {
|
|
13952
14428
|
if (reason === void 0 || reason === null) return;
|
|
14429
|
+
this.sawFinishReason = true;
|
|
13953
14430
|
this.stopReason = mapOpenAIFinish(reason);
|
|
13954
14431
|
}
|
|
13955
14432
|
finish() {
|
|
@@ -13994,18 +14471,6 @@ var OpenAIStreamAccumulator = class {
|
|
|
13994
14471
|
});
|
|
13995
14472
|
}
|
|
13996
14473
|
};
|
|
13997
|
-
function mapOpenAIFinish(reason) {
|
|
13998
|
-
switch (reason) {
|
|
13999
|
-
case "tool_calls":
|
|
14000
|
-
return "tool_use";
|
|
14001
|
-
case "length":
|
|
14002
|
-
return "max_tokens";
|
|
14003
|
-
case "stop":
|
|
14004
|
-
return "end_turn";
|
|
14005
|
-
default:
|
|
14006
|
-
return "end_turn";
|
|
14007
|
-
}
|
|
14008
|
-
}
|
|
14009
14474
|
function applyReasoningRequest(body, effort, providerName) {
|
|
14010
14475
|
if (providerName === "openai") {
|
|
14011
14476
|
body.reasoning_effort = effort;
|
|
@@ -14103,19 +14568,76 @@ function assistantMessage(message) {
|
|
|
14103
14568
|
|
|
14104
14569
|
// src/internal/llm/pool-aware-client.ts
|
|
14105
14570
|
init_errors();
|
|
14571
|
+
|
|
14572
|
+
// src/internal/resilience/circuit-breaker.ts
|
|
14573
|
+
var DEFAULT_MAX_TIMEOUTS = 3;
|
|
14574
|
+
var DEFAULT_COOLDOWN_MS2 = 6e4;
|
|
14575
|
+
var CircuitBreaker = class {
|
|
14576
|
+
constructor(opts = {}) {
|
|
14577
|
+
this.opts = opts;
|
|
14578
|
+
}
|
|
14579
|
+
opts;
|
|
14580
|
+
states = /* @__PURE__ */ new Map();
|
|
14581
|
+
/** @returns true when the breaker is open and the call should be skipped. */
|
|
14582
|
+
shouldSkip(key2) {
|
|
14583
|
+
const state4 = this.states.get(key2);
|
|
14584
|
+
if (state4 === void 0) return false;
|
|
14585
|
+
if (state4.cooldownUntilMs === 0) return false;
|
|
14586
|
+
if (this.now() < state4.cooldownUntilMs) return true;
|
|
14587
|
+
state4.cooldownUntilMs = 0;
|
|
14588
|
+
state4.consecutiveTimeouts = 0;
|
|
14589
|
+
return false;
|
|
14590
|
+
}
|
|
14591
|
+
recordSuccess(key2) {
|
|
14592
|
+
const state4 = this.states.get(key2);
|
|
14593
|
+
if (state4 === void 0) return;
|
|
14594
|
+
state4.consecutiveTimeouts = 0;
|
|
14595
|
+
state4.cooldownUntilMs = 0;
|
|
14596
|
+
}
|
|
14597
|
+
recordTimeout(key2) {
|
|
14598
|
+
const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
14599
|
+
state4.consecutiveTimeouts += 1;
|
|
14600
|
+
if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
|
|
14601
|
+
state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
|
|
14602
|
+
}
|
|
14603
|
+
this.states.set(key2, state4);
|
|
14604
|
+
}
|
|
14605
|
+
/** @internal — tests inspect counter state. */
|
|
14606
|
+
inspect(key2) {
|
|
14607
|
+
return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
14608
|
+
}
|
|
14609
|
+
now() {
|
|
14610
|
+
return this.opts.now?.() ?? Date.now();
|
|
14611
|
+
}
|
|
14612
|
+
};
|
|
14613
|
+
|
|
14614
|
+
// src/internal/llm/pool-aware-client.ts
|
|
14615
|
+
init_retry();
|
|
14106
14616
|
var PoolAwareLlmClient = class {
|
|
14107
|
-
constructor(pool, buildClient2, waitForAvailableMs = 3e4) {
|
|
14617
|
+
constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
|
|
14108
14618
|
this.pool = pool;
|
|
14109
14619
|
this.buildClient = buildClient2;
|
|
14110
14620
|
this.waitForAvailableMs = waitForAvailableMs;
|
|
14111
14621
|
this.name = `pool-aware:${pool.provider}`;
|
|
14622
|
+
this.breaker = resilience.breaker ?? new CircuitBreaker();
|
|
14623
|
+
this.backoffBaseMs = resilience.backoffBaseMs;
|
|
14624
|
+
this.rng = resilience.rng;
|
|
14112
14625
|
}
|
|
14113
14626
|
pool;
|
|
14114
14627
|
buildClient;
|
|
14115
14628
|
waitForAvailableMs;
|
|
14116
14629
|
name;
|
|
14630
|
+
/** M2 #60 — provider-level circuit breaker (consecutive-failure). */
|
|
14631
|
+
breaker;
|
|
14632
|
+
backoffBaseMs;
|
|
14633
|
+
rng;
|
|
14117
14634
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: stream() must serialize pool-select → build client → first-event probe → classify → retry/rotate/propagate. Extracting helpers fragments the linear narrative; the comments above each branch keep it readable.
|
|
14118
14635
|
async *stream(request, signal) {
|
|
14636
|
+
if (this.breaker.shouldSkip(this.pool.provider)) {
|
|
14637
|
+
throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
|
|
14638
|
+
code: "circuit_open"
|
|
14639
|
+
});
|
|
14640
|
+
}
|
|
14119
14641
|
let hasRetried429 = false;
|
|
14120
14642
|
while (true) {
|
|
14121
14643
|
if (signal.aborted) throw abortError2(signal);
|
|
@@ -14130,6 +14652,7 @@ var PoolAwareLlmClient = class {
|
|
|
14130
14652
|
}
|
|
14131
14653
|
}
|
|
14132
14654
|
if (entry === null) {
|
|
14655
|
+
this.breaker.recordTimeout(this.pool.provider);
|
|
14133
14656
|
throw new CredentialPoolExhaustedError(
|
|
14134
14657
|
`All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
|
|
14135
14658
|
{ provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
|
|
@@ -14139,10 +14662,19 @@ var PoolAwareLlmClient = class {
|
|
|
14139
14662
|
const realClient = this.buildClient(entry.accessToken);
|
|
14140
14663
|
const attempt = await tryFirstEvent(realClient, request, signal);
|
|
14141
14664
|
if (attempt.kind === "ok") {
|
|
14665
|
+
this.breaker.recordSuccess(this.pool.provider);
|
|
14142
14666
|
return yield* relayStream(attempt.generator, attempt.firstResult);
|
|
14143
14667
|
}
|
|
14144
14668
|
const decision = classifyAndDecide(attempt.error, hasRetried429);
|
|
14145
14669
|
if (decision === "retry") {
|
|
14670
|
+
await sleepWithAbort(
|
|
14671
|
+
computeBackoffMs({
|
|
14672
|
+
attempt: 0,
|
|
14673
|
+
...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
|
|
14674
|
+
...this.rng !== void 0 ? { rng: this.rng } : {}
|
|
14675
|
+
}),
|
|
14676
|
+
signal
|
|
14677
|
+
);
|
|
14146
14678
|
hasRetried429 = true;
|
|
14147
14679
|
continue;
|
|
14148
14680
|
}
|
|
@@ -14162,6 +14694,7 @@ var PoolAwareLlmClient = class {
|
|
|
14162
14694
|
hasRetried429 = false;
|
|
14163
14695
|
continue;
|
|
14164
14696
|
}
|
|
14697
|
+
this.breaker.recordTimeout(this.pool.provider);
|
|
14165
14698
|
throw attempt.error;
|
|
14166
14699
|
}
|
|
14167
14700
|
}
|
|
@@ -14597,9 +15130,28 @@ function selectTransport(profile, apiKey) {
|
|
|
14597
15130
|
// src/internal/mcp/client.ts
|
|
14598
15131
|
init_errors();
|
|
14599
15132
|
init_path_guard();
|
|
14600
|
-
function createMcpClient(name, config) {
|
|
15133
|
+
function createMcpClient(name, config, fetchImpl = fetch) {
|
|
14601
15134
|
if (isStdio(config)) return new StdioMcpClient(name, config);
|
|
14602
|
-
return new HttpMcpClient(name, config);
|
|
15135
|
+
return new HttpMcpClient(name, config, fetchImpl);
|
|
15136
|
+
}
|
|
15137
|
+
var DEFAULT_MCP_TIMEOUT_MS = 3e4;
|
|
15138
|
+
var MAX_STDIO_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
15139
|
+
var RECONNECT_BASE_MS = 250;
|
|
15140
|
+
var MAX_RECONNECT_ATTEMPTS = 2;
|
|
15141
|
+
function reconnectDelay(attempt) {
|
|
15142
|
+
const ceiling = RECONNECT_BASE_MS * 2 ** attempt;
|
|
15143
|
+
const ms = Math.floor(Math.random() * ceiling);
|
|
15144
|
+
return ms <= 0 ? Promise.resolve() : new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
15145
|
+
}
|
|
15146
|
+
function mcpTimeoutError(name, timeoutMs) {
|
|
15147
|
+
return new NetworkError(`MCP ${name} request timed out after ${timeoutMs}ms`, {
|
|
15148
|
+
code: "mcp_timeout"
|
|
15149
|
+
});
|
|
15150
|
+
}
|
|
15151
|
+
function isAbortLike(cause) {
|
|
15152
|
+
if (typeof cause !== "object" || cause === null || !("name" in cause)) return false;
|
|
15153
|
+
const name = cause.name;
|
|
15154
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
14603
15155
|
}
|
|
14604
15156
|
async function rpcInitialize(request) {
|
|
14605
15157
|
await request("initialize", {
|
|
@@ -14645,32 +15197,107 @@ var StdioMcpClient = class extends BaseMcpClient {
|
|
|
14645
15197
|
name;
|
|
14646
15198
|
child;
|
|
14647
15199
|
nextId = 1;
|
|
15200
|
+
// #59 — pending requests carry a reject + timer so a silent server times out
|
|
15201
|
+
// (typed error), a late reply after timeout is a no-op, and close() settles them.
|
|
14648
15202
|
pending = /* @__PURE__ */ new Map();
|
|
14649
15203
|
buffer = "";
|
|
14650
|
-
|
|
15204
|
+
// M2 #59 — reconnect-after-drop state. `dropped` is set when the child exits
|
|
15205
|
+
// unexpectedly OR times out (not via close()); the next request re-spawns with
|
|
15206
|
+
// backoff. `reconnectPromise` is a SINGLE in-flight reconnect shared by every
|
|
15207
|
+
// concurrent request so parallel tool dispatch after a drop awaits one handshake
|
|
15208
|
+
// instead of racing (or spuriously failing with mcp_not_init).
|
|
15209
|
+
dropped = false;
|
|
15210
|
+
reconnectAttempts = 0;
|
|
15211
|
+
reconnectPromise;
|
|
15212
|
+
get timeoutMs() {
|
|
15213
|
+
return this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
|
|
15214
|
+
}
|
|
15215
|
+
/** Spawn the server child and wire stdout/stderr/error/exit handlers.
|
|
15216
|
+
* Shared by `initialize()` and the M2 #59 reconnect path. */
|
|
15217
|
+
spawnChild() {
|
|
14651
15218
|
const resolvedCwd = resolveMcpCwd(this.config.cwd);
|
|
14652
15219
|
const child = spawn(this.config.command, this.config.args ?? [], {
|
|
14653
15220
|
cwd: resolvedCwd,
|
|
14654
|
-
|
|
15221
|
+
// #54 (F-H1) — a third-party MCP server binary must not inherit host
|
|
15222
|
+
// secrets. Scrub secret-like vars by default; `config.env` still wins.
|
|
15223
|
+
env: resolveChildEnv({ policy: this.config.envPolicy, overrides: this.config.env })
|
|
14655
15224
|
});
|
|
14656
15225
|
this.child = child;
|
|
14657
15226
|
child.stdout.on("data", (chunk) => this.consume(chunk));
|
|
14658
15227
|
child.stderr.on("data", () => void 0);
|
|
15228
|
+
child.stdin.on("error", () => void 0);
|
|
14659
15229
|
child.on("error", () => {
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
15230
|
+
this.rejectAllPending(
|
|
15231
|
+
new NetworkError(`MCP ${this.name} process crashed`, { code: "mcp_crashed" })
|
|
15232
|
+
);
|
|
15233
|
+
});
|
|
15234
|
+
child.on("exit", () => {
|
|
15235
|
+
if (this.child !== child) return;
|
|
15236
|
+
this.child = void 0;
|
|
15237
|
+
this.dropped = true;
|
|
15238
|
+
this.rejectAllPending(
|
|
15239
|
+
new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
|
|
15240
|
+
);
|
|
14664
15241
|
});
|
|
15242
|
+
}
|
|
15243
|
+
async initialize() {
|
|
15244
|
+
this.spawnChild();
|
|
14665
15245
|
await super.initialize();
|
|
14666
15246
|
}
|
|
15247
|
+
/** M2 #59 — ensure a live child before a request. Reconnect (bounded, with
|
|
15248
|
+
* full-jitter backoff) when the client was dropped; fail fast when never
|
|
15249
|
+
* initialized. Concurrent callers share ONE reconnect handshake. */
|
|
15250
|
+
ensureConnected() {
|
|
15251
|
+
if (this.child !== void 0) return Promise.resolve();
|
|
15252
|
+
if (!this.dropped) {
|
|
15253
|
+
return Promise.reject(
|
|
15254
|
+
new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
|
|
15255
|
+
);
|
|
15256
|
+
}
|
|
15257
|
+
this.reconnectPromise ??= this.reconnect().finally(() => {
|
|
15258
|
+
this.reconnectPromise = void 0;
|
|
15259
|
+
});
|
|
15260
|
+
return this.reconnectPromise;
|
|
15261
|
+
}
|
|
15262
|
+
async reconnect() {
|
|
15263
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
15264
|
+
throw new NetworkError(`MCP ${this.name} reconnect exhausted`, { code: "mcp_disconnected" });
|
|
15265
|
+
}
|
|
15266
|
+
await reconnectDelay(this.reconnectAttempts);
|
|
15267
|
+
this.reconnectAttempts += 1;
|
|
15268
|
+
this.spawnChild();
|
|
15269
|
+
await super.initialize();
|
|
15270
|
+
this.dropped = false;
|
|
15271
|
+
this.reconnectAttempts = 0;
|
|
15272
|
+
}
|
|
14667
15273
|
async close() {
|
|
14668
|
-
|
|
14669
|
-
this.child
|
|
15274
|
+
this.rejectAllPending(new NetworkError(`MCP ${this.name} closed`, { code: "mcp_closed" }));
|
|
15275
|
+
const child = this.child;
|
|
14670
15276
|
this.child = void 0;
|
|
15277
|
+
this.dropped = false;
|
|
15278
|
+
child?.kill("SIGTERM");
|
|
15279
|
+
}
|
|
15280
|
+
/** Reject + clear every pending request (crash / close). @internal */
|
|
15281
|
+
rejectAllPending(error) {
|
|
15282
|
+
for (const entry of this.pending.values()) {
|
|
15283
|
+
clearTimeout(entry.timer);
|
|
15284
|
+
entry.reject(error);
|
|
15285
|
+
}
|
|
15286
|
+
this.pending.clear();
|
|
14671
15287
|
}
|
|
14672
15288
|
consume(chunk) {
|
|
14673
15289
|
this.buffer += chunk.toString("utf8");
|
|
15290
|
+
if (this.buffer.length > MAX_STDIO_BUFFER_BYTES) {
|
|
15291
|
+
this.buffer = "";
|
|
15292
|
+
this.rejectAllPending(
|
|
15293
|
+
new NetworkError(`MCP ${this.name} exceeded stdout buffer limit`, {
|
|
15294
|
+
code: "mcp_buffer_overflow"
|
|
15295
|
+
})
|
|
15296
|
+
);
|
|
15297
|
+
this.child?.kill("SIGKILL");
|
|
15298
|
+
this.child = void 0;
|
|
15299
|
+
return;
|
|
15300
|
+
}
|
|
14674
15301
|
let newlineIndex = this.buffer.indexOf("\n");
|
|
14675
15302
|
while (newlineIndex !== -1) {
|
|
14676
15303
|
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
@@ -14687,23 +15314,47 @@ var StdioMcpClient = class extends BaseMcpClient {
|
|
|
14687
15314
|
return;
|
|
14688
15315
|
}
|
|
14689
15316
|
if (typeof message.id !== "number") return;
|
|
14690
|
-
const
|
|
14691
|
-
if (
|
|
15317
|
+
const entry = this.pending.get(message.id);
|
|
15318
|
+
if (entry === void 0) return;
|
|
14692
15319
|
this.pending.delete(message.id);
|
|
14693
|
-
|
|
15320
|
+
clearTimeout(entry.timer);
|
|
15321
|
+
entry.resolve(message);
|
|
14694
15322
|
}
|
|
14695
15323
|
request(method, params) {
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
15324
|
+
const child = this.child;
|
|
15325
|
+
if (child !== void 0) return this.send(child, method, params);
|
|
15326
|
+
if (this.dropped) return this.reconnectAndRequest(method, params);
|
|
15327
|
+
return Promise.reject(
|
|
15328
|
+
new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" })
|
|
15329
|
+
);
|
|
15330
|
+
}
|
|
15331
|
+
/** M2 #59 — reconnect a dropped client, then send. Separate async path so the
|
|
15332
|
+
* happy path above never pays an extra microtask tick. */
|
|
15333
|
+
async reconnectAndRequest(method, params) {
|
|
15334
|
+
await this.ensureConnected();
|
|
15335
|
+
const child = this.child;
|
|
15336
|
+
if (child === void 0) {
|
|
15337
|
+
throw new ConfigurationError(`MCP ${this.name} is not initialized`, { code: "mcp_not_init" });
|
|
14700
15338
|
}
|
|
15339
|
+
return this.send(child, method, params);
|
|
15340
|
+
}
|
|
15341
|
+
send(child, method, params) {
|
|
14701
15342
|
const id = this.nextId++;
|
|
14702
15343
|
const payload = { jsonrpc: "2.0", id, method, params };
|
|
14703
|
-
|
|
15344
|
+
child.stdin.write(`${JSON.stringify(payload)}
|
|
14704
15345
|
`);
|
|
14705
|
-
return new Promise((resolve3) => {
|
|
14706
|
-
|
|
15346
|
+
return new Promise((resolve3, reject) => {
|
|
15347
|
+
const timer = setTimeout(() => {
|
|
15348
|
+
this.pending.delete(id);
|
|
15349
|
+
reject(mcpTimeoutError(this.name, this.timeoutMs));
|
|
15350
|
+
this.child?.kill("SIGKILL");
|
|
15351
|
+
this.child = void 0;
|
|
15352
|
+
this.dropped = true;
|
|
15353
|
+
this.rejectAllPending(
|
|
15354
|
+
new NetworkError(`MCP ${this.name} disconnected`, { code: "mcp_disconnected" })
|
|
15355
|
+
);
|
|
15356
|
+
}, this.timeoutMs);
|
|
15357
|
+
this.pending.set(id, { resolve: resolve3, reject, timer });
|
|
14707
15358
|
});
|
|
14708
15359
|
}
|
|
14709
15360
|
};
|
|
@@ -14729,11 +15380,20 @@ var HttpMcpClient = class extends BaseMcpClient {
|
|
|
14729
15380
|
accept: "application/json",
|
|
14730
15381
|
...this.config.headers ?? {}
|
|
14731
15382
|
};
|
|
14732
|
-
const
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
15383
|
+
const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_MCP_TIMEOUT_MS;
|
|
15384
|
+
let response;
|
|
15385
|
+
try {
|
|
15386
|
+
response = await this.fetchImpl(this.config.url, {
|
|
15387
|
+
method: "POST",
|
|
15388
|
+
headers,
|
|
15389
|
+
body: JSON.stringify(payload),
|
|
15390
|
+
// #59 — bound the request; a non-responding endpoint aborts here.
|
|
15391
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
15392
|
+
});
|
|
15393
|
+
} catch (cause) {
|
|
15394
|
+
if (isAbortLike(cause)) throw mcpTimeoutError(this.name, timeoutMs);
|
|
15395
|
+
throw cause;
|
|
15396
|
+
}
|
|
14737
15397
|
if (!response.ok) {
|
|
14738
15398
|
throw new NetworkError(`MCP ${this.name} returned ${response.status}`, {
|
|
14739
15399
|
code: "mcp_http_error"
|
|
@@ -14848,11 +15508,33 @@ function resolveRunProvider(options) {
|
|
|
14848
15508
|
);
|
|
14849
15509
|
}
|
|
14850
15510
|
const parsedModel = parseModelId(options.model?.id);
|
|
14851
|
-
const
|
|
14852
|
-
const
|
|
14853
|
-
const
|
|
15511
|
+
const modelInferredProvider = parsedModel.provider !== void 0 && getProviderProfile(parsedModel.provider) !== void 0 ? parsedModel.provider : void 0;
|
|
15512
|
+
const keyInferredProvider = inferProviderFromApiKey(options.agentOptions.apiKey);
|
|
15513
|
+
const primary = options.agentOptions.providers?.routes?.[0]?.provider ?? keyInferredProvider ?? modelInferredProvider ?? detectPrimaryProvider();
|
|
15514
|
+
const effectiveModelId = modelInferredProvider !== void 0 && modelInferredProvider === primary ? parsedModel.name : options.model?.id ?? "claude-sonnet-4-6";
|
|
14854
15515
|
return { primary, effectiveModelId };
|
|
14855
15516
|
}
|
|
15517
|
+
function inferProviderFromApiKey(apiKey) {
|
|
15518
|
+
if (apiKey === void 0 || apiKey.length === 0) return void 0;
|
|
15519
|
+
const byPrefix = [
|
|
15520
|
+
{ provider: "openrouter", prefix: "sk-or-" },
|
|
15521
|
+
{ provider: "anthropic", prefix: "sk-ant-" },
|
|
15522
|
+
{ provider: "openai", prefix: "sk-" }
|
|
15523
|
+
];
|
|
15524
|
+
for (const { provider, prefix } of byPrefix) {
|
|
15525
|
+
if (apiKey.startsWith(prefix) && getProviderProfile(provider) !== void 0) {
|
|
15526
|
+
return provider;
|
|
15527
|
+
}
|
|
15528
|
+
}
|
|
15529
|
+
return void 0;
|
|
15530
|
+
}
|
|
15531
|
+
function mergeExplicitApiKey(pools, primary, apiKey) {
|
|
15532
|
+
if (apiKey === void 0 || apiKey.length === 0) return pools;
|
|
15533
|
+
if (isFixtureApiKey(apiKey) || apiKey === LOCAL_RUNTIME_MOCK_KEY) return pools;
|
|
15534
|
+
const existing = pools?.[primary];
|
|
15535
|
+
if (existing !== void 0 && existing.length > 0) return pools;
|
|
15536
|
+
return { ...pools ?? {}, [primary]: [apiKey] };
|
|
15537
|
+
}
|
|
14856
15538
|
function buildLoopInputs(options, runId, userText) {
|
|
14857
15539
|
const maxIterations = options.sendOptions.maxIterations;
|
|
14858
15540
|
if (maxIterations !== void 0 && (!Number.isInteger(maxIterations) || maxIterations < 1)) {
|
|
@@ -14863,7 +15545,11 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
14863
15545
|
}
|
|
14864
15546
|
const { primary, effectiveModelId } = resolveRunProvider(options);
|
|
14865
15547
|
const fallback = options.agentOptions.providers?.fallback;
|
|
14866
|
-
const apiKeys =
|
|
15548
|
+
const apiKeys = mergeExplicitApiKey(
|
|
15549
|
+
options.agentOptions.providers?.apiKeys,
|
|
15550
|
+
primary,
|
|
15551
|
+
options.agentOptions.apiKey
|
|
15552
|
+
);
|
|
14867
15553
|
const credentialPoolStrategy = options.agentOptions.providers?.credentialPoolStrategy;
|
|
14868
15554
|
const extractToolCallsFromContent = options.agentOptions.providers?.routes?.[0]?.extractToolCallsFromContent;
|
|
14869
15555
|
const chain = resolveProviderChain({
|
|
@@ -14907,6 +15593,10 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
14907
15593
|
// D318 — forward SendOptions.signal to the agent loop so streamLlmTurn
|
|
14908
15594
|
// can attach it to the LLM `fetch({ signal })` call.
|
|
14909
15595
|
...options.sendOptions.signal !== void 0 ? { signal: options.sendOptions.signal } : {},
|
|
15596
|
+
// #58 / #57 — forward the per-tool timeout + tool-result guard so a consumer
|
|
15597
|
+
// can enable them via SendOptions (not only internal AgentLoopInputs).
|
|
15598
|
+
...options.sendOptions.perToolTimeoutMs !== void 0 ? { perToolTimeoutMs: options.sendOptions.perToolTimeoutMs } : {},
|
|
15599
|
+
...options.sendOptions.toolResultGuard !== void 0 ? { toolResultGuard: options.sendOptions.toolResultGuard } : {},
|
|
14910
15600
|
// M1-2: per-send iteration ceiling (validated above). The loop reads
|
|
14911
15601
|
// inputs.maxIterations (default 8 when unset).
|
|
14912
15602
|
...maxIterations !== void 0 ? { maxIterations } : {},
|
|
@@ -15224,7 +15914,12 @@ async function runActiveMemory(args) {
|
|
|
15224
15914
|
hits: []
|
|
15225
15915
|
});
|
|
15226
15916
|
}
|
|
15227
|
-
const
|
|
15917
|
+
const tenantCtx = {
|
|
15918
|
+
namespace: args.namespace,
|
|
15919
|
+
userId: args.userId,
|
|
15920
|
+
scope: args.scope
|
|
15921
|
+
};
|
|
15922
|
+
const cached2 = args.cache?.get(args.userText, cfg.queryMode, tenantCtx);
|
|
15228
15923
|
if (cached2 !== void 0) return endRecallSpan(span, args, cached2);
|
|
15229
15924
|
const query = buildQuery(args.userText, args.priorMessages, cfg.queryMode, cfg.recentUserTurns);
|
|
15230
15925
|
if (query.trim().length === 0) {
|
|
@@ -15309,7 +16004,12 @@ function notifyBreaker(breaker, key2, status) {
|
|
|
15309
16004
|
else if (status === "ok" || status === "no-recall") breaker.recordSuccess(key2);
|
|
15310
16005
|
}
|
|
15311
16006
|
async function finalize(args, queryMode, result) {
|
|
15312
|
-
|
|
16007
|
+
const tenantCtx = {
|
|
16008
|
+
namespace: args.namespace,
|
|
16009
|
+
userId: args.userId,
|
|
16010
|
+
scope: args.scope
|
|
16011
|
+
};
|
|
16012
|
+
args.cache?.set(args.userText, queryMode, result, tenantCtx);
|
|
15313
16013
|
if (args.persistTranscripts === true && args.cwd !== void 0) {
|
|
15314
16014
|
await persistActiveMemoryTranscript(args.cwd, {
|
|
15315
16015
|
runId: args.runId ?? `run-${Date.now()}`,
|
|
@@ -15882,48 +16582,6 @@ var MEMORY_EMBEDDING_ADAPTERS = {
|
|
|
15882
16582
|
gemini: geminiMemoryEmbeddingProviderAdapter
|
|
15883
16583
|
};
|
|
15884
16584
|
|
|
15885
|
-
// src/internal/memory/circuit-breaker.ts
|
|
15886
|
-
var DEFAULT_MAX_TIMEOUTS = 3;
|
|
15887
|
-
var DEFAULT_COOLDOWN_MS2 = 6e4;
|
|
15888
|
-
var CircuitBreaker = class {
|
|
15889
|
-
constructor(opts = {}) {
|
|
15890
|
-
this.opts = opts;
|
|
15891
|
-
}
|
|
15892
|
-
opts;
|
|
15893
|
-
states = /* @__PURE__ */ new Map();
|
|
15894
|
-
/** @returns true when the breaker is open and the call should be skipped. */
|
|
15895
|
-
shouldSkip(key2) {
|
|
15896
|
-
const state4 = this.states.get(key2);
|
|
15897
|
-
if (state4 === void 0) return false;
|
|
15898
|
-
if (state4.cooldownUntilMs === 0) return false;
|
|
15899
|
-
if (this.now() < state4.cooldownUntilMs) return true;
|
|
15900
|
-
state4.cooldownUntilMs = 0;
|
|
15901
|
-
state4.consecutiveTimeouts = 0;
|
|
15902
|
-
return false;
|
|
15903
|
-
}
|
|
15904
|
-
recordSuccess(key2) {
|
|
15905
|
-
const state4 = this.states.get(key2);
|
|
15906
|
-
if (state4 === void 0) return;
|
|
15907
|
-
state4.consecutiveTimeouts = 0;
|
|
15908
|
-
state4.cooldownUntilMs = 0;
|
|
15909
|
-
}
|
|
15910
|
-
recordTimeout(key2) {
|
|
15911
|
-
const state4 = this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
15912
|
-
state4.consecutiveTimeouts += 1;
|
|
15913
|
-
if (state4.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
|
|
15914
|
-
state4.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
|
|
15915
|
-
}
|
|
15916
|
-
this.states.set(key2, state4);
|
|
15917
|
-
}
|
|
15918
|
-
/** @internal — tests inspect counter state. */
|
|
15919
|
-
inspect(key2) {
|
|
15920
|
-
return this.states.get(key2) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
|
|
15921
|
-
}
|
|
15922
|
-
now() {
|
|
15923
|
-
return this.opts.now?.() ?? Date.now();
|
|
15924
|
-
}
|
|
15925
|
-
};
|
|
15926
|
-
|
|
15927
16585
|
// src/internal/runtime/local-agent/local-agent-memory.ts
|
|
15928
16586
|
init_index_manager();
|
|
15929
16587
|
|
|
@@ -18512,13 +19170,13 @@ function defineTool(spec) {
|
|
|
18512
19170
|
name: spec.name,
|
|
18513
19171
|
description: spec.description,
|
|
18514
19172
|
inputSchema,
|
|
18515
|
-
handler: async (input) => {
|
|
19173
|
+
handler: async (input, ctx) => {
|
|
18516
19174
|
const raw = spec.sanitize ? sanitizeToolInput(input, {
|
|
18517
19175
|
...spec.sanitize === true ? {} : spec.sanitize,
|
|
18518
19176
|
schema: spec.inputSchema
|
|
18519
19177
|
}).value : input;
|
|
18520
19178
|
const parsed = spec.inputSchema.parse(raw);
|
|
18521
|
-
return await spec.handler(parsed);
|
|
19179
|
+
return await spec.handler(parsed, ctx);
|
|
18522
19180
|
}
|
|
18523
19181
|
};
|
|
18524
19182
|
}
|
|
@@ -18529,6 +19187,14 @@ init_errors();
|
|
|
18529
19187
|
// src/event-bus.ts
|
|
18530
19188
|
var EventBus = class {
|
|
18531
19189
|
handlers = /* @__PURE__ */ new Map();
|
|
19190
|
+
// M3 #64 — a swallowed handler error used to vanish without a trace (fail-loud
|
|
19191
|
+
// violation). We now log it AND expose an observable count so ops/tests can see
|
|
19192
|
+
// that a subscriber is silently failing, without breaking the EC-2 contract.
|
|
19193
|
+
#handlerErrorCount = 0;
|
|
19194
|
+
/** M3 #64 — number of handler invocations that threw (and were logged). */
|
|
19195
|
+
get handlerErrorCount() {
|
|
19196
|
+
return this.#handlerErrorCount;
|
|
19197
|
+
}
|
|
18532
19198
|
/**
|
|
18533
19199
|
* Subscribe to an event. Returns an unsubscribe function.
|
|
18534
19200
|
*/
|
|
@@ -18551,7 +19217,13 @@ var EventBus = class {
|
|
|
18551
19217
|
for (const handler of set) {
|
|
18552
19218
|
try {
|
|
18553
19219
|
handler(payload);
|
|
18554
|
-
} catch {
|
|
19220
|
+
} catch (cause) {
|
|
19221
|
+
this.#handlerErrorCount += 1;
|
|
19222
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
19223
|
+
process.stderr.write(
|
|
19224
|
+
`[theokit-sdk] event-bus: handler for "${String(event)}" threw: ${message}
|
|
19225
|
+
`
|
|
19226
|
+
);
|
|
18555
19227
|
}
|
|
18556
19228
|
}
|
|
18557
19229
|
}
|
|
@@ -18574,11 +19246,17 @@ init_generate_object();
|
|
|
18574
19246
|
// src/internal/persistence/conversation-storage-memory.ts
|
|
18575
19247
|
var InMemoryConversationStorage = class {
|
|
18576
19248
|
#store = /* @__PURE__ */ new Map();
|
|
18577
|
-
async getMessages(conversationId) {
|
|
19249
|
+
async getMessages(conversationId, opts) {
|
|
18578
19250
|
const existing = this.#store.get(conversationId);
|
|
18579
|
-
return existing === void 0 ? [] : existing.slice();
|
|
19251
|
+
return existing === void 0 ? [] : paginate(existing.slice(), opts);
|
|
18580
19252
|
}
|
|
18581
19253
|
async appendMessage(conversationId, message) {
|
|
19254
|
+
this.#appendOne(conversationId, message);
|
|
19255
|
+
}
|
|
19256
|
+
async appendMessages(conversationId, messages) {
|
|
19257
|
+
for (const message of messages) this.#appendOne(conversationId, message);
|
|
19258
|
+
}
|
|
19259
|
+
#appendOne(conversationId, message) {
|
|
18582
19260
|
const existing = this.#store.get(conversationId);
|
|
18583
19261
|
const stamped = message.at === void 0 ? { ...message, at: Date.now() } : message;
|
|
18584
19262
|
if (existing === void 0) {
|
|
@@ -18587,9 +19265,26 @@ var InMemoryConversationStorage = class {
|
|
|
18587
19265
|
}
|
|
18588
19266
|
existing.push(stamped);
|
|
18589
19267
|
}
|
|
19268
|
+
async truncateConversation(conversationId, keepCount) {
|
|
19269
|
+
const existing = this.#store.get(conversationId);
|
|
19270
|
+
if (existing === void 0) return 0;
|
|
19271
|
+
const keep = Math.max(0, Math.min(keepCount, existing.length));
|
|
19272
|
+
this.#store.set(conversationId, existing.slice(0, keep));
|
|
19273
|
+
return keep;
|
|
19274
|
+
}
|
|
18590
19275
|
async deleteConversation(conversationId) {
|
|
18591
19276
|
this.#store.delete(conversationId);
|
|
18592
19277
|
}
|
|
19278
|
+
async deleteScope(prefix) {
|
|
19279
|
+
let n = 0;
|
|
19280
|
+
for (const id of [...this.#store.keys()]) {
|
|
19281
|
+
if (id.startsWith(prefix)) {
|
|
19282
|
+
this.#store.delete(id);
|
|
19283
|
+
n += 1;
|
|
19284
|
+
}
|
|
19285
|
+
}
|
|
19286
|
+
return n;
|
|
19287
|
+
}
|
|
18593
19288
|
async listConversationIds(opts) {
|
|
18594
19289
|
const all = Array.from(this.#store.keys());
|
|
18595
19290
|
if (opts?.limit !== void 0) return all.slice(0, opts.limit);
|
|
@@ -18767,23 +19462,40 @@ function createNoopMemoryProvider() {
|
|
|
18767
19462
|
}
|
|
18768
19463
|
var JobQueue = class {
|
|
18769
19464
|
jobs = /* @__PURE__ */ new Map();
|
|
19465
|
+
controllers = /* @__PURE__ */ new Map();
|
|
19466
|
+
maxConcurrency;
|
|
19467
|
+
running = 0;
|
|
19468
|
+
waiting = [];
|
|
19469
|
+
constructor(options = {}) {
|
|
19470
|
+
this.maxConcurrency = options.maxConcurrency === void 0 ? Number.POSITIVE_INFINITY : Math.max(1, options.maxConcurrency);
|
|
19471
|
+
}
|
|
18770
19472
|
/**
|
|
18771
|
-
* Enqueue a background function. Returns the job ID immediately.
|
|
18772
|
-
*
|
|
19473
|
+
* Enqueue a background function. Returns the job ID immediately. The function
|
|
19474
|
+
* receives an `AbortSignal` that fires when the job is cancelled (#58) — a
|
|
19475
|
+
* cooperative job should observe it to stop early. Existing `() => Promise<T>`
|
|
19476
|
+
* callers are unaffected (the signal argument is simply ignored).
|
|
18773
19477
|
*/
|
|
18774
19478
|
enqueue(fn) {
|
|
18775
19479
|
const id = randomUUID();
|
|
18776
19480
|
const job = { id, status: "pending" };
|
|
18777
19481
|
this.jobs.set(id, job);
|
|
18778
|
-
|
|
18779
|
-
|
|
18780
|
-
|
|
18781
|
-
job.
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
job.
|
|
18786
|
-
|
|
19482
|
+
const controller = new AbortController();
|
|
19483
|
+
this.controllers.set(id, controller);
|
|
19484
|
+
void this.#acquire().then(() => {
|
|
19485
|
+
if (job.status === "cancelled") {
|
|
19486
|
+
this.#release(id);
|
|
19487
|
+
return;
|
|
19488
|
+
}
|
|
19489
|
+
job.status = "running";
|
|
19490
|
+
Promise.resolve().then(() => fn(controller.signal)).then((result) => {
|
|
19491
|
+
if (job.status === "cancelled") return;
|
|
19492
|
+
job.result = result;
|
|
19493
|
+
job.status = "completed";
|
|
19494
|
+
}).catch((err) => {
|
|
19495
|
+
if (job.status === "cancelled") return;
|
|
19496
|
+
job.error = err instanceof Error ? err.message : String(err);
|
|
19497
|
+
job.status = "failed";
|
|
19498
|
+
}).finally(() => this.#release(id));
|
|
18787
19499
|
});
|
|
18788
19500
|
return id;
|
|
18789
19501
|
}
|
|
@@ -18794,17 +19506,39 @@ var JobQueue = class {
|
|
|
18794
19506
|
return [...this.jobs.values()];
|
|
18795
19507
|
}
|
|
18796
19508
|
/**
|
|
18797
|
-
* Cancel a pending or running job. Returns true if cancelled.
|
|
19509
|
+
* Cancel a pending or running job. Returns true if cancelled. #58 — aborts the
|
|
19510
|
+
* job's `AbortSignal` so a cooperative running job is actually interrupted.
|
|
18798
19511
|
*/
|
|
18799
19512
|
cancel(id) {
|
|
18800
19513
|
const job = this.jobs.get(id);
|
|
18801
19514
|
if (!job) return false;
|
|
18802
19515
|
if (job.status === "pending" || job.status === "running") {
|
|
18803
19516
|
job.status = "cancelled";
|
|
19517
|
+
this.controllers.get(id)?.abort();
|
|
18804
19518
|
return true;
|
|
18805
19519
|
}
|
|
18806
19520
|
return false;
|
|
18807
19521
|
}
|
|
19522
|
+
/** Acquire a concurrency slot (resolves immediately when unbounded/free). */
|
|
19523
|
+
#acquire() {
|
|
19524
|
+
if (this.running < this.maxConcurrency) {
|
|
19525
|
+
this.running += 1;
|
|
19526
|
+
return Promise.resolve();
|
|
19527
|
+
}
|
|
19528
|
+
return new Promise((resolve3) => {
|
|
19529
|
+
this.waiting.push(() => {
|
|
19530
|
+
this.running += 1;
|
|
19531
|
+
resolve3();
|
|
19532
|
+
});
|
|
19533
|
+
});
|
|
19534
|
+
}
|
|
19535
|
+
/** Release a slot + clean up the controller; start the next waiting job. */
|
|
19536
|
+
#release(id) {
|
|
19537
|
+
this.controllers.delete(id);
|
|
19538
|
+
this.running -= 1;
|
|
19539
|
+
const next = this.waiting.shift();
|
|
19540
|
+
if (next !== void 0) next();
|
|
19541
|
+
}
|
|
18808
19542
|
};
|
|
18809
19543
|
|
|
18810
19544
|
// src/internal/memory/dreaming/run.ts
|
|
@@ -19297,24 +20031,42 @@ async function migrateSqliteToLance2(options) {
|
|
|
19297
20031
|
}
|
|
19298
20032
|
|
|
19299
20033
|
// src/permission-engine.ts
|
|
20034
|
+
function argMatches(matcher, value) {
|
|
20035
|
+
if (typeof matcher === "function") return matcher(value);
|
|
20036
|
+
if (value === void 0) return false;
|
|
20037
|
+
if (matcher instanceof RegExp) return matcher.test(String(value));
|
|
20038
|
+
return matcher === value;
|
|
20039
|
+
}
|
|
19300
20040
|
var PermissionEngine = class {
|
|
19301
20041
|
constructor(rules, options = {}) {
|
|
19302
20042
|
this.rules = rules;
|
|
19303
|
-
this.defaultAction = options.defaultAction ?? "
|
|
20043
|
+
this.defaultAction = options.defaultAction ?? "ask";
|
|
19304
20044
|
}
|
|
19305
20045
|
rules;
|
|
19306
20046
|
defaultAction;
|
|
19307
20047
|
/**
|
|
19308
|
-
* Evaluate a tool name against the rules. First
|
|
19309
|
-
* configured `defaultAction` (default `"
|
|
20048
|
+
* Evaluate a tool name (and optional arguments) against the rules. First
|
|
20049
|
+
* match wins; falls back to the configured `defaultAction` (default `"ask"`,
|
|
20050
|
+
* fail-closed) when no rule matches. #55 — a rule with `args` gates on the
|
|
20051
|
+
* argument values, so the same tool name can resolve to different actions
|
|
20052
|
+
* depending on what it is asked to do.
|
|
19310
20053
|
*/
|
|
19311
|
-
evaluate(toolName) {
|
|
20054
|
+
evaluate(toolName, args) {
|
|
19312
20055
|
for (const rule of this.rules) {
|
|
19313
|
-
const
|
|
19314
|
-
if (
|
|
20056
|
+
const nameMatches = typeof rule.tool === "string" ? rule.tool === toolName : rule.tool.test(toolName);
|
|
20057
|
+
if (!nameMatches) continue;
|
|
20058
|
+
if (rule.args !== void 0 && !this.#argsMatch(rule.args, args)) continue;
|
|
20059
|
+
return rule.action;
|
|
19315
20060
|
}
|
|
19316
20061
|
return this.defaultAction;
|
|
19317
20062
|
}
|
|
20063
|
+
#argsMatch(matchers, args) {
|
|
20064
|
+
const call = args ?? {};
|
|
20065
|
+
for (const [key2, matcher] of Object.entries(matchers)) {
|
|
20066
|
+
if (!argMatches(matcher, call[key2])) return false;
|
|
20067
|
+
}
|
|
20068
|
+
return true;
|
|
20069
|
+
}
|
|
19318
20070
|
};
|
|
19319
20071
|
|
|
19320
20072
|
// src/permission-plugin.ts
|
|
@@ -19325,8 +20077,8 @@ function createPermissionPlugin(engine, opts = {}) {
|
|
|
19325
20077
|
kind: "general",
|
|
19326
20078
|
register(ctx) {
|
|
19327
20079
|
ctx.on("pre_tool_call", (rawCtx) => {
|
|
19328
|
-
const { name } = rawCtx;
|
|
19329
|
-
const action = engine.evaluate(name);
|
|
20080
|
+
const { name, args } = rawCtx;
|
|
20081
|
+
const action = engine.evaluate(name, args);
|
|
19330
20082
|
if (action === "deny") {
|
|
19331
20083
|
return { block: true, message: `denied by permission engine: ${name}` };
|
|
19332
20084
|
}
|
|
@@ -19391,6 +20143,14 @@ var Security = class {
|
|
|
19391
20143
|
}
|
|
19392
20144
|
};
|
|
19393
20145
|
|
|
20146
|
+
// src/session-scope.ts
|
|
20147
|
+
function scopedConversationId(scope, id) {
|
|
20148
|
+
return `${scope}__${id}`;
|
|
20149
|
+
}
|
|
20150
|
+
function sessionScopePrefix(scope) {
|
|
20151
|
+
return `${scope}__`;
|
|
20152
|
+
}
|
|
20153
|
+
|
|
19394
20154
|
// src/squad.ts
|
|
19395
20155
|
init_errors();
|
|
19396
20156
|
var PersistenceSchema = z.object({
|
|
@@ -20108,6 +20868,6 @@ function safeStringify2(v) {
|
|
|
20108
20868
|
}
|
|
20109
20869
|
}
|
|
20110
20870
|
|
|
20111
|
-
export { Agent, AgentBuilder, AgentDisposedError, AgentRunError, AuthenticationError, Budget, BudgetExceededError, ConfigurationError, Cron, EventBus, FileSystemConversationStorage, GenerateObjectError, InMemoryConversationStorage, IntegrationNotConnectedError, InvalidTaskIdError, JobQueue, Memory, MemoryAdapterError, NetworkError, PermissionEngine, RateLimitError, Security, StreamObjectError, Task, TaskNotFoundError, Theokit, TheokitAgentError, UnknownAgentError, UnsupportedBudgetOperationError, UnsupportedRunOperationError, UnsupportedTaskOperationError, UsageAccumulator, buildReplayHistory, chargeAndCheckThresholds, computeCost, createAgentFactory, createCounterBudgetTracker, createNoopMemoryProvider, createPermissionPlugin, createSquad, definePlugin, defineProvider, defineTool, extractRawId, getPricingEntry, inferApiMode, isTransientError, migrateSqliteToLance2 as migrateSqliteToLance, mkMemoryId, normalizeUsage, preflightCheck, toShareGptTrajectory, withCwdMutex };
|
|
20871
|
+
export { Agent, AgentBuilder, AgentDisposedError, AgentRunError, AuthenticationError, Budget, BudgetExceededError, ConfigurationError, Cron, EventBus, FileSystemConversationStorage, GenerateObjectError, InMemoryConversationStorage, IntegrationNotConnectedError, InvalidTaskIdError, JobQueue, Memory, MemoryAdapterError, NetworkError, PermissionEngine, RateLimitError, Security, StreamObjectError, Task, TaskNotFoundError, Theokit, TheokitAgentError, UnknownAgentError, UnsupportedBudgetOperationError, UnsupportedRunOperationError, UnsupportedTaskOperationError, UsageAccumulator, buildReplayHistory, chargeAndCheckThresholds, computeCost, createAgentFactory, createCounterBudgetTracker, createNoopMemoryProvider, createPermissionPlugin, createSquad, definePlugin, defineProvider, defineTool, extractRawId, getPricingEntry, inferApiMode, isTransientError, migrateSqliteToLance2 as migrateSqliteToLance, mkMemoryId, normalizeUsage, preflightCheck, scopedConversationId, sessionScopePrefix, toShareGptTrajectory, withCwdMutex };
|
|
20112
20872
|
//# sourceMappingURL=index.js.map
|
|
20113
20873
|
//# sourceMappingURL=index.js.map
|