@cognipeer/agent-sdk 0.1.1 → 0.1.2
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/README.md +177 -103
- package/dist/index.cjs +2014 -421
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +529 -111
- package/dist/index.d.ts +529 -111
- package/dist/index.mjs +2822 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -26
- package/dist/index.js +0 -1212
- package/dist/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2822 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { Buffer } from 'buffer';
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
import AjvModule from 'ajv';
|
|
8
|
+
|
|
9
|
+
// src/adapters/langchain.ts
|
|
10
|
+
var moduleName = "@langchain/core/tools";
|
|
11
|
+
var cachedToolFactory;
|
|
12
|
+
var cachedHasAttemptedFactory = false;
|
|
13
|
+
var requireFn = null;
|
|
14
|
+
try {
|
|
15
|
+
requireFn = createRequire(import.meta.url);
|
|
16
|
+
} catch {
|
|
17
|
+
requireFn = null;
|
|
18
|
+
}
|
|
19
|
+
function getExecutor(tool) {
|
|
20
|
+
if (typeof tool.invoke === "function") return (input) => tool.invoke(input);
|
|
21
|
+
if (typeof tool.call === "function") return (input) => tool.call(input);
|
|
22
|
+
if (typeof tool.func === "function") return (input) => tool.func(input);
|
|
23
|
+
if (typeof tool.run === "function") return (input) => tool.run(input);
|
|
24
|
+
if (typeof tool === "function") {
|
|
25
|
+
return tool;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Tool ${tool?.name ?? "<unnamed>"} is not invokable`);
|
|
28
|
+
}
|
|
29
|
+
function loadLangchainToolFactory() {
|
|
30
|
+
if (cachedHasAttemptedFactory) return cachedToolFactory ?? null;
|
|
31
|
+
cachedHasAttemptedFactory = true;
|
|
32
|
+
cachedToolFactory = null;
|
|
33
|
+
if (!requireFn) return null;
|
|
34
|
+
try {
|
|
35
|
+
const mod = requireFn(moduleName);
|
|
36
|
+
const factory = typeof mod?.tool === "function" ? mod.tool : void 0;
|
|
37
|
+
if (typeof factory === "function") {
|
|
38
|
+
cachedToolFactory = factory;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
cachedToolFactory = null;
|
|
42
|
+
}
|
|
43
|
+
return cachedToolFactory ?? null;
|
|
44
|
+
}
|
|
45
|
+
function ensureSchema(candidate) {
|
|
46
|
+
if (!candidate) return z.any();
|
|
47
|
+
if (candidate instanceof z.ZodType) return candidate;
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
function toLangchainTools(tools) {
|
|
51
|
+
const factory = loadLangchainToolFactory();
|
|
52
|
+
if (!factory) return tools;
|
|
53
|
+
return tools.map((tool) => {
|
|
54
|
+
if (!tool) return tool;
|
|
55
|
+
if (tool.__lcTool) return tool.__lcTool;
|
|
56
|
+
const executor = getExecutor(tool);
|
|
57
|
+
try {
|
|
58
|
+
const lcTool = factory(async (input) => executor(input), {
|
|
59
|
+
name: tool.name,
|
|
60
|
+
description: tool.description,
|
|
61
|
+
schema: ensureSchema(tool.schema)
|
|
62
|
+
});
|
|
63
|
+
tool.__lcTool = lcTool;
|
|
64
|
+
return lcTool;
|
|
65
|
+
} catch {
|
|
66
|
+
return tool;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function fromLangchainTools(tools) {
|
|
71
|
+
return tools.map((tool) => {
|
|
72
|
+
if (!tool) return tool;
|
|
73
|
+
if (tool.__source === "smart") return tool;
|
|
74
|
+
if (typeof tool === "function" && !tool.name) {
|
|
75
|
+
const wrapped2 = createSimpleTool({
|
|
76
|
+
name: "anonymous_tool",
|
|
77
|
+
description: void 0,
|
|
78
|
+
schema: z.any(),
|
|
79
|
+
executor: tool
|
|
80
|
+
});
|
|
81
|
+
wrapped2.__source = "langchain";
|
|
82
|
+
wrapped2.__lc = tool;
|
|
83
|
+
return wrapped2;
|
|
84
|
+
}
|
|
85
|
+
const name = tool.name || tool?.lc_kwargs?.name || tool?.config?.name || "tool";
|
|
86
|
+
const description = tool.description || tool?.lc_kwargs?.description;
|
|
87
|
+
const schema = tool.schema || tool?.lc_kwargs?.schema || z.any();
|
|
88
|
+
const executor = (input) => {
|
|
89
|
+
if (typeof tool.invoke === "function") return tool.invoke(input);
|
|
90
|
+
if (typeof tool.call === "function") return tool.call(input);
|
|
91
|
+
if (typeof tool._call === "function") return tool._call(input);
|
|
92
|
+
if (typeof tool.run === "function") return tool.run(input);
|
|
93
|
+
if (typeof tool.func === "function") return tool.func(input);
|
|
94
|
+
if (typeof tool === "function") return tool(input);
|
|
95
|
+
throw new Error(`LangChain tool ${name} is not invokable`);
|
|
96
|
+
};
|
|
97
|
+
const wrapped = createSimpleTool({
|
|
98
|
+
name,
|
|
99
|
+
description,
|
|
100
|
+
schema,
|
|
101
|
+
executor
|
|
102
|
+
});
|
|
103
|
+
wrapped.__source = "langchain";
|
|
104
|
+
wrapped.__lc = tool;
|
|
105
|
+
return wrapped;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function createSimpleTool({
|
|
109
|
+
name,
|
|
110
|
+
description,
|
|
111
|
+
schema,
|
|
112
|
+
executor
|
|
113
|
+
}) {
|
|
114
|
+
const invoke = async (input) => executor(input);
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
schema,
|
|
119
|
+
invoke,
|
|
120
|
+
call: invoke,
|
|
121
|
+
run: invoke,
|
|
122
|
+
func: invoke
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/model.ts
|
|
127
|
+
function isSmartModel(m) {
|
|
128
|
+
return !!m && typeof m === "object" && typeof m.invoke === "function";
|
|
129
|
+
}
|
|
130
|
+
function withTools(model, tools) {
|
|
131
|
+
if (model?.bindTools) return model.bindTools(tools);
|
|
132
|
+
return model;
|
|
133
|
+
}
|
|
134
|
+
function fromLangchainModel(lcModel) {
|
|
135
|
+
if (!lcModel) throw new Error("fromLangchainModel: model is undefined/null");
|
|
136
|
+
const adapted = {
|
|
137
|
+
invoke: async (messages) => {
|
|
138
|
+
const normalizeContent = (content) => {
|
|
139
|
+
if (content == null) return content;
|
|
140
|
+
if (typeof content === "string") return content;
|
|
141
|
+
if (Array.isArray(content)) {
|
|
142
|
+
return content.map((part) => {
|
|
143
|
+
if (!part || typeof part !== "object") return part;
|
|
144
|
+
if (part.type === "text") return { type: "text", text: String(part.text ?? "") };
|
|
145
|
+
if (part.type === "image_url") {
|
|
146
|
+
const img = part.image_url;
|
|
147
|
+
if (typeof img === "string") return { type: "image_url", image_url: { url: img } };
|
|
148
|
+
if (img && typeof img === "object") {
|
|
149
|
+
if ("url" in img) return { type: "image_url", image_url: img };
|
|
150
|
+
if ("base64" in img) {
|
|
151
|
+
const media = img.media_type || "image/jpeg";
|
|
152
|
+
const dataUrl = `data:${media};base64,${img.base64}`;
|
|
153
|
+
const detail = img.detail;
|
|
154
|
+
return { type: "image_url", image_url: { url: dataUrl, detail } };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return part;
|
|
158
|
+
}
|
|
159
|
+
return part;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return content;
|
|
163
|
+
};
|
|
164
|
+
const toLC = (m) => {
|
|
165
|
+
if (m._getType || m.lc_serializable) return m;
|
|
166
|
+
const role = m.role;
|
|
167
|
+
return { role, content: normalizeContent(m.content), name: m.name, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id };
|
|
168
|
+
};
|
|
169
|
+
const lcMessages = messages.map(toLC);
|
|
170
|
+
const response = await lcModel.invoke(lcMessages);
|
|
171
|
+
if (response && typeof response === "object") {
|
|
172
|
+
const content = response.content ?? response.text ?? "";
|
|
173
|
+
return {
|
|
174
|
+
role: response.role || "assistant",
|
|
175
|
+
content,
|
|
176
|
+
tool_calls: response.tool_calls,
|
|
177
|
+
usage: response.usage,
|
|
178
|
+
response_metadata: response.response_metadata,
|
|
179
|
+
...response
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { role: "assistant", content: String(response ?? "") };
|
|
183
|
+
},
|
|
184
|
+
bindTools: (tools) => {
|
|
185
|
+
const lcReady = toLangchainTools(tools);
|
|
186
|
+
if (typeof lcModel.bindTools === "function") {
|
|
187
|
+
const bound = lcModel.bindTools(lcReady);
|
|
188
|
+
return fromLangchainModel(bound);
|
|
189
|
+
}
|
|
190
|
+
if (typeof lcModel.bind === "function") {
|
|
191
|
+
const bound = lcModel.bind({ tools: lcReady });
|
|
192
|
+
return fromLangchainModel(bound);
|
|
193
|
+
}
|
|
194
|
+
return adapted;
|
|
195
|
+
},
|
|
196
|
+
modelName: lcModel.modelName || lcModel._modelId || lcModel._llmType || lcModel.name,
|
|
197
|
+
_lc: lcModel
|
|
198
|
+
};
|
|
199
|
+
return adapted;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/tool.ts
|
|
203
|
+
function createTool({
|
|
204
|
+
name,
|
|
205
|
+
description,
|
|
206
|
+
schema,
|
|
207
|
+
func,
|
|
208
|
+
needsApproval,
|
|
209
|
+
approvalPrompt,
|
|
210
|
+
approvalDefaults
|
|
211
|
+
}) {
|
|
212
|
+
const execute = async (input) => func(input);
|
|
213
|
+
const toolRecord = {
|
|
214
|
+
name,
|
|
215
|
+
description,
|
|
216
|
+
schema,
|
|
217
|
+
// Our runtime prefers invoke but fallback helpers keep compatibility with LC-style tools.
|
|
218
|
+
invoke: execute,
|
|
219
|
+
call: execute,
|
|
220
|
+
run: execute,
|
|
221
|
+
func: execute
|
|
222
|
+
};
|
|
223
|
+
if (typeof needsApproval === "boolean") {
|
|
224
|
+
toolRecord.needsApproval = needsApproval;
|
|
225
|
+
}
|
|
226
|
+
if (approvalPrompt !== void 0) {
|
|
227
|
+
toolRecord.approvalPrompt = approvalPrompt;
|
|
228
|
+
}
|
|
229
|
+
if (approvalDefaults !== void 0) {
|
|
230
|
+
toolRecord.approvalDefaults = approvalDefaults;
|
|
231
|
+
}
|
|
232
|
+
toolRecord.__source = toolRecord.__source || "smart";
|
|
233
|
+
toolRecord.__impl = func;
|
|
234
|
+
return toolRecord;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/prompts.ts
|
|
238
|
+
function buildSystemPrompt(extra, planning, name = "Agent") {
|
|
239
|
+
const extraTrimmed = extra?.trim();
|
|
240
|
+
const agentHeader = `Agent Name: ${name}`;
|
|
241
|
+
const planningBlock = planning ? `<planning>
|
|
242
|
+
PLANNING IS MANDATORY.
|
|
243
|
+
|
|
244
|
+
Rules:
|
|
245
|
+
1) Your FIRST action in every task is a single call to the tool "manage_todo_list" with operation="write" and a full, ordered plan (1+ items). Even for trivial tasks, write a one-item plan.
|
|
246
|
+
2) After every non-planning tool call, you MUST immediately call "manage_todo_list" with operation="write" (or re-write) to update statuses and append one-line evidence on the affected item only.
|
|
247
|
+
3) Keep exactly ONE item "in-progress" at any time; all others are "not-started" or "completed".
|
|
248
|
+
4) Never reveal the plan text. Never summarize the plan in the assistant messages.
|
|
249
|
+
5) If you ever produce an assistant message without having written a plan in this session, STOP and first write the plan via "manage_todo_list".
|
|
250
|
+
</planning>` : "";
|
|
251
|
+
return [
|
|
252
|
+
agentHeader,
|
|
253
|
+
"You are an advanced AI agent that is concise, accurate, and helpful.",
|
|
254
|
+
"Follow these rules:",
|
|
255
|
+
"- Use tools only when they add value; avoid redundant calls.",
|
|
256
|
+
"- Never fabricate tool results; if unavailable, say so briefly.",
|
|
257
|
+
"- Prefer short, structured answers; use bullet points when helpful.",
|
|
258
|
+
"- Keep privacy and safety: do not reveal secrets or sensitive data.",
|
|
259
|
+
"- If inputs are ambiguous or missing, ask one concise clarifying question.",
|
|
260
|
+
"- Reuse prior tool results already present in the conversation when sufficient.",
|
|
261
|
+
planningBlock,
|
|
262
|
+
extraTrimmed ? `Extra instructions: ${extraTrimmed}` : ""
|
|
263
|
+
].filter(Boolean).join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/types.ts
|
|
267
|
+
var GuardrailPhase = /* @__PURE__ */ ((GuardrailPhase2) => {
|
|
268
|
+
GuardrailPhase2["Request"] = "request";
|
|
269
|
+
GuardrailPhase2["Response"] = "response";
|
|
270
|
+
return GuardrailPhase2;
|
|
271
|
+
})(GuardrailPhase || {});
|
|
272
|
+
|
|
273
|
+
// src/nodes/resolver.ts
|
|
274
|
+
function createResolverNode() {
|
|
275
|
+
return async (state) => {
|
|
276
|
+
const messages = Array.isArray(state.messages) ? state.messages : [];
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
messages,
|
|
280
|
+
summaries: state.summaries || [],
|
|
281
|
+
toolCallCount: state.toolCallCount || 0,
|
|
282
|
+
toolCache: state.toolCache || {},
|
|
283
|
+
toolHistory: state.toolHistory || [],
|
|
284
|
+
toolHistoryArchived: state.toolHistoryArchived || [],
|
|
285
|
+
pendingApprovals: state.pendingApprovals || [],
|
|
286
|
+
plan: state.plan || null,
|
|
287
|
+
planVersion: state.planVersion || 0
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/utils/usage.ts
|
|
293
|
+
function normalizeUsage(raw) {
|
|
294
|
+
if (!raw) return void 0;
|
|
295
|
+
const prompt = num(raw.prompt_tokens, raw.input_tokens, raw.promptTokens, raw.total_prompt_tokens);
|
|
296
|
+
const completion = num(raw.completion_tokens, raw.output_tokens, raw.completionTokens, raw.total_completion_tokens);
|
|
297
|
+
const total = num(
|
|
298
|
+
raw.total_tokens,
|
|
299
|
+
raw.totalTokens,
|
|
300
|
+
(typeof prompt === "number" ? prompt : 0) + (typeof completion === "number" ? completion : 0)
|
|
301
|
+
);
|
|
302
|
+
const promptDetailsSrc = raw.prompt_tokens_details || raw.promptTokensDetails || {};
|
|
303
|
+
const completionDetailsSrc = raw.completion_tokens_details || raw.completionTokensDetails || {};
|
|
304
|
+
const normalized = {
|
|
305
|
+
prompt_tokens: safe(prompt),
|
|
306
|
+
completion_tokens: safe(completion),
|
|
307
|
+
total_tokens: safe(total || (prompt || 0) + (completion || 0)),
|
|
308
|
+
prompt_tokens_details: {
|
|
309
|
+
cached_tokens: safe(
|
|
310
|
+
promptDetailsSrc.cached_tokens || promptDetailsSrc.cached || raw.cached_input_tokens || raw.cached_prompt_tokens
|
|
311
|
+
),
|
|
312
|
+
audio_tokens: safe(promptDetailsSrc.audio_tokens)
|
|
313
|
+
},
|
|
314
|
+
completion_tokens_details: {
|
|
315
|
+
reasoning_tokens: safe(completionDetailsSrc.reasoning_tokens),
|
|
316
|
+
audio_tokens: safe(completionDetailsSrc.audio_tokens),
|
|
317
|
+
accepted_prediction_tokens: safe(completionDetailsSrc.accepted_prediction_tokens),
|
|
318
|
+
rejected_prediction_tokens: safe(completionDetailsSrc.rejected_prediction_tokens)
|
|
319
|
+
},
|
|
320
|
+
raw
|
|
321
|
+
};
|
|
322
|
+
return normalized;
|
|
323
|
+
}
|
|
324
|
+
function num(...vals) {
|
|
325
|
+
for (const v of vals) {
|
|
326
|
+
if (v === 0) return 0;
|
|
327
|
+
if (v == null) continue;
|
|
328
|
+
const n = Number(v);
|
|
329
|
+
if (!Number.isNaN(n)) return n;
|
|
330
|
+
}
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
function safe(v) {
|
|
334
|
+
const n = Number(v);
|
|
335
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
336
|
+
}
|
|
337
|
+
var DEFAULT_COGNIPEER_URL = "https://api.cognipeer.com/v1/client/tracing/sessions";
|
|
338
|
+
function fileSink(path2) {
|
|
339
|
+
return { type: "file", path: path2 };
|
|
340
|
+
}
|
|
341
|
+
function customSink(handler) {
|
|
342
|
+
if (typeof handler === "function") {
|
|
343
|
+
return { type: "custom", onEvent: handler };
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
type: "custom",
|
|
347
|
+
onEvent: typeof handler?.onEvent === "function" ? handler.onEvent : void 0,
|
|
348
|
+
onSession: typeof handler?.onSession === "function" ? handler.onSession : void 0
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function cognipeerSink(first, second) {
|
|
352
|
+
if (second === void 0) {
|
|
353
|
+
const apiKey = first ?? "";
|
|
354
|
+
return { type: "cognipeer", apiKey };
|
|
355
|
+
}
|
|
356
|
+
const url = typeof first === "string" && first.trim().length > 0 ? first.trim() : void 0;
|
|
357
|
+
return { type: "cognipeer", url, apiKey: second };
|
|
358
|
+
}
|
|
359
|
+
function httpSink(url, headers) {
|
|
360
|
+
return { type: "http", url, headers };
|
|
361
|
+
}
|
|
362
|
+
function resolveLogsBaseDir(customPath, ensureDirectory = true) {
|
|
363
|
+
const root = process.cwd();
|
|
364
|
+
const base = customPath && customPath.trim().length > 0 ? customPath : path.join(root, "logs");
|
|
365
|
+
if (ensureDirectory) {
|
|
366
|
+
ensureDir(base);
|
|
367
|
+
}
|
|
368
|
+
return base;
|
|
369
|
+
}
|
|
370
|
+
function coerceToString(value) {
|
|
371
|
+
if (value == null) return void 0;
|
|
372
|
+
if (typeof value === "string") {
|
|
373
|
+
const trimmed = value.trim();
|
|
374
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
377
|
+
return String(value);
|
|
378
|
+
}
|
|
379
|
+
if (typeof value === "function" && value.length === 0) {
|
|
380
|
+
try {
|
|
381
|
+
return coerceToString(value());
|
|
382
|
+
} catch {
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(value)) {
|
|
387
|
+
for (const item of value) {
|
|
388
|
+
const normalized = coerceToString(item);
|
|
389
|
+
if (normalized) return normalized;
|
|
390
|
+
}
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
function firstNonEmptyString(...candidates) {
|
|
396
|
+
for (const candidate of candidates) {
|
|
397
|
+
const normalized = coerceToString(candidate);
|
|
398
|
+
if (normalized) return normalized;
|
|
399
|
+
}
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
function getModelName(model) {
|
|
403
|
+
if (!model) return void 0;
|
|
404
|
+
const maybeLc = model?._lc;
|
|
405
|
+
const direct = firstNonEmptyString(
|
|
406
|
+
model?.model,
|
|
407
|
+
model?.options?.model,
|
|
408
|
+
model?.params?.model,
|
|
409
|
+
model?.config?.model,
|
|
410
|
+
model?.configuration?.model,
|
|
411
|
+
model?.metadata?.model,
|
|
412
|
+
model?.lc_kwargs?.model,
|
|
413
|
+
maybeLc?.model,
|
|
414
|
+
maybeLc?.options?.model,
|
|
415
|
+
maybeLc?.params?.model,
|
|
416
|
+
maybeLc?.config?.model,
|
|
417
|
+
maybeLc?.configuration?.model,
|
|
418
|
+
maybeLc?.metadata?.model,
|
|
419
|
+
maybeLc?.lc_kwargs?.model,
|
|
420
|
+
model?.metadata?.modelName,
|
|
421
|
+
maybeLc?.metadata?.modelName,
|
|
422
|
+
model?.modelName,
|
|
423
|
+
maybeLc?.modelName,
|
|
424
|
+
model?.lc_alias,
|
|
425
|
+
model?.model_id,
|
|
426
|
+
model?.id,
|
|
427
|
+
maybeLc?.model_id,
|
|
428
|
+
maybeLc?.id,
|
|
429
|
+
model?._model,
|
|
430
|
+
model?._modelName,
|
|
431
|
+
model?.__model
|
|
432
|
+
);
|
|
433
|
+
if (direct) return direct;
|
|
434
|
+
const maybeClient = model?.client || model?.api || model?.service;
|
|
435
|
+
const clientValue = firstNonEmptyString(
|
|
436
|
+
maybeClient?.model,
|
|
437
|
+
maybeClient?.config?.model,
|
|
438
|
+
maybeClient?.options?.model,
|
|
439
|
+
maybeClient?.default?.model
|
|
440
|
+
);
|
|
441
|
+
if (clientValue) return clientValue;
|
|
442
|
+
const fnNames = firstNonEmptyString(model?._modelId, model?._llmType, maybeLc?._modelId, maybeLc?._llmType, model?.constructor?.name);
|
|
443
|
+
return fnNames;
|
|
444
|
+
}
|
|
445
|
+
function getProviderName(model) {
|
|
446
|
+
if (!model) return void 0;
|
|
447
|
+
const direct = firstNonEmptyString(
|
|
448
|
+
model?.provider,
|
|
449
|
+
model?.options?.provider,
|
|
450
|
+
model?.params?.provider,
|
|
451
|
+
model?.config?.provider,
|
|
452
|
+
model?.configuration?.provider,
|
|
453
|
+
model?.metadata?.provider,
|
|
454
|
+
model?.lc_kwargs?.provider,
|
|
455
|
+
model?.lc_alias,
|
|
456
|
+
model?.__provider
|
|
457
|
+
);
|
|
458
|
+
if (direct) return direct;
|
|
459
|
+
const maybeLc = model?._lc;
|
|
460
|
+
const lcValue = maybeLc ? firstNonEmptyString(
|
|
461
|
+
maybeLc?.provider,
|
|
462
|
+
maybeLc?.options?.provider,
|
|
463
|
+
maybeLc?.config?.provider,
|
|
464
|
+
maybeLc?.configuration?.provider,
|
|
465
|
+
maybeLc?.metadata?.provider,
|
|
466
|
+
maybeLc?.lc_kwargs?.provider,
|
|
467
|
+
maybeLc?.client?.config?.provider
|
|
468
|
+
) : void 0;
|
|
469
|
+
if (lcValue) return lcValue;
|
|
470
|
+
const maybeClient = model?.client || model?.api || model?.service;
|
|
471
|
+
return firstNonEmptyString(
|
|
472
|
+
maybeClient?.provider,
|
|
473
|
+
maybeClient?.config?.provider,
|
|
474
|
+
maybeClient?.options?.provider,
|
|
475
|
+
maybeClient?.metadata?.provider
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
function inferModelFromMessages(messageList) {
|
|
479
|
+
if (!Array.isArray(messageList)) return void 0;
|
|
480
|
+
for (let i = messageList.length - 1; i >= 0; i -= 1) {
|
|
481
|
+
const message = messageList[i];
|
|
482
|
+
if (!message) continue;
|
|
483
|
+
const candidate = firstNonEmptyString(
|
|
484
|
+
message?.metadata?.model,
|
|
485
|
+
message?.metadata?.modelName,
|
|
486
|
+
message?.metadata?.modelNames,
|
|
487
|
+
message?.metadata?.model_id,
|
|
488
|
+
message?.response_metadata?.model,
|
|
489
|
+
message?.response_metadata?.modelName,
|
|
490
|
+
message?.response_metadata?.modelNames,
|
|
491
|
+
message?.response_metadata?.model_id,
|
|
492
|
+
message?.usage_metadata?.model,
|
|
493
|
+
message?.model,
|
|
494
|
+
message?.modelName,
|
|
495
|
+
message?.model_id,
|
|
496
|
+
message?.additional_kwargs?.model,
|
|
497
|
+
message?.additional_kwargs?.modelName,
|
|
498
|
+
message?.info?.model,
|
|
499
|
+
message?.annotations?.model
|
|
500
|
+
);
|
|
501
|
+
if (candidate) return candidate;
|
|
502
|
+
}
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
function inferProviderFromMessages(messageList) {
|
|
506
|
+
if (!Array.isArray(messageList)) return void 0;
|
|
507
|
+
for (let i = messageList.length - 1; i >= 0; i -= 1) {
|
|
508
|
+
const message = messageList[i];
|
|
509
|
+
if (!message) continue;
|
|
510
|
+
const candidate = firstNonEmptyString(
|
|
511
|
+
message?.metadata?.provider,
|
|
512
|
+
message?.metadata?.providers,
|
|
513
|
+
message?.response_metadata?.provider,
|
|
514
|
+
message?.response_metadata?.providers,
|
|
515
|
+
message?.usage_metadata?.provider,
|
|
516
|
+
message?.provider,
|
|
517
|
+
message?.additional_kwargs?.provider,
|
|
518
|
+
message?.info?.provider,
|
|
519
|
+
message?.annotations?.provider
|
|
520
|
+
);
|
|
521
|
+
if (candidate) return candidate;
|
|
522
|
+
}
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
var DEFAULT_TRACE_CONFIG = {
|
|
526
|
+
logData: true
|
|
527
|
+
};
|
|
528
|
+
function ensureDir(p) {
|
|
529
|
+
if (!fs.existsSync(p)) {
|
|
530
|
+
fs.mkdirSync(p, { recursive: true });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function withDefaults(config) {
|
|
534
|
+
const enabled = Boolean(config?.enabled);
|
|
535
|
+
const logData = config?.logData ?? DEFAULT_TRACE_CONFIG.logData;
|
|
536
|
+
let sink;
|
|
537
|
+
try {
|
|
538
|
+
sink = resolveSink(config?.sink);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
541
|
+
console.warn("Invalid tracing sink configuration. Falling back to file sink.", err);
|
|
542
|
+
}
|
|
543
|
+
sink = resolveSink({ type: "file" });
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
enabled,
|
|
547
|
+
logData,
|
|
548
|
+
mode: "batched",
|
|
549
|
+
sink
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function resolveSink(sink) {
|
|
553
|
+
const candidate = sink ?? { type: "file" };
|
|
554
|
+
switch (candidate.type) {
|
|
555
|
+
case "file": {
|
|
556
|
+
const baseDir = resolveLogsBaseDir(candidate.path, false);
|
|
557
|
+
return { type: "file", baseDir };
|
|
558
|
+
}
|
|
559
|
+
case "custom": {
|
|
560
|
+
const onEvent = typeof candidate.onEvent === "function" ? candidate.onEvent : void 0;
|
|
561
|
+
const onSession = typeof candidate.onSession === "function" ? candidate.onSession : void 0;
|
|
562
|
+
return { type: "custom", onEvent, onSession };
|
|
563
|
+
}
|
|
564
|
+
case "cognipeer": {
|
|
565
|
+
const apiKey = typeof candidate.apiKey === "string" ? candidate.apiKey.trim() : "";
|
|
566
|
+
if (!apiKey) {
|
|
567
|
+
throw new Error("cognipeer sink requires a non-empty apiKey");
|
|
568
|
+
}
|
|
569
|
+
const url = typeof candidate.url === "string" && candidate.url.trim().length > 0 ? candidate.url.trim() : DEFAULT_COGNIPEER_URL;
|
|
570
|
+
return { type: "cognipeer", url, apiKey };
|
|
571
|
+
}
|
|
572
|
+
case "http": {
|
|
573
|
+
const url = typeof candidate.url === "string" ? candidate.url.trim() : "";
|
|
574
|
+
if (!url) {
|
|
575
|
+
throw new Error("http sink requires a non-empty url");
|
|
576
|
+
}
|
|
577
|
+
const headers = candidate.headers ? { ...candidate.headers } : void 0;
|
|
578
|
+
return headers ? { type: "http", url, headers } : { type: "http", url };
|
|
579
|
+
}
|
|
580
|
+
default: {
|
|
581
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
582
|
+
console.warn("Unknown tracing sink type. Falling back to file sink.", candidate);
|
|
583
|
+
}
|
|
584
|
+
return resolveSink({ type: "file" });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function snapshotSink(runtime) {
|
|
589
|
+
const sink = runtime.resolvedConfig.sink;
|
|
590
|
+
switch (sink.type) {
|
|
591
|
+
case "file":
|
|
592
|
+
return { type: "file", path: runtime.fileBaseDir || sink.baseDir };
|
|
593
|
+
case "custom":
|
|
594
|
+
return { type: "custom" };
|
|
595
|
+
case "cognipeer":
|
|
596
|
+
return { type: "cognipeer", url: sink.url };
|
|
597
|
+
case "http":
|
|
598
|
+
return { type: "http", url: sink.url };
|
|
599
|
+
default:
|
|
600
|
+
return { type: "custom" };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function buildConfigSnapshot(runtime) {
|
|
604
|
+
return {
|
|
605
|
+
enabled: runtime.resolvedConfig.enabled,
|
|
606
|
+
logData: runtime.resolvedConfig.logData,
|
|
607
|
+
sink: snapshotSink(runtime)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function postTraceSession(url, headers, payload) {
|
|
611
|
+
const fetchFn = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0;
|
|
612
|
+
if (!fetchFn) {
|
|
613
|
+
throw new Error("HTTP sink requires fetch to be available in this runtime.");
|
|
614
|
+
}
|
|
615
|
+
const finalHeaders = { ...headers || {} };
|
|
616
|
+
if (!Object.keys(finalHeaders).some((key) => key.toLowerCase() === "content-type")) {
|
|
617
|
+
finalHeaders["content-type"] = "application/json";
|
|
618
|
+
}
|
|
619
|
+
const response = await fetchFn(url, {
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: finalHeaders,
|
|
622
|
+
body: JSON.stringify(payload)
|
|
623
|
+
});
|
|
624
|
+
if (!response.ok) {
|
|
625
|
+
let responseText = "";
|
|
626
|
+
try {
|
|
627
|
+
responseText = await response.text();
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
const statusLine = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
631
|
+
const bodyPreview = responseText ? ` - ${responseText.slice(0, 200)}` : "";
|
|
632
|
+
throw new Error(`${statusLine}${bodyPreview}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function createEmptySummary() {
|
|
636
|
+
return {
|
|
637
|
+
totalDurationMs: 0,
|
|
638
|
+
totalInputTokens: 0,
|
|
639
|
+
totalOutputTokens: 0,
|
|
640
|
+
totalCachedInputTokens: 0,
|
|
641
|
+
totalBytesIn: 0,
|
|
642
|
+
totalBytesOut: 0,
|
|
643
|
+
eventCounts: {}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function generateSessionId() {
|
|
647
|
+
return `sess_${nanoid(18)}`;
|
|
648
|
+
}
|
|
649
|
+
function generateEventId(sequence) {
|
|
650
|
+
return `evt_${String(sequence).padStart(4, "0")}_${nanoid(4)}`;
|
|
651
|
+
}
|
|
652
|
+
function defaultEventLabel(type) {
|
|
653
|
+
switch (type) {
|
|
654
|
+
case "ai_call":
|
|
655
|
+
return "Assistant Response";
|
|
656
|
+
case "tool_call":
|
|
657
|
+
return "Tool Execution";
|
|
658
|
+
case "session":
|
|
659
|
+
return "Session Event";
|
|
660
|
+
default:
|
|
661
|
+
return type.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function ensureUniqueSections(eventId, sections) {
|
|
665
|
+
if (!sections || sections.length === 0) return void 0;
|
|
666
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
667
|
+
const normalized = [];
|
|
668
|
+
let counter = 1;
|
|
669
|
+
for (const section of sections) {
|
|
670
|
+
const baseLabel = section.label?.trim().length ? section.label.trim() : defaultSectionLabel(section.kind);
|
|
671
|
+
const nextCount = (labelCounts.get(baseLabel) || 0) + 1;
|
|
672
|
+
labelCounts.set(baseLabel, nextCount);
|
|
673
|
+
const finalLabel = nextCount > 1 ? `${baseLabel} (${nextCount})` : baseLabel;
|
|
674
|
+
let id = section.id?.trim();
|
|
675
|
+
if (!id) {
|
|
676
|
+
id = `${section.kind}-${eventId}-${String(counter).padStart(2, "0")}`;
|
|
677
|
+
}
|
|
678
|
+
counter += 1;
|
|
679
|
+
normalized.push({
|
|
680
|
+
...section,
|
|
681
|
+
id,
|
|
682
|
+
label: finalLabel
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return normalized;
|
|
686
|
+
}
|
|
687
|
+
function defaultSectionLabel(kind) {
|
|
688
|
+
switch (kind) {
|
|
689
|
+
case "message":
|
|
690
|
+
return "Message";
|
|
691
|
+
case "tool_call":
|
|
692
|
+
return "Tool Call";
|
|
693
|
+
case "tool_result":
|
|
694
|
+
return "Tool Result";
|
|
695
|
+
case "tool_response":
|
|
696
|
+
return "Tool Response";
|
|
697
|
+
case "summary":
|
|
698
|
+
return "Summary";
|
|
699
|
+
case "metadata":
|
|
700
|
+
return "Details";
|
|
701
|
+
default:
|
|
702
|
+
return "Section";
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function buildMessageLabel(role, index) {
|
|
706
|
+
const normalized = role?.trim().length ? role.trim() : "message";
|
|
707
|
+
const base = `${normalized.charAt(0).toUpperCase()}${normalized.slice(1)} Message`;
|
|
708
|
+
return index > 1 ? `${base} #${index}` : base;
|
|
709
|
+
}
|
|
710
|
+
function toDisplayString(value) {
|
|
711
|
+
const sanitized = sanitizeTracePayload(value);
|
|
712
|
+
if (sanitized == null) return "";
|
|
713
|
+
if (typeof sanitized === "string") return sanitized;
|
|
714
|
+
if (Array.isArray(sanitized)) {
|
|
715
|
+
return sanitized.map((item) => toDisplayString(item)).filter((part) => part && part.trim().length > 0).join("\n");
|
|
716
|
+
}
|
|
717
|
+
if (typeof sanitized === "object") {
|
|
718
|
+
if (typeof sanitized.text === "string" && Object.keys(sanitized).length === 1) {
|
|
719
|
+
return sanitized.text;
|
|
720
|
+
}
|
|
721
|
+
if (typeof sanitized.content === "string") {
|
|
722
|
+
return sanitized.content;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
return JSON.stringify(sanitized, null, 2);
|
|
726
|
+
} catch {
|
|
727
|
+
return String(sanitized);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return String(sanitized);
|
|
731
|
+
}
|
|
732
|
+
function parseToolArguments(args) {
|
|
733
|
+
if (args == null) return void 0;
|
|
734
|
+
if (typeof args === "string") {
|
|
735
|
+
try {
|
|
736
|
+
return JSON.parse(args);
|
|
737
|
+
} catch {
|
|
738
|
+
return args;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return args;
|
|
742
|
+
}
|
|
743
|
+
function collectToolCallSectionsFromMessage(message) {
|
|
744
|
+
const sections = [];
|
|
745
|
+
if (!message) return sections;
|
|
746
|
+
const addCall = (call) => {
|
|
747
|
+
if (!call) return;
|
|
748
|
+
const toolName = call?.name || call?.tool || call?.function?.name || call?.function_call?.name || "tool";
|
|
749
|
+
const rawArgs = call?.arguments ?? call?.args ?? call?.input ?? call?.function?.arguments ?? call?.function_call?.arguments;
|
|
750
|
+
const parsedArgs = parseToolArguments(rawArgs);
|
|
751
|
+
const argumentsPayload = sanitizeTracePayload(parsedArgs ?? rawArgs ?? {});
|
|
752
|
+
sections.push({
|
|
753
|
+
kind: "tool_call",
|
|
754
|
+
label: `Tool Call: ${toolName}`,
|
|
755
|
+
tool: toolName,
|
|
756
|
+
arguments: argumentsPayload
|
|
757
|
+
});
|
|
758
|
+
};
|
|
759
|
+
const directCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : void 0;
|
|
760
|
+
if (directCalls) {
|
|
761
|
+
directCalls.forEach((call) => addCall(call));
|
|
762
|
+
}
|
|
763
|
+
return sections;
|
|
764
|
+
}
|
|
765
|
+
function convertMessageListToSections(messageList) {
|
|
766
|
+
if (!Array.isArray(messageList) || messageList.length === 0) return void 0;
|
|
767
|
+
const sections = [];
|
|
768
|
+
const roleCounts = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const message of messageList) {
|
|
770
|
+
if (!message) continue;
|
|
771
|
+
const toolSections = collectToolCallSectionsFromMessage(message);
|
|
772
|
+
if (toolSections.length > 0) {
|
|
773
|
+
sections.push(...toolSections);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const role = typeof message?.role === "string" ? message.role : "assistant";
|
|
777
|
+
const next = (roleCounts.get(role) || 0) + 1;
|
|
778
|
+
roleCounts.set(role, next);
|
|
779
|
+
let content = toDisplayString(message?.content ?? "");
|
|
780
|
+
if (!content || !content.trim().length) {
|
|
781
|
+
if (toolSections.length > 0) {
|
|
782
|
+
content = "[tool call]";
|
|
783
|
+
} else if (typeof message?.content === "undefined" || message?.content === null) {
|
|
784
|
+
continue;
|
|
785
|
+
} else {
|
|
786
|
+
content = toDisplayString(sanitizeTracePayload(message?.content));
|
|
787
|
+
if (!content || !content.trim().length) continue;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
sections.push({
|
|
791
|
+
kind: "message",
|
|
792
|
+
label: buildMessageLabel(role, next),
|
|
793
|
+
role,
|
|
794
|
+
content,
|
|
795
|
+
metadata: typeof message?.name === "string" ? { name: message.name } : void 0
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
return sections.length > 0 ? sections : void 0;
|
|
799
|
+
}
|
|
800
|
+
function createTraceSession(opts) {
|
|
801
|
+
const cfg = withDefaults(opts.tracing);
|
|
802
|
+
if (!cfg.enabled) return void 0;
|
|
803
|
+
const sessionId = generateSessionId();
|
|
804
|
+
const runtime = {
|
|
805
|
+
sessionId,
|
|
806
|
+
startedAt: Date.now(),
|
|
807
|
+
resolvedConfig: cfg,
|
|
808
|
+
events: [],
|
|
809
|
+
summary: createEmptySummary(),
|
|
810
|
+
status: "in_progress",
|
|
811
|
+
errors: []
|
|
812
|
+
};
|
|
813
|
+
if (cfg.sink.type === "file") {
|
|
814
|
+
const baseDir = cfg.sink.baseDir;
|
|
815
|
+
ensureDir(baseDir);
|
|
816
|
+
const sessionDir = path.join(baseDir, sessionId);
|
|
817
|
+
ensureDir(sessionDir);
|
|
818
|
+
runtime.fileBaseDir = baseDir;
|
|
819
|
+
runtime.fileSessionDir = sessionDir;
|
|
820
|
+
}
|
|
821
|
+
return runtime;
|
|
822
|
+
}
|
|
823
|
+
function recordTraceEvent(session, event) {
|
|
824
|
+
if (!session) return void 0;
|
|
825
|
+
const sequence = session.events.length + 1;
|
|
826
|
+
const id = generateEventId(sequence);
|
|
827
|
+
const timestampIso = event.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
828
|
+
const status = event.status ?? "success";
|
|
829
|
+
const baseLabel = event.label?.trim().length ? event.label.trim() : defaultEventLabel(event.type);
|
|
830
|
+
const eventLabel = `${baseLabel} #${sequence}`;
|
|
831
|
+
const durationMs = event.durationMs !== void 0 ? Number(event.durationMs) : void 0;
|
|
832
|
+
if (!Number.isNaN(durationMs ?? NaN) && durationMs !== void 0) {
|
|
833
|
+
session.summary.totalDurationMs += durationMs;
|
|
834
|
+
}
|
|
835
|
+
const inputTokens = event.inputTokens !== void 0 ? Number(event.inputTokens) : void 0;
|
|
836
|
+
if (!Number.isNaN(inputTokens ?? NaN) && inputTokens !== void 0) {
|
|
837
|
+
session.summary.totalInputTokens += inputTokens;
|
|
838
|
+
}
|
|
839
|
+
const outputTokens = event.outputTokens !== void 0 ? Number(event.outputTokens) : void 0;
|
|
840
|
+
if (!Number.isNaN(outputTokens ?? NaN) && outputTokens !== void 0) {
|
|
841
|
+
session.summary.totalOutputTokens += outputTokens;
|
|
842
|
+
}
|
|
843
|
+
const cachedInputTokens = event.cachedInputTokens !== void 0 ? Number(event.cachedInputTokens) : void 0;
|
|
844
|
+
if (!Number.isNaN(cachedInputTokens ?? NaN) && cachedInputTokens !== void 0) {
|
|
845
|
+
session.summary.totalCachedInputTokens += cachedInputTokens;
|
|
846
|
+
}
|
|
847
|
+
const totalTokens = event.totalTokens !== void 0 ? Number(event.totalTokens) : void 0;
|
|
848
|
+
const requestBytes = event.requestBytes !== void 0 ? Number(event.requestBytes) : void 0;
|
|
849
|
+
if (!Number.isNaN(requestBytes ?? NaN) && requestBytes !== void 0) {
|
|
850
|
+
session.summary.totalBytesIn += requestBytes;
|
|
851
|
+
}
|
|
852
|
+
const responseBytes = event.responseBytes !== void 0 ? Number(event.responseBytes) : void 0;
|
|
853
|
+
if (!Number.isNaN(responseBytes ?? NaN) && responseBytes !== void 0) {
|
|
854
|
+
session.summary.totalBytesOut += responseBytes;
|
|
855
|
+
}
|
|
856
|
+
session.summary.eventCounts[event.type] = (session.summary.eventCounts[event.type] || 0) + 1;
|
|
857
|
+
let sections;
|
|
858
|
+
if (session.resolvedConfig.logData) {
|
|
859
|
+
const converted = convertMessageListToSections(event.messageList);
|
|
860
|
+
sections = ensureUniqueSections(id, converted);
|
|
861
|
+
}
|
|
862
|
+
const resolvedModel = event.model ?? inferModelFromMessages(event.messageList);
|
|
863
|
+
const resolvedProvider = event.provider ?? inferProviderFromMessages(event.messageList);
|
|
864
|
+
const isAiCall = event.type === "ai_call";
|
|
865
|
+
const record = {
|
|
866
|
+
sessionId: session.sessionId,
|
|
867
|
+
id,
|
|
868
|
+
type: event.type,
|
|
869
|
+
label: eventLabel,
|
|
870
|
+
sequence,
|
|
871
|
+
timestamp: timestampIso,
|
|
872
|
+
actor: event.actor,
|
|
873
|
+
status,
|
|
874
|
+
durationMs,
|
|
875
|
+
...isAiCall ? {
|
|
876
|
+
inputTokens: inputTokens ?? 0,
|
|
877
|
+
outputTokens: outputTokens ?? 0,
|
|
878
|
+
totalTokens: totalTokens ?? 0,
|
|
879
|
+
cachedInputTokens: cachedInputTokens ?? 0
|
|
880
|
+
} : {
|
|
881
|
+
...inputTokens !== void 0 && { inputTokens },
|
|
882
|
+
...outputTokens !== void 0 && { outputTokens },
|
|
883
|
+
...totalTokens !== void 0 && { totalTokens },
|
|
884
|
+
...cachedInputTokens !== void 0 && { cachedInputTokens }
|
|
885
|
+
},
|
|
886
|
+
requestBytes,
|
|
887
|
+
responseBytes,
|
|
888
|
+
model: resolvedModel,
|
|
889
|
+
provider: resolvedProvider,
|
|
890
|
+
toolExecutionId: event.toolExecutionId,
|
|
891
|
+
retryOf: event.retryOf,
|
|
892
|
+
error: event.error ?? (status === "error" ? { message: "Unknown error" } : void 0) ?? void 0,
|
|
893
|
+
data: sections ? { sections } : void 0,
|
|
894
|
+
debug: event.debug
|
|
895
|
+
};
|
|
896
|
+
if (status === "error") {
|
|
897
|
+
const errorInfo = event.error ?? { message: "Unknown error" };
|
|
898
|
+
session.errors.push({
|
|
899
|
+
eventId: id,
|
|
900
|
+
message: errorInfo.message,
|
|
901
|
+
stack: errorInfo.stack,
|
|
902
|
+
type: event.type,
|
|
903
|
+
timestamp: timestampIso
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
session.events.push(record);
|
|
907
|
+
const sink = session.resolvedConfig.sink;
|
|
908
|
+
if (sink.type === "custom" && typeof sink.onEvent === "function") {
|
|
909
|
+
try {
|
|
910
|
+
const cloned = JSON.parse(JSON.stringify(record));
|
|
911
|
+
const result = sink.onEvent(cloned);
|
|
912
|
+
if (result && typeof result.then === "function") {
|
|
913
|
+
result.then(void 0, (error) => {
|
|
914
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
915
|
+
console.warn("Trace custom sink onEvent rejected", error);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
921
|
+
console.warn("Trace custom sink onEvent failed", err);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return record;
|
|
926
|
+
}
|
|
927
|
+
async function finalizeTraceSession(session, params = {}) {
|
|
928
|
+
if (!session) return void 0;
|
|
929
|
+
const endedAtMs = Date.now();
|
|
930
|
+
const endedAtIso = new Date(endedAtMs).toISOString();
|
|
931
|
+
const durationMs = endedAtMs - session.startedAt;
|
|
932
|
+
const startedAtIso = new Date(session.startedAt).toISOString();
|
|
933
|
+
const agentInfo = params.agentRuntime ? {
|
|
934
|
+
name: params.agentRuntime.name,
|
|
935
|
+
version: params.agentRuntime.version,
|
|
936
|
+
model: getModelName(params.agentRuntime.model),
|
|
937
|
+
provider: getProviderName(params.agentRuntime.model)
|
|
938
|
+
} : void 0;
|
|
939
|
+
if (params.error) {
|
|
940
|
+
session.errors.push({
|
|
941
|
+
eventId: "session",
|
|
942
|
+
message: params.error.message || "Unknown error",
|
|
943
|
+
stack: params.error.stack,
|
|
944
|
+
type: "session",
|
|
945
|
+
timestamp: endedAtIso
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
const configSnapshot = buildConfigSnapshot(session);
|
|
949
|
+
const initialStatus = params.status ? params.status : session.errors.length > 0 ? "error" : "success";
|
|
950
|
+
const buildPayload = (status2) => ({
|
|
951
|
+
sessionId: session.sessionId,
|
|
952
|
+
startedAt: startedAtIso,
|
|
953
|
+
endedAt: endedAtIso,
|
|
954
|
+
durationMs,
|
|
955
|
+
agent: agentInfo,
|
|
956
|
+
config: configSnapshot,
|
|
957
|
+
summary: session.summary,
|
|
958
|
+
events: session.events,
|
|
959
|
+
status: status2,
|
|
960
|
+
errors: session.errors
|
|
961
|
+
});
|
|
962
|
+
const payloadForSink = buildPayload(initialStatus);
|
|
963
|
+
let sinkFailed = false;
|
|
964
|
+
const sink = session.resolvedConfig.sink;
|
|
965
|
+
if (sink.type === "cognipeer" || sink.type === "http") {
|
|
966
|
+
try {
|
|
967
|
+
const headers = sink.type === "cognipeer" ? { Authorization: `Bearer ${sink.apiKey}` } : sink.headers;
|
|
968
|
+
await postTraceSession(sink.url, headers, payloadForSink);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
sinkFailed = true;
|
|
971
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
972
|
+
session.errors.push({
|
|
973
|
+
eventId: "sink",
|
|
974
|
+
message,
|
|
975
|
+
type: "sink",
|
|
976
|
+
timestamp: endedAtIso
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const hadNonSinkErrors = session.errors.some((error) => error.type && error.type !== "sink");
|
|
981
|
+
let status = params.status ?? (hadNonSinkErrors ? "error" : "success");
|
|
982
|
+
if (status === "success" && sinkFailed) {
|
|
983
|
+
status = "partial";
|
|
984
|
+
}
|
|
985
|
+
let finalPayload = buildPayload(status);
|
|
986
|
+
if (sink.type === "file" && session.fileSessionDir) {
|
|
987
|
+
try {
|
|
988
|
+
const filePath = path.join(session.fileSessionDir, "trace.session.json");
|
|
989
|
+
await fs.promises.writeFile(filePath, JSON.stringify(finalPayload, null, 2), "utf8");
|
|
990
|
+
} catch (err) {
|
|
991
|
+
sinkFailed = true;
|
|
992
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
993
|
+
session.errors.push({
|
|
994
|
+
eventId: "sink",
|
|
995
|
+
message,
|
|
996
|
+
type: "sink",
|
|
997
|
+
timestamp: endedAtIso
|
|
998
|
+
});
|
|
999
|
+
if (status === "success") {
|
|
1000
|
+
status = "partial";
|
|
1001
|
+
finalPayload = buildPayload(status);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (sink.type === "custom" && typeof sink.onSession === "function") {
|
|
1006
|
+
try {
|
|
1007
|
+
const cloned = JSON.parse(JSON.stringify(finalPayload));
|
|
1008
|
+
await sink.onSession(cloned);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
sinkFailed = true;
|
|
1011
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1012
|
+
session.errors.push({
|
|
1013
|
+
eventId: "sink",
|
|
1014
|
+
message,
|
|
1015
|
+
type: "sink",
|
|
1016
|
+
timestamp: endedAtIso
|
|
1017
|
+
});
|
|
1018
|
+
if (status === "success") {
|
|
1019
|
+
status = "partial";
|
|
1020
|
+
finalPayload = buildPayload(status);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (!params.status) {
|
|
1025
|
+
const finalHadNonSinkErrors = session.errors.some((error) => error.type && error.type !== "sink");
|
|
1026
|
+
const hasSinkErrors = session.errors.some((error) => error.type === "sink");
|
|
1027
|
+
let computedStatus = finalHadNonSinkErrors ? "error" : "success";
|
|
1028
|
+
if (computedStatus === "success" && hasSinkErrors) {
|
|
1029
|
+
computedStatus = "partial";
|
|
1030
|
+
}
|
|
1031
|
+
if (computedStatus !== status) {
|
|
1032
|
+
status = computedStatus;
|
|
1033
|
+
finalPayload = buildPayload(status);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return finalPayload;
|
|
1037
|
+
}
|
|
1038
|
+
function sanitizeTracePayload(value) {
|
|
1039
|
+
try {
|
|
1040
|
+
const cache = /* @__PURE__ */ new WeakSet();
|
|
1041
|
+
const json = JSON.stringify(
|
|
1042
|
+
value,
|
|
1043
|
+
(_key, val) => {
|
|
1044
|
+
if (typeof val === "function") return `[Function ${val.name || "anonymous"}]`;
|
|
1045
|
+
if (typeof val === "bigint") return val.toString();
|
|
1046
|
+
if (val instanceof Error) return { message: val.message, stack: val.stack };
|
|
1047
|
+
if (val && typeof val === "object") {
|
|
1048
|
+
if (cache.has(val)) return "[Circular]";
|
|
1049
|
+
cache.add(val);
|
|
1050
|
+
}
|
|
1051
|
+
return val;
|
|
1052
|
+
},
|
|
1053
|
+
2
|
|
1054
|
+
);
|
|
1055
|
+
return JSON.parse(json);
|
|
1056
|
+
} catch {
|
|
1057
|
+
try {
|
|
1058
|
+
return typeof value === "string" ? value : String(value);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return "[Unserializable]";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function estimatePayloadBytes(value) {
|
|
1065
|
+
try {
|
|
1066
|
+
const json = JSON.stringify(value);
|
|
1067
|
+
return Buffer.byteLength(json ?? "", "utf8");
|
|
1068
|
+
} catch {
|
|
1069
|
+
try {
|
|
1070
|
+
return Buffer.byteLength(String(value ?? ""), "utf8");
|
|
1071
|
+
} catch {
|
|
1072
|
+
return 0;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/nodes/agentCore.ts
|
|
1078
|
+
function createAgentCoreNode(opts) {
|
|
1079
|
+
return async (state) => {
|
|
1080
|
+
const runtime = state.agent || {
|
|
1081
|
+
name: opts.name,
|
|
1082
|
+
version: opts.version,
|
|
1083
|
+
model: opts.model,
|
|
1084
|
+
tools: opts.tools || [],
|
|
1085
|
+
guardrails: opts.guardrails,
|
|
1086
|
+
limits: opts.limits,
|
|
1087
|
+
outputSchema: opts.outputSchema,
|
|
1088
|
+
tracing: opts.tracing
|
|
1089
|
+
};
|
|
1090
|
+
const tools = runtime.tools ?? [];
|
|
1091
|
+
const modelWithTools = runtime.model?.bindTools ? runtime.model.bindTools(tools) : runtime.model;
|
|
1092
|
+
const traceSession = state.ctx?.__traceSession;
|
|
1093
|
+
const actorName = runtime.name ?? opts.name ?? "agent";
|
|
1094
|
+
const actorVersion = runtime.version ?? opts.version;
|
|
1095
|
+
const start = Date.now();
|
|
1096
|
+
const shouldLogPrompt = !!traceSession && traceSession.resolvedConfig.logData;
|
|
1097
|
+
const promptPayload = shouldLogPrompt ? sanitizeTracePayload(state.messages) : void 0;
|
|
1098
|
+
const promptBytes = promptPayload !== void 0 ? estimatePayloadBytes(promptPayload) : void 0;
|
|
1099
|
+
let response;
|
|
1100
|
+
try {
|
|
1101
|
+
response = await modelWithTools.invoke([...state.messages]);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
const durationMs2 = Date.now() - start;
|
|
1104
|
+
recordTraceEvent(traceSession, {
|
|
1105
|
+
type: "ai_call",
|
|
1106
|
+
label: "Assistant Error",
|
|
1107
|
+
actor: { scope: "agent", name: actorName, role: "assistant", version: actorVersion },
|
|
1108
|
+
status: "error",
|
|
1109
|
+
durationMs: durationMs2,
|
|
1110
|
+
requestBytes: promptBytes,
|
|
1111
|
+
model: getModelName(runtime.model || opts.model),
|
|
1112
|
+
provider: getProviderName(runtime.model || opts.model),
|
|
1113
|
+
error: { message: err?.message || String(err), stack: err?.stack },
|
|
1114
|
+
messageList: state.messages
|
|
1115
|
+
});
|
|
1116
|
+
throw err;
|
|
1117
|
+
}
|
|
1118
|
+
const messagesWithResponse = [
|
|
1119
|
+
...state.messages,
|
|
1120
|
+
response
|
|
1121
|
+
];
|
|
1122
|
+
const rawUsage = response?.usage || response?.response_metadata?.token_usage || response?.response_metadata?.usage;
|
|
1123
|
+
const normalized = normalizeUsage(rawUsage);
|
|
1124
|
+
const modelName = getModelName(runtime.model || opts.model) || "unknown_model";
|
|
1125
|
+
const durationMs = Date.now() - start;
|
|
1126
|
+
const shouldLogResponse = !!traceSession && traceSession.resolvedConfig.logData;
|
|
1127
|
+
const responsePayload = shouldLogResponse ? sanitizeTracePayload(response) : void 0;
|
|
1128
|
+
const responseBytes = responsePayload !== void 0 ? estimatePayloadBytes(responsePayload) : void 0;
|
|
1129
|
+
recordTraceEvent(traceSession, {
|
|
1130
|
+
type: "ai_call",
|
|
1131
|
+
label: "Assistant Response",
|
|
1132
|
+
actor: { scope: "agent", name: actorName, role: "assistant", version: actorVersion },
|
|
1133
|
+
durationMs,
|
|
1134
|
+
inputTokens: normalized?.prompt_tokens,
|
|
1135
|
+
outputTokens: normalized?.completion_tokens,
|
|
1136
|
+
totalTokens: normalized?.total_tokens,
|
|
1137
|
+
cachedInputTokens: normalized?.prompt_tokens_details?.cached_tokens,
|
|
1138
|
+
requestBytes: promptBytes,
|
|
1139
|
+
responseBytes,
|
|
1140
|
+
model: modelName,
|
|
1141
|
+
provider: getProviderName(runtime.model || opts.model),
|
|
1142
|
+
messageList: messagesWithResponse
|
|
1143
|
+
});
|
|
1144
|
+
if (normalized) {
|
|
1145
|
+
const usageState = state.usage || { perRequest: [], totals: {} };
|
|
1146
|
+
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1147
|
+
const turn = usageState.perRequest.length + 1;
|
|
1148
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1149
|
+
const cachedInputTok = normalized.prompt_tokens_details?.cached_tokens;
|
|
1150
|
+
usageState.perRequest.push({ id, modelName, usage: normalized, timestamp, turn, cachedInput: cachedInputTok });
|
|
1151
|
+
const inputTok = normalized.prompt_tokens;
|
|
1152
|
+
const outputTok = normalized.completion_tokens;
|
|
1153
|
+
const totalTok = normalized.total_tokens;
|
|
1154
|
+
const key = modelName;
|
|
1155
|
+
const agg = usageState.totals[key] || { input: 0, output: 0, total: 0, cachedInput: 0 };
|
|
1156
|
+
usageState.totals[key] = {
|
|
1157
|
+
input: agg.input + (Number(inputTok) || 0),
|
|
1158
|
+
output: agg.output + (Number(outputTok) || 0),
|
|
1159
|
+
total: agg.total + (Number(totalTok) || 0),
|
|
1160
|
+
cachedInput: agg.cachedInput + (Number(cachedInputTok) || 0)
|
|
1161
|
+
};
|
|
1162
|
+
state.usage = usageState;
|
|
1163
|
+
}
|
|
1164
|
+
return { messages: messagesWithResponse, usage: state.usage };
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function createToolsNode(initialTools, opts) {
|
|
1168
|
+
const baseToolByName = /* @__PURE__ */ new Map();
|
|
1169
|
+
for (const t of initialTools) baseToolByName.set(t.name, t);
|
|
1170
|
+
return async (state) => {
|
|
1171
|
+
const runtime = state.agent || {
|
|
1172
|
+
name: opts?.name,
|
|
1173
|
+
model: opts?.model,
|
|
1174
|
+
tools: initialTools,
|
|
1175
|
+
limits: opts?.limits,
|
|
1176
|
+
systemPrompt: opts?.systemPrompt,
|
|
1177
|
+
useTodoList: opts?.useTodoList,
|
|
1178
|
+
outputSchema: opts?.outputSchema
|
|
1179
|
+
};
|
|
1180
|
+
const activeTools = runtime.tools;
|
|
1181
|
+
const toolByName = /* @__PURE__ */ new Map();
|
|
1182
|
+
for (const t of activeTools) toolByName.set(t.name, t);
|
|
1183
|
+
const limits = {
|
|
1184
|
+
maxToolCalls: runtime.limits?.maxToolCalls ?? 10,
|
|
1185
|
+
maxParallelTools: Math.max(1, runtime.limits?.maxParallelTools ?? 1)
|
|
1186
|
+
};
|
|
1187
|
+
const appended = [];
|
|
1188
|
+
const onEvent = state.ctx?.__onEvent;
|
|
1189
|
+
const traceSession = state.ctx?.__traceSession;
|
|
1190
|
+
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals.map((entry) => ({ ...entry })) : [];
|
|
1191
|
+
const pendingByCallId = new Map(pendingApprovals.map((entry) => [entry.toolCallId, entry]));
|
|
1192
|
+
let awaitingApproval = false;
|
|
1193
|
+
for (const t of toolByName.values()) {
|
|
1194
|
+
const anyT = t;
|
|
1195
|
+
if (anyT._stateRef && typeof anyT._stateRef === "object") {
|
|
1196
|
+
anyT._stateRef.toolHistory = state.toolHistory;
|
|
1197
|
+
anyT._stateRef.toolHistoryArchived = state.toolHistoryArchived;
|
|
1198
|
+
anyT._stateRef.__onEvent = onEvent;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const last = state.messages[state.messages.length - 1];
|
|
1202
|
+
let toolCount = state.toolCallCount || 0;
|
|
1203
|
+
const toolCalls = Array.isArray(
|
|
1204
|
+
last?.tool_calls
|
|
1205
|
+
) ? last.tool_calls : [];
|
|
1206
|
+
const toolHistory = state.toolHistory || [];
|
|
1207
|
+
const remaining = Math.max(0, limits.maxToolCalls - toolCount);
|
|
1208
|
+
const planned = toolCalls.slice(0, remaining);
|
|
1209
|
+
const skipped = toolCalls.slice(remaining);
|
|
1210
|
+
for (const tc of skipped) {
|
|
1211
|
+
onEvent?.({ type: "tool_call", phase: "skipped", name: tc.name, id: tc.id, args: tc.args });
|
|
1212
|
+
const sanitizedArgs = sanitizeTracePayload(tc.args);
|
|
1213
|
+
const messageList = [
|
|
1214
|
+
{
|
|
1215
|
+
role: "assistant",
|
|
1216
|
+
name: tc.name,
|
|
1217
|
+
content: `Skipped tool due to tool-call limit: ${tc.name}`,
|
|
1218
|
+
tool_calls: [
|
|
1219
|
+
{
|
|
1220
|
+
id: tc.id,
|
|
1221
|
+
type: "function",
|
|
1222
|
+
function: {
|
|
1223
|
+
name: tc.name,
|
|
1224
|
+
arguments: sanitizedArgs
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
}
|
|
1229
|
+
];
|
|
1230
|
+
recordTraceEvent(traceSession, {
|
|
1231
|
+
type: "tool_call",
|
|
1232
|
+
label: `Tool Skipped - ${tc.name}`,
|
|
1233
|
+
actor: { scope: "tool", name: tc.name, role: "tool" },
|
|
1234
|
+
status: "skipped",
|
|
1235
|
+
toolExecutionId: tc.id,
|
|
1236
|
+
messageList
|
|
1237
|
+
});
|
|
1238
|
+
appended.push({
|
|
1239
|
+
role: "tool",
|
|
1240
|
+
content: `Skipped tool due to tool-call limit: ${tc.name}`,
|
|
1241
|
+
tool_call_id: tc.id || `${tc.name}_${appended.length}`,
|
|
1242
|
+
name: tc.name
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const runOne = async (tc) => {
|
|
1246
|
+
const t = toolByName.get(tc.name);
|
|
1247
|
+
if (!t) {
|
|
1248
|
+
onEvent?.({ type: "tool_call", phase: "error", name: tc.name, id: tc.id, args: tc.args, error: { message: "Tool not found" } });
|
|
1249
|
+
appended.push({
|
|
1250
|
+
role: "tool",
|
|
1251
|
+
content: `Tool not found: ${tc.name}`,
|
|
1252
|
+
tool_call_id: tc.id || `${tc.name}_${appended.length}`,
|
|
1253
|
+
name: tc.name
|
|
1254
|
+
});
|
|
1255
|
+
toolCount += 1;
|
|
1256
|
+
return { status: "error" };
|
|
1257
|
+
}
|
|
1258
|
+
let args = tc.args;
|
|
1259
|
+
if (typeof args === "string") {
|
|
1260
|
+
try {
|
|
1261
|
+
args = JSON.parse(args);
|
|
1262
|
+
} catch (_) {
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const toolCallId = tc.id || `${tc.name}_${toolCount + 1}`;
|
|
1266
|
+
let approvalEntry = pendingByCallId.get(toolCallId);
|
|
1267
|
+
const needsApproval = Boolean(t.needsApproval);
|
|
1268
|
+
if (needsApproval) {
|
|
1269
|
+
if (!approvalEntry) {
|
|
1270
|
+
approvalEntry = {
|
|
1271
|
+
id: nanoid(),
|
|
1272
|
+
toolCallId,
|
|
1273
|
+
toolName: t.name || tc.name,
|
|
1274
|
+
args,
|
|
1275
|
+
status: "pending",
|
|
1276
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1277
|
+
metadata: t.approvalPrompt || t.approvalDefaults ? { prompt: t.approvalPrompt, defaults: t.approvalDefaults } : void 0
|
|
1278
|
+
};
|
|
1279
|
+
pendingApprovals.push(approvalEntry);
|
|
1280
|
+
pendingByCallId.set(toolCallId, approvalEntry);
|
|
1281
|
+
onEvent?.({ type: "tool_approval", status: "pending", id: approvalEntry.id, toolName: approvalEntry.toolName, toolCallId: approvalEntry.toolCallId, args });
|
|
1282
|
+
} else if (!approvalEntry.args) {
|
|
1283
|
+
approvalEntry.args = args;
|
|
1284
|
+
}
|
|
1285
|
+
if (approvalEntry.status === "pending") {
|
|
1286
|
+
awaitingApproval = true;
|
|
1287
|
+
const ctx = state.ctx = state.ctx || {};
|
|
1288
|
+
ctx.__awaitingApproval = {
|
|
1289
|
+
approvalId: approvalEntry.id,
|
|
1290
|
+
toolCallId: approvalEntry.toolCallId,
|
|
1291
|
+
toolName: approvalEntry.toolName,
|
|
1292
|
+
requestedAt: approvalEntry.requestedAt
|
|
1293
|
+
};
|
|
1294
|
+
ctx.__resumeStage = "tools";
|
|
1295
|
+
return { status: "awaiting_approval", approval: approvalEntry };
|
|
1296
|
+
}
|
|
1297
|
+
if (approvalEntry.status === "rejected") {
|
|
1298
|
+
const rejectionMessage = approvalEntry.comment || "Tool call rejected by reviewer.";
|
|
1299
|
+
appended.push({
|
|
1300
|
+
role: "tool",
|
|
1301
|
+
content: `Tool call rejected: ${rejectionMessage}`,
|
|
1302
|
+
tool_call_id: toolCallId,
|
|
1303
|
+
name: tc.name
|
|
1304
|
+
});
|
|
1305
|
+
onEvent?.({ type: "tool_approval", status: "rejected", id: approvalEntry.id, toolName: approvalEntry.toolName, toolCallId: approvalEntry.toolCallId, comment: approvalEntry.comment, decidedBy: approvalEntry.decidedBy });
|
|
1306
|
+
approvalEntry.metadata = { ...approvalEntry.metadata || {}, resolution: "rejected" };
|
|
1307
|
+
approvalEntry.status = "executed";
|
|
1308
|
+
approvalEntry.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1309
|
+
toolHistory.push({ executionId: nanoid(), toolName: t.name, args, output: `Rejected: ${rejectionMessage}`, rawOutput: null, timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool_call_id: tc.id });
|
|
1310
|
+
pendingByCallId.set(toolCallId, approvalEntry);
|
|
1311
|
+
toolCount += 1;
|
|
1312
|
+
return { status: "rejected", approval: approvalEntry };
|
|
1313
|
+
}
|
|
1314
|
+
if (approvalEntry.status === "approved") {
|
|
1315
|
+
if (approvalEntry.approvedArgs !== void 0) {
|
|
1316
|
+
args = approvalEntry.approvedArgs;
|
|
1317
|
+
}
|
|
1318
|
+
onEvent?.({ type: "tool_approval", status: "approved", id: approvalEntry.id, toolName: approvalEntry.toolName, toolCallId: approvalEntry.toolCallId, decidedBy: approvalEntry.decidedBy, comment: approvalEntry.comment });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const start = Date.now();
|
|
1322
|
+
try {
|
|
1323
|
+
onEvent?.({ type: "tool_call", phase: "start", name: t.name, id: tc.id, args });
|
|
1324
|
+
const sanitizedArgs = sanitizeTracePayload(args);
|
|
1325
|
+
const inputBytes = traceSession?.resolvedConfig.logData ? estimatePayloadBytes(sanitizedArgs) : void 0;
|
|
1326
|
+
let output;
|
|
1327
|
+
const anyTool = t;
|
|
1328
|
+
if (typeof anyTool.func === "function") output = await anyTool.func(args);
|
|
1329
|
+
else if (typeof anyTool.invoke === "function") output = await anyTool.invoke(args);
|
|
1330
|
+
else if (typeof anyTool.call === "function") output = await anyTool.call(args);
|
|
1331
|
+
else if (typeof anyTool._call === "function") output = await anyTool._call(args);
|
|
1332
|
+
else if (typeof anyTool.run === "function") output = await anyTool.run(args);
|
|
1333
|
+
else throw new Error("Tool is not invokable");
|
|
1334
|
+
if (output && typeof output === "object" && output.__handoff && output.__handoff.runtime) {
|
|
1335
|
+
const newRuntime = output.__handoff.runtime;
|
|
1336
|
+
const executionId2 = nanoid();
|
|
1337
|
+
toolHistory.push({ executionId: executionId2, toolName: t.name, args, output: "handoff:ok", rawOutput: output, timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool_call_id: tc.id });
|
|
1338
|
+
appended.push({ role: "tool", content: "ok", tool_call_id: tc.id || `${tc.name}_${appended.length}`, name: tc.name });
|
|
1339
|
+
onEvent?.({ type: "handoff", from: runtime.name, to: newRuntime.name, toolName: t.name });
|
|
1340
|
+
state.agent = newRuntime;
|
|
1341
|
+
onEvent?.({ type: "tool_call", phase: "success", name: t.name, id: tc.id, args, result: "handoff", durationMs: Date.now() - start });
|
|
1342
|
+
toolCount += 1;
|
|
1343
|
+
if (needsApproval && approvalEntry) {
|
|
1344
|
+
approvalEntry.status = "executed";
|
|
1345
|
+
approvalEntry.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1346
|
+
approvalEntry.executionId = executionId2;
|
|
1347
|
+
pendingByCallId.set(toolCallId, approvalEntry);
|
|
1348
|
+
}
|
|
1349
|
+
return { status: "success", approval: approvalEntry };
|
|
1350
|
+
}
|
|
1351
|
+
const content = typeof output === "string" ? output : JSON.stringify(output);
|
|
1352
|
+
if (output && typeof output === "object" && output.__finalStructuredOutput) {
|
|
1353
|
+
if (!state.ctx) state.ctx = {};
|
|
1354
|
+
state.ctx.__structuredOutputParsed = output.data;
|
|
1355
|
+
state.ctx.__finalizedDueToStructuredOutput = true;
|
|
1356
|
+
}
|
|
1357
|
+
const durationMs = Date.now() - start;
|
|
1358
|
+
const executionId = nanoid();
|
|
1359
|
+
toolHistory.push({ executionId, toolName: t.name, args, output, rawOutput: output, timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool_call_id: tc.id });
|
|
1360
|
+
appended.push({ role: "tool", content, tool_call_id: tc.id || `${tc.name}_${appended.length}`, name: tc.name });
|
|
1361
|
+
onEvent?.({ type: "tool_call", phase: "success", name: t.name, id: tc.id, args, result: output, durationMs });
|
|
1362
|
+
if (needsApproval && approvalEntry) {
|
|
1363
|
+
approvalEntry.status = "executed";
|
|
1364
|
+
approvalEntry.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1365
|
+
approvalEntry.executionId = executionId;
|
|
1366
|
+
pendingByCallId.set(toolCallId, approvalEntry);
|
|
1367
|
+
}
|
|
1368
|
+
const sanitizedOutput = sanitizeTracePayload(output);
|
|
1369
|
+
const outputBytes = traceSession?.resolvedConfig.logData ? estimatePayloadBytes(sanitizedOutput) : void 0;
|
|
1370
|
+
const toolName = t.name || tc.name;
|
|
1371
|
+
const messageList = [
|
|
1372
|
+
{
|
|
1373
|
+
role: "assistant",
|
|
1374
|
+
name: toolName,
|
|
1375
|
+
content: "",
|
|
1376
|
+
tool_calls: [
|
|
1377
|
+
{
|
|
1378
|
+
id: tc.id || executionId,
|
|
1379
|
+
type: "function",
|
|
1380
|
+
function: {
|
|
1381
|
+
name: toolName,
|
|
1382
|
+
arguments: sanitizedArgs
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
]
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
role: "tool",
|
|
1389
|
+
name: toolName,
|
|
1390
|
+
content: sanitizedOutput ?? output ?? ""
|
|
1391
|
+
}
|
|
1392
|
+
];
|
|
1393
|
+
recordTraceEvent(traceSession, {
|
|
1394
|
+
type: "tool_call",
|
|
1395
|
+
label: `Tool Execution - ${toolName}`,
|
|
1396
|
+
actor: { scope: "tool", name: toolName, role: "tool" },
|
|
1397
|
+
durationMs,
|
|
1398
|
+
requestBytes: inputBytes,
|
|
1399
|
+
responseBytes: outputBytes,
|
|
1400
|
+
toolExecutionId: executionId,
|
|
1401
|
+
messageList
|
|
1402
|
+
});
|
|
1403
|
+
toolCount += 1;
|
|
1404
|
+
return { status: "success", approval: approvalEntry };
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
const durationMs = Date.now() - start;
|
|
1407
|
+
const executionId = nanoid();
|
|
1408
|
+
toolHistory.push({ executionId, toolName: t.name, args, output: `Error executing tool: ${e?.message || String(e)}`, rawOutput: null, timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool_call_id: tc.id });
|
|
1409
|
+
appended.push({ role: "tool", content: `Error executing tool: ${e?.message || String(e)}`, tool_call_id: tc.id || `${tc.name}_${appended.length}`, name: tc.name });
|
|
1410
|
+
onEvent?.({ type: "tool_call", phase: "error", name: t.name, id: tc.id, args, error: { message: e?.message || String(e) } });
|
|
1411
|
+
const sanitizedArgs = sanitizeTracePayload(args);
|
|
1412
|
+
const inputBytes = traceSession?.resolvedConfig.logData ? estimatePayloadBytes(sanitizedArgs) : void 0;
|
|
1413
|
+
const toolName = t.name || tc.name;
|
|
1414
|
+
const errorMessage = e?.message || String(e);
|
|
1415
|
+
const messageList = [
|
|
1416
|
+
{
|
|
1417
|
+
role: "assistant",
|
|
1418
|
+
name: toolName,
|
|
1419
|
+
content: "",
|
|
1420
|
+
tool_calls: [
|
|
1421
|
+
{
|
|
1422
|
+
id: tc.id || executionId,
|
|
1423
|
+
type: "function",
|
|
1424
|
+
function: {
|
|
1425
|
+
name: toolName,
|
|
1426
|
+
arguments: sanitizedArgs
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
role: "tool",
|
|
1433
|
+
name: toolName,
|
|
1434
|
+
content: `Error executing tool: ${errorMessage}`
|
|
1435
|
+
}
|
|
1436
|
+
];
|
|
1437
|
+
recordTraceEvent(traceSession, {
|
|
1438
|
+
type: "tool_call",
|
|
1439
|
+
label: `Tool Error - ${toolName}`,
|
|
1440
|
+
actor: { scope: "tool", name: toolName, role: "tool" },
|
|
1441
|
+
status: "error",
|
|
1442
|
+
durationMs,
|
|
1443
|
+
requestBytes: inputBytes,
|
|
1444
|
+
toolExecutionId: executionId,
|
|
1445
|
+
error: { message: e?.message || String(e), stack: e?.stack },
|
|
1446
|
+
messageList
|
|
1447
|
+
});
|
|
1448
|
+
toolCount += 1;
|
|
1449
|
+
return { status: "error", approval: approvalEntry };
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
for (const tc of planned) {
|
|
1453
|
+
if (awaitingApproval) break;
|
|
1454
|
+
const result = await runOne(tc);
|
|
1455
|
+
if (result?.status === "awaiting_approval") {
|
|
1456
|
+
awaitingApproval = true;
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!awaitingApproval && state.ctx?.__awaitingApproval) {
|
|
1461
|
+
const ctx = { ...state.ctx };
|
|
1462
|
+
delete ctx.__awaitingApproval;
|
|
1463
|
+
if (!pendingApprovals.some((entry) => entry.status !== "executed")) {
|
|
1464
|
+
delete ctx.__resumeStage;
|
|
1465
|
+
}
|
|
1466
|
+
state.ctx = Object.keys(ctx).length > 0 ? ctx : void 0;
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
messages: [...state.messages, ...appended],
|
|
1470
|
+
toolCallCount: toolCount,
|
|
1471
|
+
toolHistory,
|
|
1472
|
+
agent: state.agent,
|
|
1473
|
+
pendingApprovals
|
|
1474
|
+
};
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// src/nodes/toolLimitFinalize.ts
|
|
1479
|
+
function createToolLimitFinalizeNode(_opts) {
|
|
1480
|
+
return async (state) => {
|
|
1481
|
+
const notice = { role: "system", content: "Tool-call limit reached. Produce the best possible final answer using the available context and prior tool outputs. Do not call any more tools." };
|
|
1482
|
+
const ctx = { ...state.ctx || {}, __finalizedDueToToolLimit: true };
|
|
1483
|
+
return { messages: [...state.messages, notice], ctx };
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function ensureArray(value) {
|
|
1487
|
+
return Array.isArray(value) ? value : [];
|
|
1488
|
+
}
|
|
1489
|
+
function buildContext(params) {
|
|
1490
|
+
const latest = params.state.messages[params.state.messages.length - 1];
|
|
1491
|
+
return {
|
|
1492
|
+
phase: params.phase,
|
|
1493
|
+
messages: params.state.messages,
|
|
1494
|
+
latestMessage: latest,
|
|
1495
|
+
state: params.state,
|
|
1496
|
+
runtime: params.runtime,
|
|
1497
|
+
options: params.options
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function normalizeDisposition(passed, disposition) {
|
|
1501
|
+
if (disposition) return disposition;
|
|
1502
|
+
return passed ? "allow" : "block";
|
|
1503
|
+
}
|
|
1504
|
+
async function evaluateGuardrails(params) {
|
|
1505
|
+
const incidents = [];
|
|
1506
|
+
const guardrails = ensureArray(params.guardrails).filter(
|
|
1507
|
+
(guardrail) => ensureArray(guardrail.appliesTo).includes(params.phase)
|
|
1508
|
+
);
|
|
1509
|
+
if (guardrails.length === 0) {
|
|
1510
|
+
return { ok: true, incidents };
|
|
1511
|
+
}
|
|
1512
|
+
const context = buildContext(params);
|
|
1513
|
+
for (const guardrail of guardrails) {
|
|
1514
|
+
for (const rule of ensureArray(guardrail.rules)) {
|
|
1515
|
+
let result;
|
|
1516
|
+
try {
|
|
1517
|
+
result = await rule.evaluate(context);
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
result = {
|
|
1520
|
+
passed: false,
|
|
1521
|
+
reason: err instanceof Error ? err.message : String(err ?? "Unknown error"),
|
|
1522
|
+
details: { thrown: true },
|
|
1523
|
+
disposition: "block"
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
const passed = result?.passed !== false;
|
|
1527
|
+
const disposition = normalizeDisposition(passed, result?.disposition);
|
|
1528
|
+
if (passed && disposition === "allow") {
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
const incident = {
|
|
1532
|
+
guardrailId: guardrail.id ?? rule.id ?? nanoid(8),
|
|
1533
|
+
guardrailTitle: guardrail.title,
|
|
1534
|
+
ruleId: rule.id,
|
|
1535
|
+
ruleTitle: rule.title,
|
|
1536
|
+
phase: params.phase,
|
|
1537
|
+
reason: result?.reason,
|
|
1538
|
+
details: result?.details,
|
|
1539
|
+
disposition
|
|
1540
|
+
};
|
|
1541
|
+
let effectiveDisposition = incident.disposition;
|
|
1542
|
+
if (typeof guardrail.onViolation === "function") {
|
|
1543
|
+
try {
|
|
1544
|
+
const override = await guardrail.onViolation(incident, context);
|
|
1545
|
+
if (override) {
|
|
1546
|
+
effectiveDisposition = override;
|
|
1547
|
+
incident.disposition = override;
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
effectiveDisposition = "block";
|
|
1551
|
+
incident.disposition = "block";
|
|
1552
|
+
incident.details = {
|
|
1553
|
+
...incident.details || {},
|
|
1554
|
+
onViolationError: err instanceof Error ? err.message : String(err ?? "Unknown error")
|
|
1555
|
+
};
|
|
1556
|
+
if (!incident.reason) {
|
|
1557
|
+
incident.reason = "Guardrail onViolation handler threw an error.";
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
incidents.push(incident);
|
|
1562
|
+
params.emit?.({
|
|
1563
|
+
type: "guardrail",
|
|
1564
|
+
phase: params.phase,
|
|
1565
|
+
guardrailId: incident.guardrailId,
|
|
1566
|
+
guardrailTitle: incident.guardrailTitle,
|
|
1567
|
+
ruleId: incident.ruleId,
|
|
1568
|
+
ruleTitle: incident.ruleTitle,
|
|
1569
|
+
disposition: effectiveDisposition,
|
|
1570
|
+
reason: incident.reason,
|
|
1571
|
+
details: incident.details
|
|
1572
|
+
});
|
|
1573
|
+
if (effectiveDisposition === "block" && guardrail.haltOnViolation !== false) {
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const ok = incidents.every((incident) => incident.disposition !== "block");
|
|
1579
|
+
return { ok, incidents };
|
|
1580
|
+
}
|
|
1581
|
+
function createGuardrail(options) {
|
|
1582
|
+
const appliesTo = options.appliesTo?.length ? options.appliesTo : ["request" /* Request */, "response" /* Response */];
|
|
1583
|
+
return {
|
|
1584
|
+
id: options.id,
|
|
1585
|
+
title: options.title,
|
|
1586
|
+
description: options.description,
|
|
1587
|
+
appliesTo,
|
|
1588
|
+
rules: options.checks,
|
|
1589
|
+
haltOnViolation: options.haltOnViolation,
|
|
1590
|
+
onViolation: options.onViolation,
|
|
1591
|
+
metadata: options.metadata
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/utils/stateSnapshot.ts
|
|
1596
|
+
var DISALLOWED_CTX_KEYS = /* @__PURE__ */ new Set(["__onEvent", "__traceSession", "__paused"]);
|
|
1597
|
+
var clone = (value) => {
|
|
1598
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
1599
|
+
return globalThis.structuredClone(value);
|
|
1600
|
+
}
|
|
1601
|
+
return JSON.parse(JSON.stringify(value));
|
|
1602
|
+
};
|
|
1603
|
+
var cleanCtx = (ctx) => {
|
|
1604
|
+
if (!ctx) return void 0;
|
|
1605
|
+
const entries = Object.entries(ctx).filter(([key]) => !DISALLOWED_CTX_KEYS.has(key));
|
|
1606
|
+
if (entries.length === 0) return void 0;
|
|
1607
|
+
return Object.fromEntries(entries);
|
|
1608
|
+
};
|
|
1609
|
+
var runtimeHintFromAgent = (agent) => {
|
|
1610
|
+
if (!agent) return void 0;
|
|
1611
|
+
const tools = Array.isArray(agent.tools) ? agent.tools.map((tool) => tool?.name).filter(Boolean) : void 0;
|
|
1612
|
+
return {
|
|
1613
|
+
name: agent.name,
|
|
1614
|
+
version: agent.version,
|
|
1615
|
+
tools: tools && tools.length > 0 ? tools : void 0
|
|
1616
|
+
};
|
|
1617
|
+
};
|
|
1618
|
+
function captureSnapshot(state, options) {
|
|
1619
|
+
const { agent, ctx, ...rest } = state;
|
|
1620
|
+
const metadata = {
|
|
1621
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1622
|
+
tag: options?.tag,
|
|
1623
|
+
paused: ctx?.__paused ?? null
|
|
1624
|
+
};
|
|
1625
|
+
const safeState = clone({
|
|
1626
|
+
...rest,
|
|
1627
|
+
ctx: cleanCtx(ctx)
|
|
1628
|
+
});
|
|
1629
|
+
if (safeState.ctx && "__paused" in safeState.ctx) {
|
|
1630
|
+
delete safeState.ctx.__paused;
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
state: safeState,
|
|
1634
|
+
runtimeHint: options?.includeRuntimeHint !== false ? runtimeHintFromAgent(agent) : void 0,
|
|
1635
|
+
metadata
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function restoreSnapshot(snapshot, restoreOptions) {
|
|
1639
|
+
const base = clone(snapshot.state);
|
|
1640
|
+
const incomingCtx = restoreOptions?.ctx;
|
|
1641
|
+
const mergedCtx = restoreOptions?.mergeCtx === false ? cleanCtx(incomingCtx) : {
|
|
1642
|
+
...base.ctx || {},
|
|
1643
|
+
...cleanCtx(incomingCtx)
|
|
1644
|
+
};
|
|
1645
|
+
const ctx = {
|
|
1646
|
+
...mergedCtx || {},
|
|
1647
|
+
__restoredFromSnapshot: true
|
|
1648
|
+
};
|
|
1649
|
+
const restored = {
|
|
1650
|
+
...base,
|
|
1651
|
+
ctx
|
|
1652
|
+
};
|
|
1653
|
+
if (restoreOptions?.agent) {
|
|
1654
|
+
restored.agent = restoreOptions.agent;
|
|
1655
|
+
}
|
|
1656
|
+
return restored;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/utils/toolApprovals.ts
|
|
1660
|
+
var cloneApprovals = (approvals) => Array.isArray(approvals) ? approvals.map((entry) => ({ ...entry })) : [];
|
|
1661
|
+
function resolveToolApprovalState(state, resolution) {
|
|
1662
|
+
const approvals = cloneApprovals(state.pendingApprovals);
|
|
1663
|
+
const matchIndex = approvals.findIndex((entry2) => entry2.id === resolution.id || entry2.toolCallId === resolution.id);
|
|
1664
|
+
if (matchIndex === -1) {
|
|
1665
|
+
throw new Error(`Pending approval not found for id: ${resolution.id}`);
|
|
1666
|
+
}
|
|
1667
|
+
const entry = { ...approvals[matchIndex] };
|
|
1668
|
+
if (entry.status === "executed") {
|
|
1669
|
+
throw new Error(`Tool approval ${entry.id} already completed.`);
|
|
1670
|
+
}
|
|
1671
|
+
entry.status = resolution.approved ? "approved" : "rejected";
|
|
1672
|
+
entry.approvedArgs = resolution.approved ? resolution.approvedArgs ?? entry.args : void 0;
|
|
1673
|
+
entry.decidedBy = resolution.decidedBy;
|
|
1674
|
+
entry.comment = resolution.comment;
|
|
1675
|
+
entry.decidedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1676
|
+
approvals[matchIndex] = entry;
|
|
1677
|
+
const ctx = { ...state.ctx || {} };
|
|
1678
|
+
delete ctx.__awaitingApproval;
|
|
1679
|
+
ctx.__resumeStage = "tools";
|
|
1680
|
+
ctx.__approvalResolved = { id: entry.id, status: entry.status };
|
|
1681
|
+
return {
|
|
1682
|
+
...state,
|
|
1683
|
+
pendingApprovals: approvals,
|
|
1684
|
+
ctx
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/agent.ts
|
|
1689
|
+
function createAgent(opts) {
|
|
1690
|
+
const resolver = createResolverNode();
|
|
1691
|
+
const agentCore = createAgentCoreNode(opts);
|
|
1692
|
+
const toolsBase = [...opts.tools ?? []];
|
|
1693
|
+
if (opts.outputSchema) {
|
|
1694
|
+
const responseTool = createTool({
|
|
1695
|
+
name: "response",
|
|
1696
|
+
description: "Finalize the answer by returning the final structured JSON matching the required schema. Call exactly once when you are fully done, then stop.",
|
|
1697
|
+
schema: opts.outputSchema,
|
|
1698
|
+
func: async (data) => ({ __finalStructuredOutput: true, data })
|
|
1699
|
+
});
|
|
1700
|
+
toolsBase.push(responseTool);
|
|
1701
|
+
}
|
|
1702
|
+
const toolsNode = createToolsNode(toolsBase, opts);
|
|
1703
|
+
const finalizeNode = createToolLimitFinalizeNode();
|
|
1704
|
+
const mergeGuardrailOutcomes = (prev, next) => {
|
|
1705
|
+
if (!prev) return next;
|
|
1706
|
+
return {
|
|
1707
|
+
ok: prev.ok && next.ok,
|
|
1708
|
+
incidents: [...prev.incidents, ...next.incidents]
|
|
1709
|
+
};
|
|
1710
|
+
};
|
|
1711
|
+
const ensureGuardrailStore = (state) => {
|
|
1712
|
+
const ctx = state.ctx = state.ctx || {};
|
|
1713
|
+
const existing = ctx.__guardrailStore || {
|
|
1714
|
+
lastRequestLength: -1,
|
|
1715
|
+
lastResponseLength: -1
|
|
1716
|
+
};
|
|
1717
|
+
ctx.__guardrailStore = existing;
|
|
1718
|
+
return existing;
|
|
1719
|
+
};
|
|
1720
|
+
const getGuardrailConfig = (state) => {
|
|
1721
|
+
const agentGuardrails = state.agent?.guardrails;
|
|
1722
|
+
return Array.isArray(agentGuardrails) ? agentGuardrails : Array.isArray(opts.guardrails) ? opts.guardrails : [];
|
|
1723
|
+
};
|
|
1724
|
+
const runtime = {
|
|
1725
|
+
name: opts.name,
|
|
1726
|
+
version: opts.version,
|
|
1727
|
+
model: opts.model,
|
|
1728
|
+
tools: toolsBase,
|
|
1729
|
+
guardrails: opts.guardrails,
|
|
1730
|
+
systemPrompt: void 0,
|
|
1731
|
+
limits: opts.limits,
|
|
1732
|
+
useTodoList: void 0,
|
|
1733
|
+
outputSchema: opts.outputSchema,
|
|
1734
|
+
tracing: opts.tracing
|
|
1735
|
+
};
|
|
1736
|
+
async function runLoop(initial, config, emit) {
|
|
1737
|
+
let state = await resolver(initial);
|
|
1738
|
+
if (state.ctx?.__paused) {
|
|
1739
|
+
const nextCtx = { ...state.ctx };
|
|
1740
|
+
delete nextCtx.__paused;
|
|
1741
|
+
state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
|
|
1742
|
+
}
|
|
1743
|
+
let resumeStage = null;
|
|
1744
|
+
if (state.ctx?.__resumeStage) {
|
|
1745
|
+
const nextCtx = { ...state.ctx };
|
|
1746
|
+
if (nextCtx.__resumeStage === "tools") {
|
|
1747
|
+
resumeStage = "tools";
|
|
1748
|
+
}
|
|
1749
|
+
delete nextCtx.__resumeStage;
|
|
1750
|
+
state = { ...state, ctx: Object.keys(nextCtx).length > 0 ? nextCtx : void 0 };
|
|
1751
|
+
}
|
|
1752
|
+
const maxToolCalls = opts.limits?.maxToolCalls ?? 10;
|
|
1753
|
+
const iterationLimit = Math.max(maxToolCalls * 3 + 10, 40);
|
|
1754
|
+
let iterations = 0;
|
|
1755
|
+
const onStateChange = config?.onStateChange;
|
|
1756
|
+
const checkpointReason = config?.checkpointReason;
|
|
1757
|
+
let pausedStage = null;
|
|
1758
|
+
const checkpointIfRequested = (stage) => {
|
|
1759
|
+
if (typeof onStateChange !== "function") return false;
|
|
1760
|
+
let result = false;
|
|
1761
|
+
try {
|
|
1762
|
+
result = onStateChange(state);
|
|
1763
|
+
} catch {
|
|
1764
|
+
result = false;
|
|
1765
|
+
}
|
|
1766
|
+
if (!result) return false;
|
|
1767
|
+
const ctx = { ...state.ctx || {} };
|
|
1768
|
+
ctx.__paused = {
|
|
1769
|
+
stage,
|
|
1770
|
+
iteration: iterations,
|
|
1771
|
+
reason: checkpointReason,
|
|
1772
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1773
|
+
};
|
|
1774
|
+
state = { ...state, ctx };
|
|
1775
|
+
pausedStage = stage;
|
|
1776
|
+
return true;
|
|
1777
|
+
};
|
|
1778
|
+
while (iterations < iterationLimit) {
|
|
1779
|
+
iterations++;
|
|
1780
|
+
const skippingAgent = resumeStage === "tools";
|
|
1781
|
+
if (!skippingAgent) {
|
|
1782
|
+
if (checkpointIfRequested("before_guardrails")) break;
|
|
1783
|
+
const preGuardrails = getGuardrailConfig(state);
|
|
1784
|
+
if (preGuardrails.length > 0) {
|
|
1785
|
+
const store = ensureGuardrailStore(state);
|
|
1786
|
+
if (store.lastRequestLength !== state.messages.length) {
|
|
1787
|
+
const outcome = await evaluateGuardrails({
|
|
1788
|
+
guardrails: preGuardrails,
|
|
1789
|
+
phase: "request" /* Request */,
|
|
1790
|
+
state,
|
|
1791
|
+
runtime: state.agent || runtime,
|
|
1792
|
+
options: opts,
|
|
1793
|
+
emit
|
|
1794
|
+
});
|
|
1795
|
+
store.lastRequestLength = state.messages.length;
|
|
1796
|
+
state.guardrailResult = mergeGuardrailOutcomes(state.guardrailResult, outcome);
|
|
1797
|
+
const blocking = outcome.incidents.find((incident) => incident.disposition === "block");
|
|
1798
|
+
if (blocking) {
|
|
1799
|
+
const guardMessage = {
|
|
1800
|
+
role: "assistant",
|
|
1801
|
+
name: "guardrail",
|
|
1802
|
+
content: blocking.reason || "Request blocked by guardrail policy.",
|
|
1803
|
+
metadata: {
|
|
1804
|
+
guardrail: {
|
|
1805
|
+
phase: "request" /* Request */,
|
|
1806
|
+
incidents: outcome.incidents
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
state = { ...state, messages: [...state.messages, guardMessage] };
|
|
1811
|
+
const ctx = state.ctx = state.ctx || {};
|
|
1812
|
+
ctx.__guardrailBlocked = {
|
|
1813
|
+
phase: "request" /* Request */,
|
|
1814
|
+
incident: blocking
|
|
1815
|
+
};
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
state = { ...state, ...await agentCore(state) };
|
|
1821
|
+
if (checkpointIfRequested("after_agent")) break;
|
|
1822
|
+
const postGuardrails = getGuardrailConfig(state);
|
|
1823
|
+
if (postGuardrails.length > 0) {
|
|
1824
|
+
const store = ensureGuardrailStore(state);
|
|
1825
|
+
if (store.lastResponseLength !== state.messages.length) {
|
|
1826
|
+
const outcome = await evaluateGuardrails({
|
|
1827
|
+
guardrails: postGuardrails,
|
|
1828
|
+
phase: "response" /* Response */,
|
|
1829
|
+
state,
|
|
1830
|
+
runtime: state.agent || runtime,
|
|
1831
|
+
options: opts,
|
|
1832
|
+
emit
|
|
1833
|
+
});
|
|
1834
|
+
store.lastResponseLength = state.messages.length;
|
|
1835
|
+
state.guardrailResult = mergeGuardrailOutcomes(state.guardrailResult, outcome);
|
|
1836
|
+
const blocking = outcome.incidents.find((incident) => incident.disposition === "block");
|
|
1837
|
+
if (blocking) {
|
|
1838
|
+
const updatedMessages = [...state.messages];
|
|
1839
|
+
const replaced = updatedMessages.pop();
|
|
1840
|
+
updatedMessages.push({
|
|
1841
|
+
role: "assistant",
|
|
1842
|
+
name: "guardrail",
|
|
1843
|
+
content: blocking.reason || "Response blocked by guardrail policy.",
|
|
1844
|
+
metadata: {
|
|
1845
|
+
guardrail: {
|
|
1846
|
+
phase: "response" /* Response */,
|
|
1847
|
+
incidents: outcome.incidents,
|
|
1848
|
+
replaced
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
state = { ...state, messages: updatedMessages };
|
|
1853
|
+
const ctx = state.ctx = state.ctx || {};
|
|
1854
|
+
ctx.__guardrailBlocked = {
|
|
1855
|
+
phase: "response" /* Response */,
|
|
1856
|
+
incident: blocking,
|
|
1857
|
+
replaced
|
|
1858
|
+
};
|
|
1859
|
+
break;
|
|
1860
|
+
} else if (outcome.incidents.length > 0) {
|
|
1861
|
+
const last = state.messages[state.messages.length - 1];
|
|
1862
|
+
if (last) {
|
|
1863
|
+
last.metadata = {
|
|
1864
|
+
...last.metadata || {},
|
|
1865
|
+
guardrail: {
|
|
1866
|
+
phase: "response" /* Response */,
|
|
1867
|
+
incidents: outcome.incidents
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
} else {
|
|
1875
|
+
resumeStage = null;
|
|
1876
|
+
}
|
|
1877
|
+
const lastMsg = state.messages[state.messages.length - 1];
|
|
1878
|
+
const toolCalls = Array.isArray(lastMsg?.tool_calls) ? lastMsg.tool_calls : [];
|
|
1879
|
+
const toolCallCount = state.toolCallCount || 0;
|
|
1880
|
+
if (state.ctx?.__finalizedDueToToolLimit) {
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
if (toolCallCount >= maxToolCalls && toolCalls.length > 0) {
|
|
1884
|
+
state = { ...state, ...await finalizeNode(state) };
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
if (toolCalls.length === 0) break;
|
|
1888
|
+
state = { ...state, ...await toolsNode(state) };
|
|
1889
|
+
if (state.ctx?.__awaitingApproval) break;
|
|
1890
|
+
if (checkpointIfRequested("after_tools")) break;
|
|
1891
|
+
if (state.ctx?.__finalizedDueToStructuredOutput) break;
|
|
1892
|
+
}
|
|
1893
|
+
if (!pausedStage && typeof onStateChange === "function" && onStateChange(state)) {
|
|
1894
|
+
checkpointIfRequested("after_loop");
|
|
1895
|
+
}
|
|
1896
|
+
return state;
|
|
1897
|
+
}
|
|
1898
|
+
const invokeAgent = async (input, config) => {
|
|
1899
|
+
const onEvent = config?.onEvent;
|
|
1900
|
+
const emit = (e) => {
|
|
1901
|
+
try {
|
|
1902
|
+
onEvent?.(e);
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
const traceSession = createTraceSession(opts);
|
|
1907
|
+
const ctx = { ...input.ctx || {}, __onEvent: onEvent };
|
|
1908
|
+
if (traceSession) ctx.__traceSession = traceSession;
|
|
1909
|
+
const initial = {
|
|
1910
|
+
messages: input.messages || [],
|
|
1911
|
+
toolCallCount: input.toolCallCount || 0,
|
|
1912
|
+
toolCache: input.toolCache || {},
|
|
1913
|
+
toolHistory: input.toolHistory || [],
|
|
1914
|
+
metadata: input.metadata,
|
|
1915
|
+
ctx,
|
|
1916
|
+
pendingApprovals: input.pendingApprovals || [],
|
|
1917
|
+
agent: input.agent || runtime,
|
|
1918
|
+
usage: input.usage || { perRequest: [], totals: {} }
|
|
1919
|
+
};
|
|
1920
|
+
let res;
|
|
1921
|
+
try {
|
|
1922
|
+
res = await runLoop(initial, config, emit);
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
await finalizeTraceSession(traceSession, {
|
|
1925
|
+
agentRuntime: runtime,
|
|
1926
|
+
status: "error",
|
|
1927
|
+
error: { message: err?.message, stack: err?.stack }
|
|
1928
|
+
});
|
|
1929
|
+
throw err;
|
|
1930
|
+
}
|
|
1931
|
+
await finalizeTraceSession(traceSession, {
|
|
1932
|
+
agentRuntime: res.agent || runtime,
|
|
1933
|
+
status: "success"
|
|
1934
|
+
});
|
|
1935
|
+
const finalMsg = res.messages[res.messages.length - 1];
|
|
1936
|
+
let content = "";
|
|
1937
|
+
if (typeof finalMsg?.content === "string") content = finalMsg.content;
|
|
1938
|
+
else if (Array.isArray(finalMsg?.content)) content = finalMsg.content.map((c) => typeof c === "string" ? c : c?.text ?? c?.content ?? "").join("");
|
|
1939
|
+
let parsed = void 0;
|
|
1940
|
+
const schema = opts.outputSchema;
|
|
1941
|
+
if (schema && res.ctx?.__structuredOutputParsed) {
|
|
1942
|
+
parsed = res.ctx.__structuredOutputParsed;
|
|
1943
|
+
} else if (schema && content) {
|
|
1944
|
+
let jsonText = null;
|
|
1945
|
+
const fenced = content.match(/```(?:json)?\n([\s\S]*?)```/i);
|
|
1946
|
+
if (fenced && fenced[1]) jsonText = fenced[1].trim();
|
|
1947
|
+
else {
|
|
1948
|
+
const braceIdx = content.indexOf("{");
|
|
1949
|
+
const bracketIdx = content.indexOf("[");
|
|
1950
|
+
const start = [braceIdx, bracketIdx].filter((i) => i >= 0).sort((a, b) => a - b)[0];
|
|
1951
|
+
if (start !== void 0) jsonText = content.slice(start).trim();
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
const raw = JSON.parse(jsonText ?? content);
|
|
1955
|
+
parsed = schema.parse(raw);
|
|
1956
|
+
} catch {
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
emit({ type: "finalAnswer", content: typeof finalMsg?.content === "string" ? finalMsg.content : content });
|
|
1960
|
+
return {
|
|
1961
|
+
content,
|
|
1962
|
+
output: parsed,
|
|
1963
|
+
metadata: { usage: res.usage },
|
|
1964
|
+
messages: res.messages,
|
|
1965
|
+
state: res
|
|
1966
|
+
};
|
|
1967
|
+
};
|
|
1968
|
+
const snapshotState = (state, options) => captureSnapshot(state, options);
|
|
1969
|
+
const resumeAgent = async (snapshot, config, restoreOptions) => {
|
|
1970
|
+
const restoredState = restoreSnapshot(snapshot, restoreOptions);
|
|
1971
|
+
return invokeAgent(restoredState, config);
|
|
1972
|
+
};
|
|
1973
|
+
const resolveToolApproval = (state, resolution) => resolveToolApprovalState(state, resolution);
|
|
1974
|
+
const instance = {
|
|
1975
|
+
invoke: invokeAgent,
|
|
1976
|
+
snapshot: snapshotState,
|
|
1977
|
+
resume: resumeAgent,
|
|
1978
|
+
resolveToolApproval,
|
|
1979
|
+
asTool: ({ toolName, description, inputDescription }) => {
|
|
1980
|
+
const schema = z.object({ input: z.string().describe(inputDescription || "Input for delegated agent") });
|
|
1981
|
+
return createTool({
|
|
1982
|
+
name: toolName,
|
|
1983
|
+
description: description || `Delegate task to agent ${opts.name || "Agent"}`,
|
|
1984
|
+
schema,
|
|
1985
|
+
func: async ({ input }) => {
|
|
1986
|
+
const res = await instance.invoke({ messages: [{ role: "user", content: input }] });
|
|
1987
|
+
return { content: res.content };
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
},
|
|
1991
|
+
asHandoff: ({ toolName, description, schema }) => {
|
|
1992
|
+
const finalName = toolName || `handoff_to_${runtime.name || "agent"}`;
|
|
1993
|
+
const zschema = schema || z.object({ reason: z.string().describe("Reason for handoff") });
|
|
1994
|
+
createTool({
|
|
1995
|
+
name: finalName,
|
|
1996
|
+
description: description || `Handoff control to agent ${runtime.name || "Agent"}`,
|
|
1997
|
+
schema: zschema,
|
|
1998
|
+
func: async (_args) => ({ __handoff: { runtime } })
|
|
1999
|
+
});
|
|
2000
|
+
return { type: "handoff", toolName: finalName, description: description || "", schema: zschema, target: instance };
|
|
2001
|
+
},
|
|
2002
|
+
__runtime: runtime
|
|
2003
|
+
};
|
|
2004
|
+
if (opts.handoffs && Array.isArray(opts.handoffs)) {
|
|
2005
|
+
const handoffTools = opts.handoffs.map((h) => {
|
|
2006
|
+
const schema = h.schema || z.object({ reason: z.string().describe("Reason for handoff") });
|
|
2007
|
+
return createTool({
|
|
2008
|
+
name: h.toolName,
|
|
2009
|
+
description: h.description || `Handoff to ${h.target.__runtime.name || "agent"}`,
|
|
2010
|
+
schema,
|
|
2011
|
+
func: async (_args) => ({ __handoff: { runtime: h.target.__runtime } })
|
|
2012
|
+
});
|
|
2013
|
+
});
|
|
2014
|
+
runtime.tools = [...runtime.tools, ...handoffTools];
|
|
2015
|
+
}
|
|
2016
|
+
return instance;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/nodes/agent.ts
|
|
2020
|
+
function createAgentNode(opts) {
|
|
2021
|
+
return async (state) => {
|
|
2022
|
+
const runtime = state.agent || {
|
|
2023
|
+
name: opts.name,
|
|
2024
|
+
version: opts.version,
|
|
2025
|
+
model: opts.model,
|
|
2026
|
+
tools: opts.tools || [],
|
|
2027
|
+
systemPrompt: opts.systemPrompt,
|
|
2028
|
+
limits: opts.limits,
|
|
2029
|
+
useTodoList: opts.useTodoList,
|
|
2030
|
+
outputSchema: opts.outputSchema
|
|
2031
|
+
};
|
|
2032
|
+
const tools = runtime.tools ?? [];
|
|
2033
|
+
const modelWithTools = runtime.model?.bindTools ? runtime.model.bindTools(tools) : runtime.model;
|
|
2034
|
+
const structuredOutputHint = runtime.outputSchema ? [
|
|
2035
|
+
"A structured output schema is active.",
|
|
2036
|
+
"Do NOT output the final JSON directly as an assistant message.",
|
|
2037
|
+
"When completely finished, call tool `response` passing the final JSON matching the schema as its arguments (direct object).",
|
|
2038
|
+
"Call it exactly once then STOP producing further assistant messages."
|
|
2039
|
+
].join("\n") : "";
|
|
2040
|
+
const systemMsg = {
|
|
2041
|
+
role: "system",
|
|
2042
|
+
content: buildSystemPrompt(
|
|
2043
|
+
[runtime.systemPrompt, structuredOutputHint].filter(Boolean).join("\n"),
|
|
2044
|
+
runtime.useTodoList === true,
|
|
2045
|
+
runtime.name || "Agent"
|
|
2046
|
+
)
|
|
2047
|
+
};
|
|
2048
|
+
const messages = [systemMsg, ...state.messages];
|
|
2049
|
+
const traceSession = state.ctx?.__traceSession;
|
|
2050
|
+
const actorName = runtime.name ?? opts.name ?? "agent";
|
|
2051
|
+
const actorVersion = runtime.version ?? opts.version;
|
|
2052
|
+
const start = Date.now();
|
|
2053
|
+
const promptMessages = [systemMsg, ...state.messages];
|
|
2054
|
+
const shouldLogPrompt = !!traceSession && traceSession.resolvedConfig.logData;
|
|
2055
|
+
const promptPayload = shouldLogPrompt ? sanitizeTracePayload(promptMessages) : void 0;
|
|
2056
|
+
const promptBytes = promptPayload !== void 0 ? estimatePayloadBytes(promptPayload) : void 0;
|
|
2057
|
+
let response;
|
|
2058
|
+
try {
|
|
2059
|
+
response = await modelWithTools.invoke(promptMessages);
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
const durationMs2 = Date.now() - start;
|
|
2062
|
+
recordTraceEvent(traceSession, {
|
|
2063
|
+
type: "ai_call",
|
|
2064
|
+
label: "Assistant Error",
|
|
2065
|
+
actor: { scope: "agent", name: actorName, role: "assistant", version: actorVersion },
|
|
2066
|
+
status: "error",
|
|
2067
|
+
durationMs: durationMs2,
|
|
2068
|
+
requestBytes: promptBytes,
|
|
2069
|
+
model: getModelName(runtime.model || opts.model),
|
|
2070
|
+
provider: getProviderName(runtime.model || opts.model),
|
|
2071
|
+
error: { message: err?.message || String(err), stack: err?.stack },
|
|
2072
|
+
messageList: promptMessages
|
|
2073
|
+
});
|
|
2074
|
+
throw err;
|
|
2075
|
+
}
|
|
2076
|
+
const messagesWithResponse = [
|
|
2077
|
+
...messages,
|
|
2078
|
+
response
|
|
2079
|
+
];
|
|
2080
|
+
const rawUsage = response?.usage || response?.response_metadata?.token_usage || response?.response_metadata?.usage;
|
|
2081
|
+
const normalized = normalizeUsage(rawUsage);
|
|
2082
|
+
const modelName = getModelName(runtime.model || opts.model) || "unknown_model";
|
|
2083
|
+
const providerName = getProviderName(runtime.model || opts.model);
|
|
2084
|
+
const durationMs = Date.now() - start;
|
|
2085
|
+
const shouldLogResponse = !!traceSession && traceSession.resolvedConfig.logData;
|
|
2086
|
+
const responsePayload = shouldLogResponse ? sanitizeTracePayload(response) : void 0;
|
|
2087
|
+
const responseBytes = responsePayload !== void 0 ? estimatePayloadBytes(responsePayload) : void 0;
|
|
2088
|
+
recordTraceEvent(traceSession, {
|
|
2089
|
+
type: "ai_call",
|
|
2090
|
+
label: "Assistant Response",
|
|
2091
|
+
actor: { scope: "agent", name: actorName, role: "assistant", version: actorVersion },
|
|
2092
|
+
durationMs,
|
|
2093
|
+
inputTokens: normalized?.prompt_tokens,
|
|
2094
|
+
outputTokens: normalized?.completion_tokens,
|
|
2095
|
+
totalTokens: normalized?.total_tokens,
|
|
2096
|
+
cachedInputTokens: normalized?.prompt_tokens_details?.cached_tokens,
|
|
2097
|
+
requestBytes: promptBytes,
|
|
2098
|
+
responseBytes,
|
|
2099
|
+
model: modelName,
|
|
2100
|
+
provider: providerName,
|
|
2101
|
+
messageList: messagesWithResponse
|
|
2102
|
+
});
|
|
2103
|
+
if (normalized) {
|
|
2104
|
+
const usageState = state.usage || { perRequest: [], totals: {} };
|
|
2105
|
+
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2106
|
+
const turn = usageState.perRequest.length + 1;
|
|
2107
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2108
|
+
const cachedInputTok = normalized.prompt_tokens_details.cached_tokens;
|
|
2109
|
+
usageState.perRequest.push({ id, modelName, usage: normalized, timestamp, turn, cachedInput: cachedInputTok });
|
|
2110
|
+
const inputTok = normalized.prompt_tokens;
|
|
2111
|
+
const outputTok = normalized.completion_tokens;
|
|
2112
|
+
const totalTok = normalized.total_tokens;
|
|
2113
|
+
const key = modelName;
|
|
2114
|
+
const agg = usageState.totals[key] || { input: 0, output: 0, total: 0, cachedInput: 0 };
|
|
2115
|
+
usageState.totals[key] = {
|
|
2116
|
+
input: agg.input + (Number(inputTok) || 0),
|
|
2117
|
+
output: agg.output + (Number(outputTok) || 0),
|
|
2118
|
+
total: agg.total + (Number(totalTok) || 0),
|
|
2119
|
+
cachedInput: agg.cachedInput + (Number(cachedInputTok) || 0)
|
|
2120
|
+
};
|
|
2121
|
+
state.usage = usageState;
|
|
2122
|
+
}
|
|
2123
|
+
return { messages: messagesWithResponse, usage: state.usage };
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/utils/utilTokens.ts
|
|
2128
|
+
function countApproxTokens(text) {
|
|
2129
|
+
if (!text) return 0;
|
|
2130
|
+
return Math.ceil(text.length / 4);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/nodes/contextSummarize.ts
|
|
2134
|
+
var systemMessage = (content) => ({ role: "system", content });
|
|
2135
|
+
var humanMessage = (content) => ({ role: "user", content });
|
|
2136
|
+
var toolMessage = (name, content, tool_call_id) => ({ role: "tool", name, content, tool_call_id });
|
|
2137
|
+
function createContextSummarizeNode(opts) {
|
|
2138
|
+
return async (state) => {
|
|
2139
|
+
const messages = state.messages || [];
|
|
2140
|
+
const summaryTokenLimit = opts.limits?.summaryTokenLimit ?? 1e5;
|
|
2141
|
+
const model = opts.model;
|
|
2142
|
+
const archived = state.toolHistoryArchived ? [...state.toolHistoryArchived] : [];
|
|
2143
|
+
const live = state.toolHistory ? [...state.toolHistory] : [];
|
|
2144
|
+
const byCallId = /* @__PURE__ */ new Map();
|
|
2145
|
+
for (const item of archived) {
|
|
2146
|
+
if (item.tool_call_id) byCallId.set(item.tool_call_id, item);
|
|
2147
|
+
}
|
|
2148
|
+
for (const item of live) {
|
|
2149
|
+
if (item.tool_call_id) byCallId.set(item.tool_call_id, item);
|
|
2150
|
+
}
|
|
2151
|
+
for (const item of live) {
|
|
2152
|
+
if (!item.executionId) item.executionId = nanoid();
|
|
2153
|
+
}
|
|
2154
|
+
const getText = (m) => {
|
|
2155
|
+
if (!m) return "";
|
|
2156
|
+
const c = m.content;
|
|
2157
|
+
if (typeof c === "string") return c;
|
|
2158
|
+
if (Array.isArray(c)) return c.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("");
|
|
2159
|
+
return String(c ?? "");
|
|
2160
|
+
};
|
|
2161
|
+
const sysText = "You are a concise summarization assistant for a tool-using agent. Summarize prior tool executions into a compact brief that preserves key facts, decisions, and outputs. Reference executionId values where helpful. Avoid repeating raw data; prefer synthesis.";
|
|
2162
|
+
const sys = systemMessage(sysText);
|
|
2163
|
+
let summaryText = "";
|
|
2164
|
+
try {
|
|
2165
|
+
const limit = Math.max(1e3, Number(summaryTokenLimit) || 1e5);
|
|
2166
|
+
const safetyBuffer = Math.floor(Math.min(4e3, limit * 0.05));
|
|
2167
|
+
const perCallBudget = Math.max(500, limit - safetyBuffer);
|
|
2168
|
+
const safeToolPair = opts?.safeToolPair !== false;
|
|
2169
|
+
const chunks = [];
|
|
2170
|
+
let current = [];
|
|
2171
|
+
let currentTokens = countApproxTokens(sysText);
|
|
2172
|
+
let pendingToolCalls = /* @__PURE__ */ new Set();
|
|
2173
|
+
const extractToolCallIds = (msg) => {
|
|
2174
|
+
const tc = msg?.additional_kwargs?.tool_calls;
|
|
2175
|
+
if (!Array.isArray(tc) || tc.length === 0) return [];
|
|
2176
|
+
return tc.map((c) => c?.id).filter((id) => typeof id === "string");
|
|
2177
|
+
};
|
|
2178
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2179
|
+
const m = messages[i];
|
|
2180
|
+
const text = getText(m);
|
|
2181
|
+
const tks = countApproxTokens(text);
|
|
2182
|
+
const isAIWithToolCalls = safeToolPair && m?.role === "assistant" && extractToolCallIds(m).length > 0;
|
|
2183
|
+
const isToolMsg = safeToolPair && m?.role === "tool";
|
|
2184
|
+
const toolCallId = isToolMsg ? m.tool_call_id : void 0;
|
|
2185
|
+
const wouldExceed = currentTokens + tks > perCallBudget && current.length > 0;
|
|
2186
|
+
if (wouldExceed) {
|
|
2187
|
+
let forceAddToCurrent = false;
|
|
2188
|
+
if (safeToolPair) {
|
|
2189
|
+
if (isToolMsg && toolCallId && pendingToolCalls.has(toolCallId)) {
|
|
2190
|
+
forceAddToCurrent = true;
|
|
2191
|
+
} else if (isAIWithToolCalls) {
|
|
2192
|
+
} else if (pendingToolCalls.size > 0) {
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (!forceAddToCurrent) {
|
|
2196
|
+
if (current.length > 0) {
|
|
2197
|
+
chunks.push(current);
|
|
2198
|
+
}
|
|
2199
|
+
current = [];
|
|
2200
|
+
currentTokens = countApproxTokens(sysText);
|
|
2201
|
+
pendingToolCalls = /* @__PURE__ */ new Set();
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
current.push(m);
|
|
2205
|
+
currentTokens += tks;
|
|
2206
|
+
if (isAIWithToolCalls) {
|
|
2207
|
+
for (const id of extractToolCallIds(m)) pendingToolCalls.add(id);
|
|
2208
|
+
}
|
|
2209
|
+
if (isToolMsg && toolCallId && pendingToolCalls.has(toolCallId)) {
|
|
2210
|
+
pendingToolCalls.delete(toolCallId);
|
|
2211
|
+
}
|
|
2212
|
+
if (currentTokens >= perCallBudget && pendingToolCalls.size === 0) {
|
|
2213
|
+
chunks.push(current);
|
|
2214
|
+
current = [];
|
|
2215
|
+
currentTokens = countApproxTokens(sysText);
|
|
2216
|
+
pendingToolCalls = /* @__PURE__ */ new Set();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (current.length > 0) {
|
|
2220
|
+
chunks.push(current);
|
|
2221
|
+
}
|
|
2222
|
+
const partials = [];
|
|
2223
|
+
for (const chunk of chunks) {
|
|
2224
|
+
let resp = null;
|
|
2225
|
+
try {
|
|
2226
|
+
resp = await (model?.invoke ? model.invoke([sys, ...chunk]) : null);
|
|
2227
|
+
const c = resp?.content;
|
|
2228
|
+
const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : "";
|
|
2229
|
+
partials.push((text || "").trim());
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
console.error("Summarization chunk failed:", e);
|
|
2232
|
+
partials.push("[summary unavailable for one chunk]");
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
const mergeSysText = "You merge partial summaries into a single cohesive, non-redundant summary that preserves key facts and decisions. Be concise.";
|
|
2236
|
+
const mergeSys = systemMessage(mergeSysText);
|
|
2237
|
+
const mergeIteratively = async (texts) => {
|
|
2238
|
+
if (texts.length === 0) return "";
|
|
2239
|
+
if (texts.length === 1) return texts[0];
|
|
2240
|
+
const groups = [];
|
|
2241
|
+
let group = [];
|
|
2242
|
+
let toks = countApproxTokens(mergeSysText);
|
|
2243
|
+
for (const t of texts) {
|
|
2244
|
+
const tt = `- ${t}`;
|
|
2245
|
+
const tk = countApproxTokens(tt);
|
|
2246
|
+
if (toks + tk > perCallBudget && group.length > 0) {
|
|
2247
|
+
groups.push(group);
|
|
2248
|
+
group = [];
|
|
2249
|
+
toks = countApproxTokens(mergeSysText);
|
|
2250
|
+
}
|
|
2251
|
+
group.push(tt);
|
|
2252
|
+
toks += tk;
|
|
2253
|
+
}
|
|
2254
|
+
if (group.length > 0) groups.push(group);
|
|
2255
|
+
const next = [];
|
|
2256
|
+
for (const g of groups) {
|
|
2257
|
+
const mergePrompt = humanMessage(`Merge the following partial summaries into a single concise brief. Avoid duplication, keep key facts and decisions.
|
|
2258
|
+
|
|
2259
|
+
${g.join("\n")}
|
|
2260
|
+
|
|
2261
|
+
Merged summary:`);
|
|
2262
|
+
try {
|
|
2263
|
+
const r = await (model?.invoke ? model.invoke([mergeSys, mergePrompt]) : null);
|
|
2264
|
+
const cc = r?.content;
|
|
2265
|
+
const txt = typeof cc === "string" ? cc : Array.isArray(cc) ? cc.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : "";
|
|
2266
|
+
next.push((txt || "").trim());
|
|
2267
|
+
} catch (e) {
|
|
2268
|
+
console.error("Merge summarization failed:", e);
|
|
2269
|
+
next.push(g.join(" "));
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return mergeIteratively(next);
|
|
2273
|
+
};
|
|
2274
|
+
summaryText = await mergeIteratively(partials);
|
|
2275
|
+
if (!summaryText) summaryText = "Context summarized.";
|
|
2276
|
+
} catch (e) {
|
|
2277
|
+
console.error("Error during context summarization:", e);
|
|
2278
|
+
summaryText = "Context summarized.";
|
|
2279
|
+
}
|
|
2280
|
+
const rewritten = [];
|
|
2281
|
+
const alreadySummarized = (content) => {
|
|
2282
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : String(content ?? "");
|
|
2283
|
+
return /\bSUMMARIZED\b/.test(text);
|
|
2284
|
+
};
|
|
2285
|
+
for (const m of messages) {
|
|
2286
|
+
if (m.getType() === "tool") {
|
|
2287
|
+
const toolMessageObj = m;
|
|
2288
|
+
if (alreadySummarized(toolMessageObj.content) || toolMessageObj.name === "context_summarize" || toolMessageObj.name === "manage_todo_list") {
|
|
2289
|
+
rewritten.push(m);
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2292
|
+
const callId = toolMessageObj.tool_call_id;
|
|
2293
|
+
const safeId = callId || `summarized_${nanoid(6)}`;
|
|
2294
|
+
let hist = callId ? byCallId.get(callId) : void 0;
|
|
2295
|
+
if (!hist) {
|
|
2296
|
+
hist = {
|
|
2297
|
+
executionId: nanoid(),
|
|
2298
|
+
toolName: toolMessageObj.name || "unknown_tool",
|
|
2299
|
+
args: null,
|
|
2300
|
+
output: typeof toolMessageObj.content === "string" ? toolMessageObj.content : Array.isArray(toolMessageObj.content) ? toolMessageObj.content.map((p) => typeof p === "string" ? p : p?.text ?? p?.content ?? "").join("") : String(toolMessageObj.content ?? ""),
|
|
2301
|
+
rawOutput: void 0,
|
|
2302
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2303
|
+
summarized: true,
|
|
2304
|
+
messageId: void 0,
|
|
2305
|
+
tool_call_id: callId,
|
|
2306
|
+
fromCache: false
|
|
2307
|
+
};
|
|
2308
|
+
if (callId) byCallId.set(callId, hist);
|
|
2309
|
+
archived.push({ ...hist });
|
|
2310
|
+
}
|
|
2311
|
+
if (!hist.executionId) hist.executionId = nanoid();
|
|
2312
|
+
hist.summarized = true;
|
|
2313
|
+
rewritten.push({
|
|
2314
|
+
role: "tool",
|
|
2315
|
+
content: `SUMMARIZED executionId:'${hist.executionId}'`,
|
|
2316
|
+
tool_call_id: safeId,
|
|
2317
|
+
name: toolMessage.name
|
|
2318
|
+
});
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
rewritten.push(m);
|
|
2322
|
+
}
|
|
2323
|
+
const summarizeCallId = `summarize_${nanoid(6)}`;
|
|
2324
|
+
const summarizeArgs = { reason: "Exceeded token limit: summarize prior tool outputs for context." };
|
|
2325
|
+
const syntheticAssistant = {
|
|
2326
|
+
role: "assistant",
|
|
2327
|
+
content: "",
|
|
2328
|
+
additional_kwargs: {
|
|
2329
|
+
tool_calls: [
|
|
2330
|
+
{ id: summarizeCallId, type: "function", function: { name: "context_summarize", arguments: JSON.stringify(summarizeArgs) } }
|
|
2331
|
+
]
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
const syntheticToolResp = {
|
|
2335
|
+
role: "tool",
|
|
2336
|
+
content: `${summaryText}`,
|
|
2337
|
+
tool_call_id: summarizeCallId,
|
|
2338
|
+
name: "context_summarize"
|
|
2339
|
+
};
|
|
2340
|
+
for (const item of live) item.summarized = true;
|
|
2341
|
+
const merged = [...archived, ...live];
|
|
2342
|
+
const dedupMap = /* @__PURE__ */ new Map();
|
|
2343
|
+
for (const it of merged) {
|
|
2344
|
+
const key = it.tool_call_id || `${it.toolName}:${it.timestamp}`;
|
|
2345
|
+
dedupMap.set(key, { ...it });
|
|
2346
|
+
}
|
|
2347
|
+
const newArchived = Array.from(dedupMap.values());
|
|
2348
|
+
const onEvent = state.ctx?.__onEvent;
|
|
2349
|
+
onEvent?.({ type: "summarization", summary: summaryText, archivedCount: newArchived.length });
|
|
2350
|
+
return {
|
|
2351
|
+
messages: [...rewritten, syntheticAssistant, syntheticToolResp],
|
|
2352
|
+
toolHistoryArchived: newArchived,
|
|
2353
|
+
toolHistory: [],
|
|
2354
|
+
ctx: { ...state.ctx || {}, __contextSummarized: true }
|
|
2355
|
+
};
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/utils/tokenManager.ts
|
|
2360
|
+
async function applyTokenLimits({
|
|
2361
|
+
state,
|
|
2362
|
+
limits
|
|
2363
|
+
}) {
|
|
2364
|
+
const messages = state.messages || [];
|
|
2365
|
+
const budget = limits.contextTokenLimit;
|
|
2366
|
+
let total = messages.reduce((acc, m) => acc + countApproxTokens(String(m.content || "")), 0);
|
|
2367
|
+
if (total <= budget) return state;
|
|
2368
|
+
const newMessages = [...messages];
|
|
2369
|
+
return { ...state, messages: newMessages };
|
|
2370
|
+
}
|
|
2371
|
+
function createContextTools(stateRef, opts) {
|
|
2372
|
+
const tools = [];
|
|
2373
|
+
if (opts?.planningEnabled) {
|
|
2374
|
+
const manageTodo = createTool({
|
|
2375
|
+
name: "manage_todo_list",
|
|
2376
|
+
description: "Manage a structured todo list to track progress and plan tasks throughout your coding session. Use this tool VERY frequently to ensure task visibility and proper planning.\n\nWhen to use this tool:\n- Complex multi-step work requiring planning and tracking\n- When user provides multiple tasks or requests (numbered/comma-separated)\n- After receiving new instructions that require multiple steps\n- BEFORE starting work on any todo (mark as in-progress)\n- IMMEDIATELY after completing each todo (mark completed individually)\n- When breaking down larger tasks into smaller actionable steps\n- To give users visibility into your progress and planning\n\nWhen NOT to use:\n- Single, trivial tasks that can be completed in one step\n- Purely conversational/informational requests\n- When just reading files or performing simple searches\n\nCRITICAL workflow:\n1. Plan tasks by writing todo list with specific, actionable items\n2. Complete the work for that specific todo\n3. Mark that todo as completed IMMEDIATELY\n4. Move to next todo and repeat\n\nTodo states:\n- not-started: Todo not yet begun\n- in-progress: Currently working (limit ONE at a time)\n- completed: Finished successfully\n\nIMPORTANT: Mark todos completed as soon as they are done. Do not batch completions.",
|
|
2377
|
+
schema: z.object({
|
|
2378
|
+
operation: z.enum(["write", "read"]),
|
|
2379
|
+
todoList: z.array(z.object({
|
|
2380
|
+
id: z.number().int().positive().describe("Sequential id starting from 1"),
|
|
2381
|
+
title: z.string().min(1),
|
|
2382
|
+
description: z.string().min(1),
|
|
2383
|
+
status: z.enum(["not-started", "in-progress", "completed"]),
|
|
2384
|
+
evidence: z.string().max(200).optional()
|
|
2385
|
+
})).optional()
|
|
2386
|
+
}),
|
|
2387
|
+
func: async ({ operation, todoList }) => {
|
|
2388
|
+
const onEvent = manageTodo._stateRef?.__onEvent;
|
|
2389
|
+
if (operation === "read") {
|
|
2390
|
+
const list = stateRef.todoList || [];
|
|
2391
|
+
onEvent?.({ type: "plan", source: "manage_todo_list", operation: "read", todoList: list });
|
|
2392
|
+
return list;
|
|
2393
|
+
}
|
|
2394
|
+
if (Array.isArray(todoList)) {
|
|
2395
|
+
stateRef.todoList = todoList;
|
|
2396
|
+
}
|
|
2397
|
+
const payload = {
|
|
2398
|
+
status: "ok",
|
|
2399
|
+
operation,
|
|
2400
|
+
count: Array.isArray(todoList) ? todoList.length : void 0
|
|
2401
|
+
};
|
|
2402
|
+
onEvent?.({ type: "plan", source: "manage_todo_list", operation: "write", todoList });
|
|
2403
|
+
return payload;
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
manageTodo._stateRef = stateRef;
|
|
2407
|
+
tools.push(manageTodo);
|
|
2408
|
+
}
|
|
2409
|
+
const getTool = createTool({
|
|
2410
|
+
name: "get_tool_response",
|
|
2411
|
+
description: "RETRIEVE tool execution response: Use this to access the full output of a tool execution that shows as 'SUMMARIZED' in the tool history.",
|
|
2412
|
+
schema: z.object({ executionId: z.string().describe("Tool execution id") }),
|
|
2413
|
+
func: async ({ executionId }) => {
|
|
2414
|
+
let execution = stateRef.toolHistory?.find((t) => t.executionId === executionId);
|
|
2415
|
+
if (!execution) {
|
|
2416
|
+
execution = stateRef.toolHistoryArchived?.find((t) => t.executionId === executionId);
|
|
2417
|
+
}
|
|
2418
|
+
if (execution) {
|
|
2419
|
+
return execution.rawOutput || execution.output;
|
|
2420
|
+
}
|
|
2421
|
+
return "Execution not found. Please check the executionId.";
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
getTool._stateRef = stateRef;
|
|
2425
|
+
tools.push(getTool);
|
|
2426
|
+
if (opts?.outputSchema) {
|
|
2427
|
+
const responseTool = createTool({
|
|
2428
|
+
name: "response",
|
|
2429
|
+
description: "Finalize the answer by returning the final structured JSON matching the required schema. Call exactly once when you are fully done, then stop.",
|
|
2430
|
+
schema: opts.outputSchema.shape ? opts.outputSchema : z.any(),
|
|
2431
|
+
func: async (data) => {
|
|
2432
|
+
try {
|
|
2433
|
+
const validated = opts.outputSchema.parse ? opts.outputSchema.parse(data) : data;
|
|
2434
|
+
return { __finalStructuredOutput: true, data: validated };
|
|
2435
|
+
} catch (e) {
|
|
2436
|
+
return { error: "Schema validation failed", details: e?.message };
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
responseTool._stateRef = stateRef;
|
|
2441
|
+
tools.push(responseTool);
|
|
2442
|
+
}
|
|
2443
|
+
return tools;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// src/graph/decisions.ts
|
|
2447
|
+
function needsSummarization(state, opts, summarizationEnabled) {
|
|
2448
|
+
const maxTok = opts.limits?.maxToken;
|
|
2449
|
+
if (!maxTok || !summarizationEnabled) return false;
|
|
2450
|
+
try {
|
|
2451
|
+
const allText = (state.messages || []).map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((c) => typeof c === "string" ? c : c?.text ?? c?.content ?? "").join("") : "").join("\n");
|
|
2452
|
+
const tokenCount = countApproxTokens(allText);
|
|
2453
|
+
return tokenCount > maxTok;
|
|
2454
|
+
} catch {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
function resolverDecisionFactory(opts, summarizationEnabled) {
|
|
2459
|
+
return function resolverDecision(state) {
|
|
2460
|
+
return needsSummarization(state, opts, summarizationEnabled) ? "contextSummarize" : "agent";
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function toolsDecisionFactory(opts, summarizationEnabled) {
|
|
2464
|
+
return function toolsDecision(state) {
|
|
2465
|
+
const max = opts.limits?.maxToolCalls ?? 10;
|
|
2466
|
+
const count = state.toolCallCount || 0;
|
|
2467
|
+
if (count >= max) return "toolLimitFinalize";
|
|
2468
|
+
return needsSummarization(state, opts, summarizationEnabled) ? "contextSummarize" : "agent";
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/smart/index.ts
|
|
2473
|
+
function createSmartAgent(opts) {
|
|
2474
|
+
const stateRef = { toolHistory: void 0, toolHistoryArchived: void 0, todoList: void 0 };
|
|
2475
|
+
const planningEnabled = opts.useTodoList === true;
|
|
2476
|
+
const contextTools = createContextTools(stateRef, { planningEnabled, outputSchema: void 0 });
|
|
2477
|
+
const mergedTools = [...opts.tools ?? [], ...contextTools];
|
|
2478
|
+
const base = createAgent({ ...opts, tools: mergedTools });
|
|
2479
|
+
const summarizationEnabled = opts.summarization !== false;
|
|
2480
|
+
const summarizer = summarizationEnabled ? createContextSummarizeNode(opts) : void 0;
|
|
2481
|
+
const decideBefore = resolverDecisionFactory(opts, summarizationEnabled);
|
|
2482
|
+
const decideAfter = toolsDecisionFactory(opts, summarizationEnabled);
|
|
2483
|
+
const structuredOutputHint = opts.outputSchema ? [
|
|
2484
|
+
"A structured output schema is active.",
|
|
2485
|
+
"Do NOT output the final JSON directly as an assistant message.",
|
|
2486
|
+
"When completely finished, call tool `response` passing the final JSON matching the schema as its arguments (direct object).",
|
|
2487
|
+
"Call it exactly once then STOP producing further assistant messages."
|
|
2488
|
+
].join("\n") : "";
|
|
2489
|
+
function systemMessage2() {
|
|
2490
|
+
const sys = buildSystemPrompt(
|
|
2491
|
+
[opts.systemPrompt, structuredOutputHint].filter(Boolean).join("\n"),
|
|
2492
|
+
opts.useTodoList === true,
|
|
2493
|
+
opts.name || "Agent"
|
|
2494
|
+
);
|
|
2495
|
+
return { role: "system", content: sys };
|
|
2496
|
+
}
|
|
2497
|
+
const instance = {
|
|
2498
|
+
invoke: async (input, config) => {
|
|
2499
|
+
stateRef.toolHistory = input.toolHistory;
|
|
2500
|
+
stateRef.toolHistoryArchived = input.toolHistoryArchived;
|
|
2501
|
+
const alreadyHasSystem = Array.isArray(input.messages) && input.messages[0]?.role === "system";
|
|
2502
|
+
const seedMessages = alreadyHasSystem ? [...input.messages || []] : [systemMessage2(), ...input.messages || []];
|
|
2503
|
+
let state = { ...input, messages: seedMessages };
|
|
2504
|
+
let lastResult = null;
|
|
2505
|
+
const iterationLimit = Math.max((opts.limits?.maxToolCalls ?? 10) * 3 + 5, 30);
|
|
2506
|
+
for (let i = 0; i < iterationLimit; i++) {
|
|
2507
|
+
const next = summarizationEnabled ? decideBefore(state) : "agent";
|
|
2508
|
+
if (next === "contextSummarize" && summarizer) {
|
|
2509
|
+
const delta = await summarizer(state);
|
|
2510
|
+
state = { ...state, ...delta };
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
const res = await base.invoke(state, config);
|
|
2514
|
+
lastResult = res;
|
|
2515
|
+
state = res.state || { ...state, messages: res.messages };
|
|
2516
|
+
if (state.ctx?.__finalizedDueToStructuredOutput) break;
|
|
2517
|
+
if (summarizationEnabled) {
|
|
2518
|
+
const after = decideAfter(state);
|
|
2519
|
+
if (after === "contextSummarize" && summarizer) {
|
|
2520
|
+
const delta = await summarizer(state);
|
|
2521
|
+
state = { ...state, ...delta };
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
break;
|
|
2526
|
+
}
|
|
2527
|
+
if (!lastResult) {
|
|
2528
|
+
const res = await base.invoke(state, config);
|
|
2529
|
+
lastResult = res;
|
|
2530
|
+
}
|
|
2531
|
+
return lastResult;
|
|
2532
|
+
},
|
|
2533
|
+
snapshot: base.snapshot,
|
|
2534
|
+
resume: base.resume,
|
|
2535
|
+
resolveToolApproval: base.resolveToolApproval,
|
|
2536
|
+
asTool: base.asTool,
|
|
2537
|
+
asHandoff: base.asHandoff,
|
|
2538
|
+
__runtime: base.__runtime
|
|
2539
|
+
};
|
|
2540
|
+
return instance;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// src/utils/content.ts
|
|
2544
|
+
function contentToString(content) {
|
|
2545
|
+
try {
|
|
2546
|
+
if (content == null) return "";
|
|
2547
|
+
if (typeof content === "string") return content;
|
|
2548
|
+
if (Array.isArray(content)) {
|
|
2549
|
+
return content.map((p) => {
|
|
2550
|
+
if (!p || typeof p !== "object") return String(p);
|
|
2551
|
+
if (p.type === "text") return String(p.text ?? "");
|
|
2552
|
+
if (p.type === "image_url") {
|
|
2553
|
+
const img = p.image_url;
|
|
2554
|
+
const url = typeof img === "string" ? img : img?.url;
|
|
2555
|
+
const detail = typeof img === "string" ? void 0 : img?.detail;
|
|
2556
|
+
return `[image:${url || "unknown"}${detail ? ` detail=${detail}` : ""}]`;
|
|
2557
|
+
}
|
|
2558
|
+
return JSON.stringify(p);
|
|
2559
|
+
}).join("\n");
|
|
2560
|
+
}
|
|
2561
|
+
return JSON.stringify(content);
|
|
2562
|
+
} catch {
|
|
2563
|
+
return String(content);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function mergeContentsToString(contents) {
|
|
2567
|
+
return contents.map((c) => contentToString(c)).join("\n");
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// src/guardrails/checks.ts
|
|
2571
|
+
var Ajv = AjvModule?.default ?? AjvModule;
|
|
2572
|
+
var defaultAjv = new Ajv({ allErrors: true });
|
|
2573
|
+
defaultAjv.opts.strict = false;
|
|
2574
|
+
function ensureRegExp(pattern, flags) {
|
|
2575
|
+
if (pattern instanceof RegExp) return pattern;
|
|
2576
|
+
return new RegExp(pattern, flags ?? "i");
|
|
2577
|
+
}
|
|
2578
|
+
function latestText(context) {
|
|
2579
|
+
return contentToString(context.latestMessage?.content ?? "");
|
|
2580
|
+
}
|
|
2581
|
+
function threadText(context) {
|
|
2582
|
+
return mergeContentsToString(context.messages.map((msg) => msg.content));
|
|
2583
|
+
}
|
|
2584
|
+
function regexRule(options) {
|
|
2585
|
+
const regex = ensureRegExp(options.pattern, options.flags);
|
|
2586
|
+
const selector = options.selector || latestText;
|
|
2587
|
+
const failureMessage = options.failureMessage || `Message violates pattern: ${regex.toString()}`;
|
|
2588
|
+
const matchDisposition = options.matchDisposition || "block";
|
|
2589
|
+
const allowIfMatch = options.allowIfMatch === true;
|
|
2590
|
+
return {
|
|
2591
|
+
id: options.id,
|
|
2592
|
+
title: options.title || "Regex compliance",
|
|
2593
|
+
description: options.description,
|
|
2594
|
+
async evaluate(context) {
|
|
2595
|
+
const text = selector(context) ?? "";
|
|
2596
|
+
const matches = regex.test(text);
|
|
2597
|
+
const passed = allowIfMatch ? matches : !matches;
|
|
2598
|
+
const disposition = matches ? matchDisposition : "allow";
|
|
2599
|
+
return {
|
|
2600
|
+
passed,
|
|
2601
|
+
reason: matches ? failureMessage : void 0,
|
|
2602
|
+
disposition,
|
|
2603
|
+
details: matches ? { pattern: regex.toString(), textSnippet: text.slice(0, 200) } : void 0
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
function compileValidator(schema) {
|
|
2609
|
+
if (schema?._def) {
|
|
2610
|
+
const typed = schema;
|
|
2611
|
+
return (input) => {
|
|
2612
|
+
const result = typed.safeParse(input);
|
|
2613
|
+
return {
|
|
2614
|
+
valid: result.success,
|
|
2615
|
+
errors: result.success ? void 0 : result.error.issues.map((issue) => issue.message)
|
|
2616
|
+
};
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
const validator = defaultAjv.compile(schema);
|
|
2620
|
+
return (input) => {
|
|
2621
|
+
const valid = validator(input);
|
|
2622
|
+
const errors = validator.errors?.map((err) => {
|
|
2623
|
+
const path2 = "instancePath" in err && typeof err.instancePath === "string" && err.instancePath.length > 0 ? err.instancePath : err.dataPath;
|
|
2624
|
+
return `${path2 || "/"} ${err.message || "invalid"}`;
|
|
2625
|
+
});
|
|
2626
|
+
return { valid: !!valid, errors };
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
function jsonSchemaRule(options) {
|
|
2630
|
+
const selector = options.selector || latestText;
|
|
2631
|
+
const disposition = options.disposition || "block";
|
|
2632
|
+
const validator = compileValidator(options.schema);
|
|
2633
|
+
return {
|
|
2634
|
+
id: options.id,
|
|
2635
|
+
title: options.title || "JSON schema validation",
|
|
2636
|
+
description: options.description,
|
|
2637
|
+
async evaluate(context) {
|
|
2638
|
+
const text = selector(context) ?? "";
|
|
2639
|
+
let parsed;
|
|
2640
|
+
try {
|
|
2641
|
+
parsed = JSON.parse(text);
|
|
2642
|
+
} catch (err) {
|
|
2643
|
+
if (options.allowOnParseError) {
|
|
2644
|
+
return { passed: true };
|
|
2645
|
+
}
|
|
2646
|
+
return {
|
|
2647
|
+
passed: false,
|
|
2648
|
+
disposition,
|
|
2649
|
+
reason: "Message content is not valid JSON.",
|
|
2650
|
+
details: { error: err instanceof Error ? err.message : String(err) }
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
const result = validator(parsed);
|
|
2654
|
+
if (result.valid) {
|
|
2655
|
+
return { passed: true };
|
|
2656
|
+
}
|
|
2657
|
+
return {
|
|
2658
|
+
passed: false,
|
|
2659
|
+
disposition,
|
|
2660
|
+
reason: "Message JSON failed schema validation.",
|
|
2661
|
+
details: { errors: result.errors }
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
function containsAllowListed(text, allowList) {
|
|
2667
|
+
if (!allowList?.length) return false;
|
|
2668
|
+
return allowList.some((pattern) => ensureRegExp(pattern).test(text));
|
|
2669
|
+
}
|
|
2670
|
+
var codePattern = /```[\s\S]*?```|\b(function|class|import|export|const|let|var)\b|<[^>]+>/i;
|
|
2671
|
+
function codePresenceRule(options = {}) {
|
|
2672
|
+
const selector = options.selector || latestText;
|
|
2673
|
+
const disposition = options.disposition || "warn";
|
|
2674
|
+
return {
|
|
2675
|
+
id: options.id,
|
|
2676
|
+
title: options.title || "Code presence detection",
|
|
2677
|
+
description: options.description,
|
|
2678
|
+
async evaluate(context) {
|
|
2679
|
+
const text = selector(context) ?? "";
|
|
2680
|
+
if (!text) return { passed: true };
|
|
2681
|
+
if (containsAllowListed(text, options.allowList)) {
|
|
2682
|
+
return { passed: true };
|
|
2683
|
+
}
|
|
2684
|
+
const matches = codePattern.test(text);
|
|
2685
|
+
if (!matches) return { passed: true };
|
|
2686
|
+
return {
|
|
2687
|
+
passed: false,
|
|
2688
|
+
disposition,
|
|
2689
|
+
reason: "Potential code content detected in message.",
|
|
2690
|
+
details: { snippet: text.slice(0, 200) }
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
var guardianSchema = z.object({
|
|
2696
|
+
allow: z.boolean().describe("Whether the message should be allowed."),
|
|
2697
|
+
disposition: z.enum(["allow", "warn", "block"]).optional(),
|
|
2698
|
+
reason: z.string().max(400, "Reason must be 400 characters or fewer.").optional(),
|
|
2699
|
+
details: z.record(z.any()).optional()
|
|
2700
|
+
});
|
|
2701
|
+
function defaultPayload(context) {
|
|
2702
|
+
const latest = latestText(context);
|
|
2703
|
+
const transcript = threadText(context);
|
|
2704
|
+
return JSON.stringify(
|
|
2705
|
+
{
|
|
2706
|
+
phase: context.phase,
|
|
2707
|
+
latestMessage: latest,
|
|
2708
|
+
transcript,
|
|
2709
|
+
metadata: {
|
|
2710
|
+
runtime: {
|
|
2711
|
+
name: context.runtime?.name,
|
|
2712
|
+
version: context.runtime?.version
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
},
|
|
2716
|
+
null,
|
|
2717
|
+
2
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
function agentVerdictRule(options) {
|
|
2721
|
+
const payloadBuilder = options.payloadBuilder || defaultPayload;
|
|
2722
|
+
const fallbackDisposition = options.dispositionFallback || "block";
|
|
2723
|
+
return {
|
|
2724
|
+
id: options.id,
|
|
2725
|
+
title: options.title || "Agent-based guardrail",
|
|
2726
|
+
description: options.description,
|
|
2727
|
+
async evaluate(context) {
|
|
2728
|
+
const payload = payloadBuilder(context);
|
|
2729
|
+
const systemPrompt = options.systemPrompt || "You are a safety auditor. Review the provided conversation payload and decide whether it should continue. Return JSON with keys allow (boolean), optional disposition ('allow' | 'warn' | 'block'), optional reason (string), and optional details (object).";
|
|
2730
|
+
const review = await options.agent.invoke({
|
|
2731
|
+
messages: [
|
|
2732
|
+
{ role: "system", content: systemPrompt },
|
|
2733
|
+
{ role: "user", content: payload }
|
|
2734
|
+
]
|
|
2735
|
+
});
|
|
2736
|
+
let parsed = review.output;
|
|
2737
|
+
if (!parsed) {
|
|
2738
|
+
try {
|
|
2739
|
+
parsed = guardianSchema.parse(JSON.parse(review.content));
|
|
2740
|
+
} catch {
|
|
2741
|
+
parsed = guardianSchema.parse({ allow: false, reason: "Guard agent did not return structured output." });
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const disposition = parsed.disposition || (parsed.allow ? "allow" : fallbackDisposition);
|
|
2745
|
+
return {
|
|
2746
|
+
passed: parsed.allow,
|
|
2747
|
+
disposition,
|
|
2748
|
+
reason: parsed.reason,
|
|
2749
|
+
details: parsed.details
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function customCallbackRule(options) {
|
|
2755
|
+
const fallbackDisposition = options.defaultDisposition || "block";
|
|
2756
|
+
return {
|
|
2757
|
+
id: options.id,
|
|
2758
|
+
title: options.title || "Custom callback guardrail",
|
|
2759
|
+
description: options.description,
|
|
2760
|
+
async evaluate(context) {
|
|
2761
|
+
const outcome = await options.callback(context);
|
|
2762
|
+
if (typeof outcome === "boolean") {
|
|
2763
|
+
return {
|
|
2764
|
+
passed: outcome,
|
|
2765
|
+
disposition: outcome ? "allow" : fallbackDisposition
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
passed: outcome.allow,
|
|
2770
|
+
disposition: outcome.disposition || (outcome.allow ? "allow" : fallbackDisposition),
|
|
2771
|
+
reason: outcome.reason,
|
|
2772
|
+
details: outcome.details
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// src/guardrails/index.ts
|
|
2779
|
+
function createRegexGuardrail(pattern, options) {
|
|
2780
|
+
const rule = regexRule({ pattern, ...options?.rule || {} });
|
|
2781
|
+
return createGuardrail({
|
|
2782
|
+
id: options?.guardrailId,
|
|
2783
|
+
title: options?.guardrailTitle || options?.rule?.title || "Regex Guardrail",
|
|
2784
|
+
description: options?.guardrailDescription || options?.rule?.description,
|
|
2785
|
+
appliesTo: options?.phases,
|
|
2786
|
+
checks: [rule],
|
|
2787
|
+
haltOnViolation: options?.haltOnViolation,
|
|
2788
|
+
onViolation: options?.onViolation,
|
|
2789
|
+
metadata: options?.metadata
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
function createJsonGuardrail(schema, options) {
|
|
2793
|
+
const rule = jsonSchemaRule({ schema, ...options?.rule || {} });
|
|
2794
|
+
return createGuardrail({
|
|
2795
|
+
id: options?.guardrailId,
|
|
2796
|
+
title: options?.guardrailTitle || options?.rule?.title || "JSON Schema Guardrail",
|
|
2797
|
+
description: options?.guardrailDescription || options?.rule?.description,
|
|
2798
|
+
appliesTo: options?.phases,
|
|
2799
|
+
checks: [rule],
|
|
2800
|
+
haltOnViolation: options?.haltOnViolation,
|
|
2801
|
+
onViolation: options?.onViolation,
|
|
2802
|
+
metadata: options?.metadata
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
function createCodeGuardrail(options) {
|
|
2806
|
+
const ruleOptions = { disposition: "warn", ...options?.rule || {} };
|
|
2807
|
+
const rule = codePresenceRule(ruleOptions);
|
|
2808
|
+
return createGuardrail({
|
|
2809
|
+
id: options?.guardrailId,
|
|
2810
|
+
title: options?.guardrailTitle || ruleOptions.title || "Code Presence Guardrail",
|
|
2811
|
+
description: options?.guardrailDescription || ruleOptions.description,
|
|
2812
|
+
appliesTo: options?.phases,
|
|
2813
|
+
checks: [rule],
|
|
2814
|
+
haltOnViolation: options?.haltOnViolation,
|
|
2815
|
+
onViolation: options?.onViolation,
|
|
2816
|
+
metadata: options?.metadata
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
export { GuardrailPhase, agentVerdictRule, applyTokenLimits, buildSystemPrompt, captureSnapshot, codePresenceRule, cognipeerSink, countApproxTokens, createAgent, createAgentNode, createCodeGuardrail, createContextSummarizeNode, createContextTools, createGuardrail, createJsonGuardrail, createRegexGuardrail, createResolverNode, createSmartAgent, createTool, createToolLimitFinalizeNode, createToolsNode, customCallbackRule, customSink, evaluateGuardrails, fileSink, fromLangchainModel, fromLangchainTools, httpSink, isSmartModel, jsonSchemaRule, regexRule, resolveToolApprovalState, restoreSnapshot, withTools };
|
|
2821
|
+
//# sourceMappingURL=index.mjs.map
|
|
2822
|
+
//# sourceMappingURL=index.mjs.map
|