@nodetool-ai/llm-nodes 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +52 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/index.d.ts +18 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +22 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/nodes/agent-control-tools.d.ts +22 -0
  8. package/dist/nodes/agent-control-tools.d.ts.map +1 -0
  9. package/dist/nodes/agent-control-tools.js +123 -0
  10. package/dist/nodes/agent-control-tools.js.map +1 -0
  11. package/dist/nodes/agent-loop.d.ts +73 -0
  12. package/dist/nodes/agent-loop.d.ts.map +1 -0
  13. package/dist/nodes/agent-loop.js +168 -0
  14. package/dist/nodes/agent-loop.js.map +1 -0
  15. package/dist/nodes/agent-recommended-models.d.ts +152 -0
  16. package/dist/nodes/agent-recommended-models.d.ts.map +1 -0
  17. package/dist/nodes/agent-recommended-models.js +656 -0
  18. package/dist/nodes/agent-recommended-models.js.map +1 -0
  19. package/dist/nodes/agent-thinking.d.ts +29 -0
  20. package/dist/nodes/agent-thinking.d.ts.map +1 -0
  21. package/dist/nodes/agent-thinking.js +139 -0
  22. package/dist/nodes/agent-thinking.js.map +1 -0
  23. package/dist/nodes/agent-threads.d.ts +17 -0
  24. package/dist/nodes/agent-threads.d.ts.map +1 -0
  25. package/dist/nodes/agent-threads.js +93 -0
  26. package/dist/nodes/agent-threads.js.map +1 -0
  27. package/dist/nodes/agent-tool-hydration.d.ts +50 -0
  28. package/dist/nodes/agent-tool-hydration.d.ts.map +1 -0
  29. package/dist/nodes/agent-tool-hydration.js +106 -0
  30. package/dist/nodes/agent-tool-hydration.js.map +1 -0
  31. package/dist/nodes/agent-utils.d.ts +174 -0
  32. package/dist/nodes/agent-utils.d.ts.map +1 -0
  33. package/dist/nodes/agent-utils.js +553 -0
  34. package/dist/nodes/agent-utils.js.map +1 -0
  35. package/dist/nodes/agents.d.ts +402 -0
  36. package/dist/nodes/agents.d.ts.map +1 -0
  37. package/dist/nodes/agents.js +1491 -0
  38. package/dist/nodes/agents.js.map +1 -0
  39. package/dist/nodes/director.d.ts +126 -0
  40. package/dist/nodes/director.d.ts.map +1 -0
  41. package/dist/nodes/director.js +337 -0
  42. package/dist/nodes/director.js.map +1 -0
  43. package/dist/nodes/gemini.d.ts +171 -0
  44. package/dist/nodes/gemini.d.ts.map +1 -0
  45. package/dist/nodes/gemini.js +985 -0
  46. package/dist/nodes/gemini.js.map +1 -0
  47. package/dist/nodes/generators.d.ts +162 -0
  48. package/dist/nodes/generators.d.ts.map +1 -0
  49. package/dist/nodes/generators.js +966 -0
  50. package/dist/nodes/generators.js.map +1 -0
  51. package/dist/nodes/mistral.d.ts +109 -0
  52. package/dist/nodes/mistral.d.ts.map +1 -0
  53. package/dist/nodes/mistral.js +447 -0
  54. package/dist/nodes/mistral.js.map +1 -0
  55. package/dist/nodes/openai.d.ts +292 -0
  56. package/dist/nodes/openai.d.ts.map +1 -0
  57. package/dist/nodes/openai.js +1806 -0
  58. package/dist/nodes/openai.js.map +1 -0
  59. package/dist/nodes/provider-spend.d.ts +45 -0
  60. package/dist/nodes/provider-spend.d.ts.map +1 -0
  61. package/dist/nodes/provider-spend.js +69 -0
  62. package/dist/nodes/provider-spend.js.map +1 -0
  63. package/dist/nodes/shots.d.ts +108 -0
  64. package/dist/nodes/shots.d.ts.map +1 -0
  65. package/dist/nodes/shots.js +378 -0
  66. package/dist/nodes/shots.js.map +1 -0
  67. package/dist/nodes/xai.d.ts +99 -0
  68. package/dist/nodes/xai.d.ts.map +1 -0
  69. package/dist/nodes/xai.js +360 -0
  70. package/dist/nodes/xai.js.map +1 -0
  71. package/package.json +111 -0
@@ -0,0 +1,1491 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { createLogger, getEnv } from "@nodetool-ai/config";
8
+ import { BaseNode, prop } from "@nodetool-ai/node-sdk";
9
+ import { budgetFromContext, createRunBudget } from "@nodetool-ai/runtime";
10
+ import { isChunk } from "@nodetool-ai/protocol";
11
+ import { tagAsServer } from "@nodetool-ai/nodes-utils";
12
+ import { isCallable, isNonEmptyString, isObjectLike, isString } from "@nodetool-ai/node-sdk";
13
+ import { asText, makeThreadId, getCategories, getModelConfig, generateStructured, parseCategory, normalizeMessage, buildUserMessage, userMessageWithMedia, uniqueToolName, normalizeTools, gateAgentTools, classifyProviderStream, toProviderTools, serializeToolResult, toolCallChunk, streamProviderMessages, getStructuredOutputSchema, hasContentType, hasProviderAccess } from "./agent-utils.js";
14
+ import { seedFallbackThread, loadThreadMessages, saveThreadMessage } from "./agent-threads.js";
15
+ import { RedactedThinkingStreamSplitter, extractThinkTags, yieldSplitThinkChunks } from "./agent-thinking.js";
16
+ import { meterProviderSpend } from "./provider-spend.js";
17
+ import { buildControlTools } from "./agent-control-tools.js";
18
+ import { gateFromContext } from "@nodetool-ai/agents";
19
+ import { SUMMARIZER_RECOMMENDED_MODELS, ENHANCE_PROMPT_RECOMMENDED_MODELS, EXTRACTOR_RECOMMENDED_MODELS, CLASSIFIER_RECOMMENDED_MODELS, AGENT_RECOMMENDED_MODELS } from "./agent-recommended-models.js";
20
+ // Re-exports so existing consumers of `@nodetool-ai/llm-nodes` (and the
21
+ // `@nodetool-ai/llm-nodes/agents` subpath) keep finding every symbol this
22
+ // module exported before the split into flat sibling modules.
23
+ //
24
+ // `expandAssetReferences`'s canonical home is `@nodetool-ai/runtime`'s
25
+ // `prompt-asset-refs` module, alongside the shared `findAssetRefs` parser.
26
+ export { expandAssetReferences } from "@nodetool-ai/runtime";
27
+ export { streamProviderMessages, toProviderTools, serializeToolResult };
28
+ export { runAgentLoop } from "./agent-loop.js";
29
+ const log = createLogger("nodetool.base-nodes.agents");
30
+ /**
31
+ * An install-wide agent bound read from the environment, or its documented
32
+ * default. These two have no node prop of their own: an unpriced model's
33
+ * prompt ceiling and the run's concurrency bound are install policy, not
34
+ * per-node authoring decisions. Names and defaults come from
35
+ * `settingCatalog()` in `@nodetool-ai/config`.
36
+ */
37
+ function agentEnvInt(key, fallback) {
38
+ const parsed = Number(getEnv(key));
39
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
40
+ }
41
+ const DEFAULT_SYSTEM_PROMPT = "You are a friendly assistant";
42
+ const AGENT_DEFAULT_MAX_TOKENS = 16_384;
43
+ const EXTRACTOR_SYSTEM_PROMPT = [
44
+ "You are a precise structured data extractor.",
45
+ "Call the extraction tool exactly once with the extracted fields.",
46
+ "Use only information present in the input; do not invent facts."
47
+ ].join(" ");
48
+ const CLASSIFIER_SYSTEM_PROMPT = [
49
+ "You are a precise classifier.",
50
+ "Choose exactly one category from the allowed list.",
51
+ 'Return only JSON matching {"category":"<allowed-category>"} with no extra text.'
52
+ ].join(" ");
53
+ const DECISION_SYSTEM_PROMPT = [
54
+ "You make one yes/no decision about the inputs you are given.",
55
+ "Answer the question by calling the decision tool exactly once:",
56
+ "decision is true for yes and false for no, and reason is one short sentence explaining it.",
57
+ "Base the decision only on the inputs. If the inputs do not settle the question, answer false."
58
+ ].join(" ");
59
+ const DECISION_MAX_TOKENS = 512;
60
+ const SUMMARIZER_SYSTEM_PROMPT = "You are an expert summarizer. Produce a concise, accurate summary.";
61
+ const ENHANCE_PROMPT_SYSTEM_PROMPT = [
62
+ "You are an expert prompt engineer.",
63
+ "Rewrite the user's draft into a single, clear, detailed prompt that gets better results.",
64
+ "Preserve the original intent, add useful specificity, and remove ambiguity.",
65
+ "Return only the improved prompt with no preamble, explanation, or surrounding quotation marks."
66
+ ].join(" ");
67
+ // Per-target guidance appended to the user message so the rewrite is tuned to
68
+ // what the prompt will ultimately drive (an LLM, an image model, etc.).
69
+ const ENHANCE_PROMPT_GUIDANCE = {
70
+ general: "Make it specific and unambiguous. State the goal, relevant context, the desired output format, and any constraints.",
71
+ text: "Optimize for a text/LLM model: state the task, audience, tone, length, output structure, and any constraints.",
72
+ image: "Optimize for an image generation model: describe the subject, setting, composition, lighting, color, art style, mood, and quality descriptors in vivid, concrete detail.",
73
+ video: "Optimize for a video generation model: describe the subject and action, camera movement, shot framing, pacing, setting, lighting, and visual style.",
74
+ audio: "Optimize for an audio/music generation model: describe the genre, mood, instruments, tempo, vocals, and production style.",
75
+ code: "Optimize for a coding model: specify the language, the precise task, inputs and outputs, edge cases, and any libraries or constraints to use."
76
+ };
77
+ const ENHANCE_PROMPT_MAX_TOKENS = 1024;
78
+ export class SummarizerNode extends BaseNode {
79
+ static nodeType = "nodetool.agents.Summarizer";
80
+ static body = "content_card";
81
+ // Persist the primary text output as a generation so the node gets a
82
+ // reload-surviving, browsable history like generative media nodes.
83
+ static autoSaveAsset = true;
84
+ static title = "Summarizer";
85
+ static description = "Generate concise summaries of text content using LLM providers with streaming output.\n text, summarization, nlp, content, streaming\n\n Specialized for creating high-quality summaries with real-time streaming:\n - Condensing long documents into key points\n - Creating executive summaries with live output\n - Extracting main ideas from text as they're generated\n - Maintaining factual accuracy while reducing length";
86
+ static metadataOutputTypes = {
87
+ text: "str",
88
+ chunk: "chunk"
89
+ };
90
+ static inlineFields = ["text"];
91
+ static inputFields = ["image", "audio", "system_prompt"];
92
+ // Streamed output: each provider piece is emitted as a `chunk` iteration,
93
+ // and the final aggregated summary is the `text` single output.
94
+ static outputCorrelation = {
95
+ text: { kind: "single", source: "__execution__" },
96
+ chunk: { kind: "iteration", source: "__execution__", group: "stream" }
97
+ };
98
+ static recommendedModels = SUMMARIZER_RECOMMENDED_MODELS;
99
+ _maxSentences() {
100
+ const raw = Number(this.max_sentences ?? 3);
101
+ return Number.isFinite(raw) ? Math.max(1, raw) : 3;
102
+ }
103
+ async *genProcess(context) {
104
+ const text = asText(this.text ?? "");
105
+ const maxSentences = this._maxSentences();
106
+ const systemPrompt = asText(this.system_prompt ?? "").trim() || SUMMARIZER_SYSTEM_PROMPT;
107
+ const { providerId, modelId } = getModelConfig(this.serialize());
108
+ if (!providerId || !modelId) {
109
+ throw new Error("Select a model");
110
+ }
111
+ if (!hasProviderAccess(context)) {
112
+ throw new Error("Processing context is required");
113
+ }
114
+ const provider = await context.getProvider(providerId);
115
+ // Route streamed text through the think-tag splitter so a local model that
116
+ // emits <think>…</think> never leaks reasoning into the summary. This node
117
+ // has no `thinking` output, so the split-off thinking parts are dropped.
118
+ const splitter = new RedactedThinkingStreamSplitter();
119
+ const emitText = (piece) => piece
120
+ ? {
121
+ chunk: {
122
+ type: "chunk",
123
+ content: piece,
124
+ content_type: "text",
125
+ done: false
126
+ },
127
+ text: null
128
+ }
129
+ : null;
130
+ let full = "";
131
+ const spend = meterProviderSpend(context, provider, modelId);
132
+ try {
133
+ for await (const item of streamProviderMessages(provider, {
134
+ messages: [
135
+ { role: "system", content: systemPrompt },
136
+ userMessageWithMedia(`Summarize the following text in about ${maxSentences} sentence(s):\n\n${text}`, this.image, this.audio)
137
+ ],
138
+ model: modelId,
139
+ maxTokens: Math.max(64, maxSentences * 128)
140
+ })) {
141
+ if (isChunk(item) && !item.thinking) {
142
+ const piece = isString(item.content) ? item.content : "";
143
+ if (piece) {
144
+ full += piece;
145
+ for (const part of splitter.feed(piece)) {
146
+ if (part.kind === "text") {
147
+ const out = emitText(part.content);
148
+ if (out)
149
+ yield out;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ finally {
157
+ spend.report();
158
+ }
159
+ for (const part of splitter.flush()) {
160
+ if (part.kind === "text") {
161
+ const out = emitText(part.content);
162
+ if (out)
163
+ yield out;
164
+ }
165
+ }
166
+ const summary = extractThinkTags(full).text.trim();
167
+ yield { chunk: null, text: summary, output: summary };
168
+ }
169
+ async process(context) {
170
+ let text = "";
171
+ for await (const item of this.genProcess(context)) {
172
+ if (isString(item.text))
173
+ text = item.text;
174
+ }
175
+ return { text, output: text };
176
+ }
177
+ }
178
+ __decorate([
179
+ prop({
180
+ type: "str",
181
+ default: "\n You are an expert summarizer. Your task is to create clear, accurate, and concise summaries using Markdown for structuring.\n Follow these guidelines:\n 1. Identify and include only the most important information.\n 2. Maintain factual accuracy - do not add or modify information.\n 3. Use clear, direct language.\n 4. Keep the summary brief and to the point.\n ",
182
+ title: "System Prompt",
183
+ description: "The system prompt for the summarizer"
184
+ })
185
+ ], SummarizerNode.prototype, "system_prompt", void 0);
186
+ __decorate([
187
+ prop({
188
+ type: "language_model",
189
+ default: {
190
+ type: "language_model",
191
+ provider: "empty",
192
+ id: "",
193
+ name: "",
194
+ path: null,
195
+ supported_tasks: []
196
+ },
197
+ title: "Model",
198
+ description: "Model to use for summarization"
199
+ })
200
+ ], SummarizerNode.prototype, "model", void 0);
201
+ __decorate([
202
+ prop({
203
+ type: "str",
204
+ default: "",
205
+ title: "Text",
206
+ description: "The text to summarize"
207
+ })
208
+ ], SummarizerNode.prototype, "text", void 0);
209
+ __decorate([
210
+ prop({
211
+ type: "image",
212
+ default: {
213
+ type: "image",
214
+ uri: "",
215
+ asset_id: null,
216
+ data: null,
217
+ metadata: null
218
+ },
219
+ title: "Image",
220
+ description: "Optional image to condition the summary"
221
+ })
222
+ ], SummarizerNode.prototype, "image", void 0);
223
+ __decorate([
224
+ prop({
225
+ type: "audio",
226
+ default: {
227
+ type: "audio",
228
+ uri: "",
229
+ asset_id: null,
230
+ data: null,
231
+ metadata: null
232
+ },
233
+ title: "Audio",
234
+ description: "Optional audio to condition the summary"
235
+ })
236
+ ], SummarizerNode.prototype, "audio", void 0);
237
+ __decorate([
238
+ prop({
239
+ type: "int",
240
+ default: 3,
241
+ title: "Max Sentences",
242
+ description: "Approximate number of sentences for the summary.",
243
+ min: 1,
244
+ max: 100
245
+ })
246
+ ], SummarizerNode.prototype, "max_sentences", void 0);
247
+ export class EnhancePromptNode extends BaseNode {
248
+ static nodeType = "nodetool.agents.EnhancePrompt";
249
+ static body = "content_card";
250
+ // Persist the improved prompt as a generation so the node keeps a
251
+ // reload-surviving, browsable history like the other content-card nodes.
252
+ static autoSaveAsset = true;
253
+ static title = "Enhance Prompt";
254
+ static description = "Rewrite a rough draft into a clearer, more detailed prompt using an LLM, with streaming output.\n text, prompt, prompt-engineering, llm, rewrite, streaming\n\n Turn a short or vague idea into an effective prompt:\n - Add specificity, structure, and missing detail\n - Tune the result for the target model (text, image, video, audio, code)\n - Preserve the original intent while removing ambiguity\n - Stream the improved prompt as it is written";
255
+ static metadataOutputTypes = {
256
+ text: "str",
257
+ chunk: "chunk"
258
+ };
259
+ static inlineFields = ["prompt"];
260
+ static inputFields = ["target", "system_prompt"];
261
+ // Streamed output: each provider piece is emitted as a `chunk` iteration,
262
+ // and the final improved prompt is the `text` single output.
263
+ static outputCorrelation = {
264
+ text: { kind: "single", source: "__execution__" },
265
+ chunk: { kind: "iteration", source: "__execution__", group: "stream" }
266
+ };
267
+ static recommendedModels = ENHANCE_PROMPT_RECOMMENDED_MODELS;
268
+ _target() {
269
+ const raw = isString(this.target) ? this.target.trim() : "";
270
+ return raw in ENHANCE_PROMPT_GUIDANCE ? raw : "general";
271
+ }
272
+ async *genProcess(context) {
273
+ const prompt = asText(this.prompt ?? "").trim();
274
+ const target = this._target();
275
+ const systemPrompt = asText(this.system_prompt ?? "").trim() || ENHANCE_PROMPT_SYSTEM_PROMPT;
276
+ const { providerId, modelId } = getModelConfig(this.serialize());
277
+ if (!prompt) {
278
+ yield { chunk: null, text: "", output: "" };
279
+ return;
280
+ }
281
+ if (!providerId || !modelId) {
282
+ throw new Error("Select a model");
283
+ }
284
+ if (!hasProviderAccess(context)) {
285
+ throw new Error("Processing context is required");
286
+ }
287
+ const provider = await context.getProvider(providerId);
288
+ const guidance = ENHANCE_PROMPT_GUIDANCE[target] ?? ENHANCE_PROMPT_GUIDANCE.general;
289
+ // Route streamed text through the think-tag splitter so a local model that
290
+ // emits <think>…</think> never leaks reasoning into the improved prompt.
291
+ // This node has no `thinking` output, so those parts are dropped.
292
+ const splitter = new RedactedThinkingStreamSplitter();
293
+ const emitText = (piece) => piece
294
+ ? {
295
+ chunk: {
296
+ type: "chunk",
297
+ content: piece,
298
+ content_type: "text",
299
+ done: false
300
+ },
301
+ text: null
302
+ }
303
+ : null;
304
+ let full = "";
305
+ const spend = meterProviderSpend(context, provider, modelId);
306
+ try {
307
+ for await (const item of streamProviderMessages(provider, {
308
+ messages: [
309
+ { role: "system", content: systemPrompt },
310
+ {
311
+ role: "user",
312
+ content: `${guidance}\n\nImprove this prompt and return only the improved version:\n\n${prompt}`
313
+ }
314
+ ],
315
+ model: modelId,
316
+ maxTokens: ENHANCE_PROMPT_MAX_TOKENS
317
+ })) {
318
+ if (isChunk(item) && !item.thinking) {
319
+ const piece = isString(item.content) ? item.content : "";
320
+ if (piece) {
321
+ full += piece;
322
+ for (const part of splitter.feed(piece)) {
323
+ if (part.kind === "text") {
324
+ const out = emitText(part.content);
325
+ if (out)
326
+ yield out;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ finally {
334
+ spend.report();
335
+ }
336
+ for (const part of splitter.flush()) {
337
+ if (part.kind === "text") {
338
+ const out = emitText(part.content);
339
+ if (out)
340
+ yield out;
341
+ }
342
+ }
343
+ const enhanced = extractThinkTags(full).text.trim() || prompt;
344
+ yield { chunk: null, text: enhanced, output: enhanced };
345
+ }
346
+ async process(context) {
347
+ let text = "";
348
+ for await (const item of this.genProcess(context)) {
349
+ if (isString(item.text))
350
+ text = item.text;
351
+ }
352
+ return { text, output: text };
353
+ }
354
+ }
355
+ __decorate([
356
+ prop({
357
+ type: "str",
358
+ default: ENHANCE_PROMPT_SYSTEM_PROMPT,
359
+ title: "System Prompt",
360
+ description: "The system prompt that guides how prompts are enhanced"
361
+ })
362
+ ], EnhancePromptNode.prototype, "system_prompt", void 0);
363
+ __decorate([
364
+ prop({
365
+ type: "language_model",
366
+ default: {
367
+ type: "language_model",
368
+ provider: "empty",
369
+ id: "",
370
+ name: "",
371
+ path: null,
372
+ supported_tasks: []
373
+ },
374
+ title: "Model",
375
+ description: "Model to use for enhancing the prompt"
376
+ })
377
+ ], EnhancePromptNode.prototype, "model", void 0);
378
+ __decorate([
379
+ prop({
380
+ type: "str",
381
+ default: "",
382
+ title: "Prompt",
383
+ description: "The draft prompt to enhance"
384
+ })
385
+ ], EnhancePromptNode.prototype, "prompt", void 0);
386
+ __decorate([
387
+ prop({
388
+ type: "enum",
389
+ default: "general",
390
+ title: "Target",
391
+ description: "What the enhanced prompt is for. Tailors the rewrite to the target model: general purpose, a text/LLM model, or an image, video, audio, or code generation model.",
392
+ values: ["general", "text", "image", "video", "audio", "code"]
393
+ })
394
+ ], EnhancePromptNode.prototype, "target", void 0);
395
+ export class CreateThreadNode extends BaseNode {
396
+ static nodeType = "nodetool.agents.CreateThread";
397
+ static title = "Create Thread";
398
+ static inlineFields = ["title"];
399
+ static inputFields = [];
400
+ static description = "Create a new conversation thread and return its ID.\n threads, chat, conversation, context\n\n Use this to seed a thread_id that downstream Agent nodes can reuse for\n persistent history across the graph or multiple runs.";
401
+ static metadataOutputTypes = {
402
+ thread_id: "str"
403
+ };
404
+ async process(context) {
405
+ const requested = String(this.thread_id ?? "").trim();
406
+ const title = String(this.title ?? "Agent Conversation");
407
+ const id = requested || makeThreadId();
408
+ // Threads are implicit in the message store: messages are keyed by
409
+ // thread_id with no separate thread row, so when a real store is wired we
410
+ // just mint/return the id and the first saved message creates the thread.
411
+ // Only the in-memory fallback needs an explicit empty thread seeded so a
412
+ // later loadThreadMessages finds it (and so a reused id survives eviction).
413
+ if (!context?.hasModelInterface?.("createMessage")) {
414
+ seedFallbackThread(id, title);
415
+ }
416
+ return { thread_id: id };
417
+ }
418
+ }
419
+ __decorate([
420
+ prop({
421
+ type: "str",
422
+ default: "Agent Conversation",
423
+ title: "Title",
424
+ description: "Optional title for the new thread"
425
+ })
426
+ ], CreateThreadNode.prototype, "title", void 0);
427
+ __decorate([
428
+ prop({
429
+ type: "str",
430
+ default: "",
431
+ title: "Thread Id",
432
+ description: "Optional custom thread ID. If provided and owned by the user, it will be reused; otherwise a new thread is created."
433
+ })
434
+ ], CreateThreadNode.prototype, "thread_id", void 0);
435
+ export class ExtractorNode extends BaseNode {
436
+ static nodeType = "nodetool.agents.Extractor";
437
+ static body = "content_card";
438
+ static title = "Extractor";
439
+ static description = "Extract structured data from text content using LLM providers.\n data-extraction, structured-data, nlp, parsing\n\n Specialized for extracting structured information:\n - Converting unstructured text into structured data\n - Identifying and extracting specific fields from documents\n - Parsing text according to predefined schemas\n - Creating structured records from natural language content";
440
+ static inlineFields = ["text"];
441
+ static inputFields = ["image", "audio", "system_prompt"];
442
+ static supportsDynamicOutputs = true;
443
+ static recommendedModels = EXTRACTOR_RECOMMENDED_MODELS;
444
+ async process(context) {
445
+ const text = asText(this.text ?? "");
446
+ const { providerId, modelId } = getModelConfig(this.serialize());
447
+ if (!providerId || !modelId) {
448
+ throw new Error("Select a model");
449
+ }
450
+ if (!hasProviderAccess(context)) {
451
+ throw new Error("Processing context is required");
452
+ }
453
+ const provider = await context.getProvider(providerId);
454
+ const schema = getStructuredOutputSchema(this) ?? {
455
+ type: "object",
456
+ properties: { output: { type: "string" } },
457
+ required: ["output"],
458
+ additionalProperties: true
459
+ };
460
+ const spend = meterProviderSpend(context, provider, modelId);
461
+ let result;
462
+ try {
463
+ result = await generateStructured(provider, {
464
+ model: modelId,
465
+ maxTokens: Number(this.max_tokens ?? 1024),
466
+ messages: [
467
+ {
468
+ role: "system",
469
+ content: asText(this.system_prompt ?? "").trim() || EXTRACTOR_SYSTEM_PROMPT
470
+ },
471
+ userMessageWithMedia(text, this.image, this.audio)
472
+ ],
473
+ toolName: "extraction_result",
474
+ toolDescription: "Submit the extracted data.",
475
+ schema
476
+ });
477
+ }
478
+ finally {
479
+ spend.report();
480
+ }
481
+ if (result)
482
+ return result;
483
+ throw new Error("Extractor: the model did not return structured data for the extraction tool.");
484
+ }
485
+ }
486
+ __decorate([
487
+ prop({
488
+ type: "str",
489
+ default: '\nYou are a precise structured data extractor.\n\nGoal\n- Extract exactly the fields described by the extraction tool\'s schema from the content in <TEXT> (and any attached media).\n\nHow to respond (MANDATORY)\n- Call the extraction tool exactly once, passing the extracted fields as its arguments.\n- Do not answer in prose; the tool call is the only output.\n\nExtraction rules\n- Use only information found in <TEXT> or attached media. Do not invent facts.\n- Preserve source values; normalize internal whitespace and trim leading/trailing spaces.\n- If a required field is missing or not explicitly stated, return the closest reasonable default consistent with its type:\n - string: ""\n - number: 0\n - boolean: false\n - array/object: empty value of that type (only if allowed by the schema)\n- Dates/times: prefer ISO 8601 when the schema type is string and the value represents a date/time.\n- If multiple candidates exist, choose the most precise and unambiguous one.\n',
490
+ title: "System Prompt",
491
+ description: "The system prompt for the data extractor"
492
+ })
493
+ ], ExtractorNode.prototype, "system_prompt", void 0);
494
+ __decorate([
495
+ prop({
496
+ type: "language_model",
497
+ default: {
498
+ type: "language_model",
499
+ provider: "empty",
500
+ id: "",
501
+ name: "",
502
+ path: null,
503
+ supported_tasks: []
504
+ },
505
+ title: "Model",
506
+ description: "Model to use for data extraction"
507
+ })
508
+ ], ExtractorNode.prototype, "model", void 0);
509
+ __decorate([
510
+ prop({
511
+ type: "str",
512
+ default: "",
513
+ title: "Text",
514
+ description: "The text to extract data from"
515
+ })
516
+ ], ExtractorNode.prototype, "text", void 0);
517
+ __decorate([
518
+ prop({
519
+ type: "image",
520
+ default: {
521
+ type: "image",
522
+ uri: "",
523
+ asset_id: null,
524
+ data: null,
525
+ metadata: null
526
+ },
527
+ title: "Image",
528
+ description: "Optional image to assist extraction"
529
+ })
530
+ ], ExtractorNode.prototype, "image", void 0);
531
+ __decorate([
532
+ prop({
533
+ type: "audio",
534
+ default: {
535
+ type: "audio",
536
+ uri: "",
537
+ asset_id: null,
538
+ data: null,
539
+ metadata: null
540
+ },
541
+ title: "Audio",
542
+ description: "Optional audio to assist extraction"
543
+ })
544
+ ], ExtractorNode.prototype, "audio", void 0);
545
+ __decorate([
546
+ prop({
547
+ type: "int",
548
+ default: 1024,
549
+ title: "Max Tokens",
550
+ description: "The maximum number of tokens to generate.",
551
+ min: 1,
552
+ max: 100000
553
+ })
554
+ ], ExtractorNode.prototype, "max_tokens", void 0);
555
+ export class ClassifierNode extends BaseNode {
556
+ static nodeType = "nodetool.agents.Classifier";
557
+ static body = "content_card";
558
+ // Persist the primary text output as a generation so the node gets a
559
+ // reload-surviving, browsable history like generative media nodes.
560
+ static autoSaveAsset = true;
561
+ static title = "Classifier";
562
+ static description = "Classify text into predefined or dynamic categories using LLM.\n classification, nlp, categorization\n\n Use cases:\n - Sentiment analysis\n - Topic classification\n - Intent detection\n - Content categorization";
563
+ static metadataOutputTypes = {
564
+ output: "str"
565
+ };
566
+ static inlineFields = ["text"];
567
+ static inputFields = ["image", "audio", "system_prompt"];
568
+ static recommendedModels = CLASSIFIER_RECOMMENDED_MODELS;
569
+ async process(context) {
570
+ const text = asText(this.text ?? "");
571
+ const categories = getCategories(this.categories);
572
+ if (categories.length < 2) {
573
+ throw new Error("At least 2 categories are required");
574
+ }
575
+ const { providerId, modelId } = getModelConfig(this.serialize());
576
+ if (!providerId || !modelId) {
577
+ throw new Error("Select a model");
578
+ }
579
+ if (!hasProviderAccess(context)) {
580
+ throw new Error("Processing context is required");
581
+ }
582
+ const provider = await context.getProvider(providerId);
583
+ const spend = meterProviderSpend(context, provider, modelId);
584
+ let result;
585
+ try {
586
+ result = await generateStructured(provider, {
587
+ model: modelId,
588
+ maxTokens: Number(this.max_tokens ?? 256),
589
+ messages: [
590
+ {
591
+ role: "system",
592
+ content: asText(this.system_prompt ?? "").trim() || CLASSIFIER_SYSTEM_PROMPT
593
+ },
594
+ userMessageWithMedia(`Allowed categories: ${categories.join(", ")}\n\nText: ${text}`, this.image, this.audio)
595
+ ],
596
+ toolName: "classification_result",
597
+ toolDescription: "Submit the classification result.",
598
+ schema: {
599
+ type: "object",
600
+ properties: {
601
+ category: { type: "string", enum: categories }
602
+ },
603
+ required: ["category"]
604
+ }
605
+ });
606
+ }
607
+ finally {
608
+ spend.report();
609
+ }
610
+ const category = parseCategory(result ? String(result.category ?? "") : "", categories);
611
+ return { output: category, category };
612
+ }
613
+ }
614
+ __decorate([
615
+ prop({
616
+ type: "str",
617
+ default: '\nYou are a precise classifier.\n\nGoal\n- Select exactly one category from the list provided by the user.\n\nOutput format (MANDATORY)\n- Return ONLY a single JSON object with this exact schema and nothing else:\n {"category": "<one-of-the-allowed-categories>"}\n- No prose, no Markdown, no code fences, no explanations, no extra keys.\n\nSelection criteria\n- Choose the single best category that captures the main intent of the text.\n- If multiple categories seem plausible, pick the most probable one; do not return multiple.\n- If none fit perfectly, choose the closest allowed category. If the list includes "Other" or "Unknown", prefer it when appropriate.\n- Be robust to casing, punctuation, emojis, and minor typos. Handle negation correctly (e.g., "not spam" ≠ spam).\n- Never invent categories that are not in the provided list.\n\nBehavior\n- Be deterministic for the same input.\n- Do not ask clarifying questions; make the best choice with what\'s given.\n',
618
+ title: "System Prompt",
619
+ description: "The system prompt for the classifier"
620
+ })
621
+ ], ClassifierNode.prototype, "system_prompt", void 0);
622
+ __decorate([
623
+ prop({
624
+ type: "language_model",
625
+ default: {
626
+ type: "language_model",
627
+ provider: "empty",
628
+ id: "",
629
+ name: "",
630
+ path: null,
631
+ supported_tasks: []
632
+ },
633
+ title: "Model",
634
+ description: "Model to use for classification"
635
+ })
636
+ ], ClassifierNode.prototype, "model", void 0);
637
+ __decorate([
638
+ prop({
639
+ type: "str",
640
+ default: "",
641
+ title: "Text",
642
+ description: "Text to classify"
643
+ })
644
+ ], ClassifierNode.prototype, "text", void 0);
645
+ __decorate([
646
+ prop({
647
+ type: "image",
648
+ default: {
649
+ type: "image",
650
+ uri: "",
651
+ asset_id: null,
652
+ data: null,
653
+ metadata: null
654
+ },
655
+ title: "Image",
656
+ description: "Optional image to classify in context"
657
+ })
658
+ ], ClassifierNode.prototype, "image", void 0);
659
+ __decorate([
660
+ prop({
661
+ type: "audio",
662
+ default: {
663
+ type: "audio",
664
+ uri: "",
665
+ asset_id: null,
666
+ data: null,
667
+ metadata: null
668
+ },
669
+ title: "Audio",
670
+ description: "Optional audio to classify in context"
671
+ })
672
+ ], ClassifierNode.prototype, "audio", void 0);
673
+ __decorate([
674
+ prop({
675
+ type: "list[str]",
676
+ default: [],
677
+ title: "Categories",
678
+ description: "List of possible categories. If empty, LLM will determine categories."
679
+ })
680
+ ], ClassifierNode.prototype, "categories", void 0);
681
+ __decorate([
682
+ prop({
683
+ type: "int",
684
+ default: 256,
685
+ title: "Max Tokens",
686
+ description: "The maximum number of tokens to generate.",
687
+ min: 1,
688
+ max: 100000
689
+ })
690
+ ], ClassifierNode.prototype, "max_tokens", void 0);
691
+ function isMediaRef(value, type) {
692
+ return isObjectLike(value) && value.type === type;
693
+ }
694
+ /** A model's decision field as a boolean, or null when it is not one. */
695
+ function parseDecision(value) {
696
+ if (value === true || value === false)
697
+ return value;
698
+ const text = asText(value).trim().toLowerCase();
699
+ if (["true", "yes", "y", "1"].includes(text))
700
+ return true;
701
+ if (["false", "no", "n", "0"].includes(text))
702
+ return false;
703
+ return null;
704
+ }
705
+ /**
706
+ * The inputs a decision is about, as prompt text plus attached media. Image
707
+ * and audio refs (and lists of them) are attached to the message; every
708
+ * other value is rendered into an `<input>` block, JSON for structured data.
709
+ */
710
+ function describeDecisionInputs(inputs) {
711
+ const images = [];
712
+ const audios = [];
713
+ const blocks = [];
714
+ for (const [name, value] of inputs) {
715
+ if (value === null || value === undefined || value === "")
716
+ continue;
717
+ const items = Array.isArray(value) ? value : [value];
718
+ if (items.length > 0 && items.every((v) => isMediaRef(v, "image"))) {
719
+ images.push(...items);
720
+ blocks.push(`<input name="${name}">[${items.length} image(s) attached]</input>`);
721
+ continue;
722
+ }
723
+ if (items.length > 0 && items.every((v) => isMediaRef(v, "audio"))) {
724
+ audios.push(...items);
725
+ blocks.push(`<input name="${name}">[${items.length} audio clip(s) attached]</input>`);
726
+ continue;
727
+ }
728
+ const rendered = isString(value) ? value : JSON.stringify(value, null, 2);
729
+ blocks.push(`<input name="${name}">\n${rendered}\n</input>`);
730
+ }
731
+ return { text: blocks.join("\n"), images, audios };
732
+ }
733
+ /**
734
+ * An LLM answers a yes/no question about its inputs and routes `value` down
735
+ * the matching branch, like `If` with the condition written as a prompt.
736
+ * `decision` is the bool itself, so it can drive `If`, `Loop.condition`, or
737
+ * any bool input. Wire extra inputs as dynamic inputs; each is shown to the
738
+ * model by name. docs/workflow-loops.md.
739
+ */
740
+ export class DecisionNode extends BaseNode {
741
+ static nodeType = "nodetool.agents.Decision";
742
+ static body = "content_card";
743
+ static title = "Decision";
744
+ static description = "Let an LLM make a yes/no decision about the inputs, then route the value down the matching branch.\n decision, judge, condition, branch, if, evaluate, approve, verify, gate, loop, agent\n\n Write the question in prompt. Wire the value to route into value, and any other context as extra inputs; images and audio are shown to the model. decision carries the answer as a bool, reason explains it, and only the taken branch (if_true or if_false) emits value.\n\n Use cases:\n - Judge whether a draft or generated image meets a brief\n - Drive a Loop's condition from a model's judgement\n - Route items to different branches by a natural-language rule";
745
+ static metadataOutputTypes = {
746
+ decision: "bool",
747
+ reason: "str",
748
+ if_true: "any",
749
+ if_false: "any"
750
+ };
751
+ static inlineFields = ["prompt"];
752
+ static inputFields = ["value", "prompt", "system_prompt"];
753
+ static supportsDynamicInputs = true;
754
+ static recommendedModels = CLASSIFIER_RECOMMENDED_MODELS;
755
+ // Every output keys to the invocation, so a Decision inside a loop body or
756
+ // behind a ForEach answers once per item. `if_true`/`if_false` are not
757
+ // `forward` from `value`: `value` may be unwired.
758
+ static outputCorrelation = {
759
+ decision: { kind: "single", source: "__execution__" },
760
+ reason: { kind: "single", source: "__execution__" },
761
+ if_true: { kind: "single", source: "__execution__" },
762
+ if_false: { kind: "single", source: "__execution__" }
763
+ };
764
+ async process(context) {
765
+ const question = asText(this.prompt ?? "").trim();
766
+ if (!question) {
767
+ throw new Error("Decision needs a prompt: write the yes/no question to decide.");
768
+ }
769
+ const { providerId, modelId } = getModelConfig(this.serialize());
770
+ if (!providerId || !modelId) {
771
+ throw new Error("Select a model");
772
+ }
773
+ if (!hasProviderAccess(context)) {
774
+ throw new Error("Processing context is required");
775
+ }
776
+ const value = this.value ?? null;
777
+ const inputs = describeDecisionInputs([
778
+ ["value", value],
779
+ ...this.dynamicProps.entries()
780
+ ]);
781
+ const userText = inputs.text
782
+ ? `Question: ${question}\n\nInputs:\n${inputs.text}`
783
+ : `Question: ${question}`;
784
+ const provider = await context.getProvider(providerId);
785
+ const spend = meterProviderSpend(context, provider, modelId);
786
+ let result;
787
+ try {
788
+ result = await generateStructured(provider, {
789
+ model: modelId,
790
+ maxTokens: Number(this.max_tokens ?? DECISION_MAX_TOKENS),
791
+ messages: [
792
+ {
793
+ role: "system",
794
+ content: asText(this.system_prompt ?? "").trim() || DECISION_SYSTEM_PROMPT
795
+ },
796
+ userMessageWithMedia(userText, inputs.images, inputs.audios)
797
+ ],
798
+ toolName: "decision_result",
799
+ toolDescription: "Submit the yes/no decision and its reason.",
800
+ schema: {
801
+ type: "object",
802
+ properties: {
803
+ decision: {
804
+ type: "boolean",
805
+ description: "true for yes, false for no"
806
+ },
807
+ reason: {
808
+ type: "string",
809
+ description: "One short sentence explaining the decision"
810
+ }
811
+ },
812
+ required: ["decision", "reason"]
813
+ }
814
+ });
815
+ }
816
+ finally {
817
+ spend.report();
818
+ }
819
+ const decision = result ? parseDecision(result.decision) : null;
820
+ if (decision === null) {
821
+ throw new Error("Decision: the model did not return a yes/no decision for the decision tool.");
822
+ }
823
+ const reason = result ? asText(result.reason ?? "").trim() : "";
824
+ // Emit only the taken branch, like If: the other branch's inbox closes
825
+ // empty and its nodes are skipped.
826
+ return decision
827
+ ? { decision, reason, if_true: value }
828
+ : { decision, reason, if_false: value };
829
+ }
830
+ }
831
+ __decorate([
832
+ prop({
833
+ type: "language_model",
834
+ default: {
835
+ type: "language_model",
836
+ provider: "empty",
837
+ id: "",
838
+ name: "",
839
+ path: null,
840
+ supported_tasks: []
841
+ },
842
+ title: "Model",
843
+ description: "Model that makes the decision"
844
+ })
845
+ ], DecisionNode.prototype, "model", void 0);
846
+ __decorate([
847
+ prop({
848
+ type: "str",
849
+ default: "",
850
+ title: "Prompt",
851
+ description: 'The yes/no question to decide, e.g. "Does the draft answer every point in the brief?"'
852
+ })
853
+ ], DecisionNode.prototype, "prompt", void 0);
854
+ __decorate([
855
+ prop({
856
+ type: "any",
857
+ default: null,
858
+ title: "Value",
859
+ description: "The value the decision is about. Shown to the model and passed through on the taken branch."
860
+ })
861
+ ], DecisionNode.prototype, "value", void 0);
862
+ __decorate([
863
+ prop({
864
+ type: "str",
865
+ default: DECISION_SYSTEM_PROMPT,
866
+ title: "System Prompt",
867
+ description: "Instructions for how the model decides"
868
+ })
869
+ ], DecisionNode.prototype, "system_prompt", void 0);
870
+ __decorate([
871
+ prop({
872
+ type: "int",
873
+ default: DECISION_MAX_TOKENS,
874
+ title: "Max Tokens",
875
+ description: "The maximum number of tokens to generate.",
876
+ min: 1,
877
+ max: 100000
878
+ })
879
+ ], DecisionNode.prototype, "max_tokens", void 0);
880
+ export class AgentNode extends BaseNode {
881
+ static nodeType = "nodetool.agents.Agent";
882
+ static body = "content_card";
883
+ // Persist the primary text output as a generation so the node gets a
884
+ // reload-surviving, browsable history like generative media nodes.
885
+ static autoSaveAsset = true;
886
+ static title = "Agent";
887
+ static description = "Chat with an LLM: send a prompt (plus optional images or audio), call tools, and stream back the response.\n agent, llm, chat, text-generation, tools, streaming";
888
+ static metadataOutputTypes = {
889
+ text: "str",
890
+ chunk: "chunk",
891
+ thinking: "chunk",
892
+ audio: "audio"
893
+ };
894
+ static inlineFields = [];
895
+ static inputFields = ["prompt", "image", "audio"];
896
+ static supportsDynamicOutputs = true;
897
+ // Streamed outputs: each yielded chunk/thinking/audio is a distinct
898
+ // iteration step. Without this, the analyzer defaults to `single`, which
899
+ // gives every yield the same lineage key — downstream collapses to the
900
+ // last value and early chunks are dropped.
901
+ static outputCorrelation = {
902
+ text: { kind: "single", source: "__execution__" },
903
+ chunk: { kind: "iteration", source: "__execution__", group: "stream" },
904
+ thinking: { kind: "iteration", source: "__execution__", group: "stream" },
905
+ audio: { kind: "iteration", source: "__execution__", group: "stream" }
906
+ };
907
+ static recommendedModels = AGENT_RECOMMENDED_MODELS;
908
+ /**
909
+ * Build the tool list for this run. Override in subclasses to inject
910
+ * additional tools (e.g. sandbox shell/file/browser tools) alongside the
911
+ * user-selected ones. Control tools from kernel control edges are
912
+ * appended separately by the genProcess paths and don't need to be
913
+ * handled here.
914
+ *
915
+ * An override returns the tools ungated: `genProcess` wraps whatever comes
916
+ * back in the run's permission gate, so a subclass cannot inject a tool that
917
+ * skips the ladder.
918
+ */
919
+ async buildTools(context) {
920
+ return normalizeTools(this.tools ?? [], context);
921
+ }
922
+ async *genProcess(context) {
923
+ const { providerId, modelId } = getModelConfig(this.serialize());
924
+ log.info("AgentNode starting", {
925
+ nodeId: this.__node_id ?? null,
926
+ providerId,
927
+ modelId,
928
+ hasContext: Boolean(context),
929
+ hasGetProvider: hasProviderAccess(context),
930
+ propKeys: Object.keys(this.serialize())
931
+ });
932
+ if (!providerId || !modelId) {
933
+ log.error("AgentNode missing model selection", {
934
+ nodeId: this.__node_id ?? null,
935
+ providerId,
936
+ modelId,
937
+ modelInput: this.model ?? null,
938
+ modelProp: this.model ?? null
939
+ });
940
+ throw new Error("Select a model");
941
+ }
942
+ if (!hasProviderAccess(context)) {
943
+ log.error("AgentNode missing processing context or provider access", {
944
+ nodeId: this.__node_id ?? null,
945
+ providerId,
946
+ modelId
947
+ });
948
+ throw new Error("Processing context is required");
949
+ }
950
+ const prompt = asText(this.prompt ?? "");
951
+ let system = asText(this.system ?? DEFAULT_SYSTEM_PROMPT);
952
+ const images = this.image;
953
+ const audios = this.audio;
954
+ const historyInput = this.history;
955
+ const history = Array.isArray(historyInput)
956
+ ? historyInput
957
+ .map((item) => normalizeMessage(item))
958
+ .filter((item) => item !== null)
959
+ : [];
960
+ const threadId = String(this.thread_id ?? "").trim();
961
+ const maxTokens = Number(this.max_tokens ?? AGENT_DEFAULT_MAX_TOKENS);
962
+ const maxTurns = Math.max(1, Number(this.max_turns ?? 100));
963
+ // A prop that is not a number must not read as "no cap" — that is the one
964
+ // direction of this coercion that silently removes a ceiling.
965
+ const capProp = Number(this.cost_cap_usd);
966
+ const costCapUsd = Number.isFinite(capProp) ? capProp : 5;
967
+ const timeoutS = Math.max(1, Number(this.timeout_s) || 1200);
968
+ const inheritedBudget = budgetFromContext(context);
969
+ // Invariant I-2: one budget per run, shared downward. A node a chat turn
970
+ // started through `run_node` inherits that turn's budget and reserves
971
+ // against the same cap; only a node whose context carries none opens an
972
+ // allowance of its own. Creating one unconditionally would give every
973
+ // nested node a fresh $5 of headroom, which is no cap at all.
974
+ const budget = inheritedBudget ??
975
+ createRunBudget({
976
+ // A cap of zero would refuse every turn rather than lift the ceiling,
977
+ // so non-positive means "no USD cap".
978
+ capUsd: costCapUsd > 0 ? costCapUsd : null,
979
+ maxOutputTokens: maxTokens,
980
+ unpricedTokenCeiling: agentEnvInt("NODETOOL_AGENT_UNPRICED_TOKEN_CEILING", 400000),
981
+ deadlineMs: timeoutS * 1000,
982
+ maxConcurrency: agentEnvInt("NODETOOL_AGENT_MAX_CONCURRENCY", 8),
983
+ maxTurns
984
+ });
985
+ // One permission ladder on every host (invariant I-1). The gate is wrapped
986
+ // here rather than inside `buildTools` because `buildTools` is the hook
987
+ // subclasses override to inject their own tools: gating inside it would be
988
+ // skipped by any override that does not call `super`. A node whose context
989
+ // carries no host gate runs headless — `auto`, denying what the ladder
990
+ // itself escalates.
991
+ const tools = gateAgentTools(await this.buildTools(context), gateFromContext(context, "Agent node"));
992
+ // Build control tools from _control_context (injected by the kernel
993
+ // for nodes that have outgoing control edges). This lets the LLM
994
+ // call controlled nodes as tools.
995
+ const controlContext = this.getDynamic("_control_context");
996
+ const controlTools = buildControlTools(controlContext);
997
+ if (controlTools.length > 0) {
998
+ tools.push(...controlTools);
999
+ log.info("AgentNode added control tools", {
1000
+ nodeId: this.__node_id ?? null,
1001
+ controlToolNames: controlTools.map((t) => t.name),
1002
+ controlTargets: controlTools.map((t) => t.targetNodeId)
1003
+ });
1004
+ }
1005
+ const structuredSchema = getStructuredOutputSchema(this);
1006
+ let structuredResult = null;
1007
+ const structuredToolName = structuredSchema
1008
+ ? uniqueToolName("submit_result", tools.map((tool) => tool.name))
1009
+ : null;
1010
+ if (structuredSchema && structuredToolName) {
1011
+ tools.push({
1012
+ name: structuredToolName,
1013
+ description: "Submit the final structured result for this agent node's dynamic outputs. Call this exactly once when you have the final answer.",
1014
+ inputSchema: structuredSchema,
1015
+ process: async (_context, params) => {
1016
+ structuredResult = params ?? {};
1017
+ return { status: "completed" };
1018
+ }
1019
+ });
1020
+ system = `${system}\n\nWhen the final answer is ready, call the ${structuredToolName} tool with values for the dynamic outputs. Do not format the final result as JSON text.`;
1021
+ }
1022
+ const messages = [
1023
+ { role: "system", content: system },
1024
+ ...(await loadThreadMessages(context, threadId)),
1025
+ ...history,
1026
+ buildUserMessage(prompt, images, audios)
1027
+ ];
1028
+ log.info("AgentNode prepared messages", {
1029
+ nodeId: this.__node_id ?? null,
1030
+ providerId,
1031
+ modelId,
1032
+ threadId: threadId || null,
1033
+ promptLength: prompt.length,
1034
+ historyCount: history.length,
1035
+ toolCount: tools.length,
1036
+ messageCount: messages.length,
1037
+ hasImage: hasContentType(messages[messages.length - 1], "image_url"),
1038
+ hasAudio: hasContentType(messages[messages.length - 1], "audio")
1039
+ });
1040
+ if (threadId) {
1041
+ await saveThreadMessage(context, threadId, messages[messages.length - 1]);
1042
+ }
1043
+ // Dereference inline asset:// references (mentioned in the prompt) to data
1044
+ // URIs before the provider call. Resolution lives in the runtime layer so
1045
+ // it's shared across nodes and providers stay asset-agnostic. Saved to the
1046
+ // thread above first, so the stored message keeps the compact asset:// URI.
1047
+ if (isCallable(context.resolveMessageMediaUris)) {
1048
+ const resolved = await context.resolveMessageMediaUris(messages);
1049
+ messages.splice(0, messages.length, ...resolved);
1050
+ }
1051
+ // Held in an object so the nested finalizeAssistantTurn closure can update
1052
+ // it without tripping control-flow narrowing at the read sites below.
1053
+ const finalText = { value: null };
1054
+ // Each provider tool carries its own `execute` (generateLoop dispatches to
1055
+ // it directly) instead of a harness-level executeTool callback. Control
1056
+ // tools route through sendControlEvent; regular tools call tool.process;
1057
+ // submit_result (the structured tool) is marked `terminal` so generateLoop
1058
+ // ends the loop after it runs — its process populates structuredResult.
1059
+ const providerTools = tools.length > 0
1060
+ ? toProviderTools(tools).map((pt, i) => {
1061
+ const tool = tools[i];
1062
+ const isStructured = structuredToolName != null && tool.name === structuredToolName;
1063
+ return {
1064
+ ...pt,
1065
+ terminal: isStructured,
1066
+ execute: async (args, toolCallId) => {
1067
+ if (!isCallable(tool.execute) && !isCallable(tool.process)) {
1068
+ log.warn("AgentNode tool call had no matching executable tool", {
1069
+ nodeId: this.__node_id ?? null,
1070
+ toolName: tool.name,
1071
+ availableTools: tools.map((candidate) => candidate.name)
1072
+ });
1073
+ return JSON.stringify({
1074
+ status: "error",
1075
+ error: `Unknown or non-executable tool: ${tool.name}`
1076
+ });
1077
+ }
1078
+ // Control tools carry their own dispatch logic in `process`
1079
+ // (filter args → sendControlEvent), so they flow through this
1080
+ // generic path like any other tool.
1081
+ let result;
1082
+ log.info("AgentNode executing tool", {
1083
+ nodeId: this.__node_id ?? null,
1084
+ toolName: tool.name
1085
+ });
1086
+ try {
1087
+ // Forward the LLM's tool-call id: tools that spawn nested
1088
+ // work (run_subtask, nested workflows) stamp it on their
1089
+ // events as parent_tool_call_id.
1090
+ result = isCallable(tool.execute)
1091
+ ? await tool.execute(context, args, { toolCallId })
1092
+ : await tool.process(context, args);
1093
+ }
1094
+ catch (err) {
1095
+ const message = err instanceof Error ? err.message : String(err);
1096
+ log.warn("AgentNode tool execution failed", {
1097
+ nodeId: this.__node_id ?? null,
1098
+ toolName: tool.name,
1099
+ error: message
1100
+ });
1101
+ result = { status: "error", error: message };
1102
+ }
1103
+ return JSON.stringify(serializeToolResult(result));
1104
+ }
1105
+ };
1106
+ })
1107
+ : undefined;
1108
+ const provider = await context.getProvider(providerId);
1109
+ // Book what this node spends against the run. See `meterProviderSpend` for
1110
+ // why a per-run provider instance makes this delta safe to add.
1111
+ const spend = meterProviderSpend(context, provider, modelId);
1112
+ try {
1113
+ // The provider drives the tool-calling loop via generateLoop. This is the
1114
+ // headline fix: on the Claude Agent SDK provider, generateLoop registers
1115
+ // `providerTools` (including submit_result) with the SDK agent and calls
1116
+ // back into `executeTool` per call — the tool-free generateMessages
1117
+ // primitive never saw them. Other providers run the standard completion
1118
+ // loop and call back the same way. generateLoop owns the message array and
1119
+ // streams: text/thinking/audio chunks, ToolCall announcements (before
1120
+ // execution), and `{ type: "message" }` events for each finalized
1121
+ // assistant turn and tool result (which we mirror to thread persistence).
1122
+ // Tool execution and loop termination are driven by the provider tools'
1123
+ // own `execute`/`terminal` (built above), not a harness callback.
1124
+ let assistantText = "";
1125
+ let streamedRedactedThinking = false;
1126
+ let thinkSplitter = new RedactedThinkingStreamSplitter();
1127
+ // Finalize one assistant turn: flush any buffered think-split remainder,
1128
+ // then emit the cleaned final text (and any non-streamed thinking) and
1129
+ // update lastTextOutput. Resets the per-turn accumulators so the next turn
1130
+ // starts clean. Driven off the assistant `message` events generateLoop
1131
+ // emits — one per assistant turn — plus a final call after the stream for
1132
+ // providers that finish without a trailing assistant message event.
1133
+ const finalizeAssistantTurn = function* () {
1134
+ for (const part of thinkSplitter.flush()) {
1135
+ if (part.kind === "thinking" && part.content.length > 0) {
1136
+ streamedRedactedThinking = true;
1137
+ yield {
1138
+ chunk: null,
1139
+ thinking: {
1140
+ type: "chunk",
1141
+ content: part.content,
1142
+ thinking: true
1143
+ },
1144
+ text: null,
1145
+ audio: null
1146
+ };
1147
+ }
1148
+ else if (part.kind === "text" && part.content.length > 0) {
1149
+ yield {
1150
+ chunk: {
1151
+ type: "chunk",
1152
+ content: part.content,
1153
+ content_type: "text",
1154
+ done: false
1155
+ },
1156
+ thinking: null,
1157
+ text: null,
1158
+ audio: null
1159
+ };
1160
+ }
1161
+ }
1162
+ if (assistantText) {
1163
+ const { thinking: thinkingText, text: cleanText } = extractThinkTags(assistantText);
1164
+ const trimmed = cleanText.trim();
1165
+ if (trimmed) {
1166
+ finalText.value = trimmed;
1167
+ }
1168
+ if (thinkingText && !streamedRedactedThinking) {
1169
+ yield {
1170
+ chunk: null,
1171
+ thinking: {
1172
+ type: "chunk",
1173
+ content: thinkingText,
1174
+ thinking: true
1175
+ },
1176
+ text: null,
1177
+ audio: null
1178
+ };
1179
+ }
1180
+ yield {
1181
+ chunk: null,
1182
+ thinking: null,
1183
+ text: cleanText,
1184
+ audio: null
1185
+ };
1186
+ }
1187
+ assistantText = "";
1188
+ streamedRedactedThinking = false;
1189
+ thinkSplitter = new RedactedThinkingStreamSplitter();
1190
+ };
1191
+ const stream = provider.generateLoop({
1192
+ messages,
1193
+ model: modelId,
1194
+ tools: providerTools,
1195
+ maxTokens,
1196
+ maxIterations: maxTurns,
1197
+ sequentialTools: true,
1198
+ threadId: threadId || undefined,
1199
+ turnBudget: budget,
1200
+ // Without this a cancelled workflow leaves
1201
+ // the provider loop (and any tool calls it makes) running.
1202
+ signal: context?.signal
1203
+ });
1204
+ // The loop's own reason for ending, when it was not the model ending its
1205
+ // turn. Read after the stream drains so any text the run did produce is
1206
+ // emitted before the node decides whether to fail.
1207
+ let stop = null;
1208
+ for await (const event of classifyProviderStream(stream)) {
1209
+ if (event.kind === "stop") {
1210
+ stop = event.stop;
1211
+ continue;
1212
+ }
1213
+ if (event.kind === "tool_call") {
1214
+ const toolCall = event.toolCall;
1215
+ log.info("AgentNode received tool call", {
1216
+ nodeId: this.__node_id ?? null,
1217
+ providerId,
1218
+ modelId,
1219
+ toolCallId: toolCall.id,
1220
+ toolName: toolCall.name,
1221
+ argKeys: Object.keys(toolCall.args ?? {})
1222
+ });
1223
+ yield {
1224
+ chunk: toolCallChunk(toolCall),
1225
+ thinking: null,
1226
+ text: null,
1227
+ audio: null
1228
+ };
1229
+ continue;
1230
+ }
1231
+ if (event.kind === "thinking") {
1232
+ yield { chunk: null, thinking: event.chunk, text: null, audio: null };
1233
+ continue;
1234
+ }
1235
+ if (event.kind === "audio") {
1236
+ const chunk = event.chunk;
1237
+ yield { chunk, thinking: null, text: null, audio: null };
1238
+ const audioBytes = isNonEmptyString(chunk.content)
1239
+ ? Buffer.from(chunk.content, "base64")
1240
+ : chunk.content instanceof Float32Array
1241
+ ? Buffer.from(chunk.content.buffer, chunk.content.byteOffset, chunk.content.byteLength)
1242
+ : Buffer.alloc(0);
1243
+ yield {
1244
+ chunk: null,
1245
+ thinking: null,
1246
+ text: null,
1247
+ audio: { data: new Uint8Array(audioBytes) }
1248
+ };
1249
+ continue;
1250
+ }
1251
+ if (event.kind === "text") {
1252
+ const rawPiece = event.delta;
1253
+ assistantText += rawPiece;
1254
+ for (const y of yieldSplitThinkChunks(event.chunk, rawPiece, thinkSplitter)) {
1255
+ if (y.thinking != null)
1256
+ streamedRedactedThinking = true;
1257
+ yield y;
1258
+ }
1259
+ continue;
1260
+ }
1261
+ // A finalized message event (assistant turn or tool result). generateLoop
1262
+ // owns the message array; we mirror the old per-turn finalization +
1263
+ // thread persistence off these. Each assistant event closes a turn.
1264
+ if (event.kind === "assistant_message") {
1265
+ const message = event.message;
1266
+ // Rebuild the persisted assistant message from our own accumulated
1267
+ // text (which excludes audio chunks) so saved history keeps its old
1268
+ // array shape; carry tool calls / Gemini thought-signature parts from
1269
+ // the event since generateLoop now owns the live message.
1270
+ const hadToolCalls = (message.toolCalls?.length ?? 0) > 0;
1271
+ const persistMessage = {
1272
+ role: "assistant",
1273
+ content: [{ type: "text", text: assistantText }],
1274
+ toolCalls: message.toolCalls ?? null
1275
+ };
1276
+ if (message._rawGeminiParts) {
1277
+ persistMessage._rawGeminiParts = message._rawGeminiParts;
1278
+ }
1279
+ const shouldPersist = Boolean(assistantText) || hadToolCalls;
1280
+ yield* finalizeAssistantTurn();
1281
+ if (threadId && shouldPersist) {
1282
+ await saveThreadMessage(context, threadId, persistMessage);
1283
+ }
1284
+ }
1285
+ else if (event.kind === "tool_message") {
1286
+ if (threadId) {
1287
+ await saveThreadMessage(context, threadId, event.message);
1288
+ }
1289
+ }
1290
+ }
1291
+ // Flush any trailing turn for providers that stream final text without a
1292
+ // closing assistant message event (no-op once a turn has been finalized).
1293
+ yield* finalizeAssistantTurn();
1294
+ // A budget or deadline stop is a failure: the node did not finish the
1295
+ // job it was given, and returning its partial text as if it had would
1296
+ // hide the cap from the user who set it (decision D2, invariant I-3).
1297
+ // `iterations` keeps its long-standing behavior — reaching Max Turns has
1298
+ // always just ended the node — and `aborted` is a cancelled workflow,
1299
+ // which must stay a clean stop.
1300
+ if (stop && (stop.reason === "budget" || stop.reason === "deadline")) {
1301
+ log.warn("AgentNode stopped by the run budget", {
1302
+ nodeId: this.__node_id ?? null,
1303
+ reason: stop.reason,
1304
+ detail: stop.detail,
1305
+ spentUsd: budget.turns.spentUsd
1306
+ });
1307
+ throw new Error(`Agent stopped: ${stop.detail}`);
1308
+ }
1309
+ }
1310
+ finally {
1311
+ // A budget stop, a provider error or a cancelled run still spent what it
1312
+ // spent before the throw. Report that rather than losing it.
1313
+ spend.report();
1314
+ }
1315
+ if (structuredSchema && structuredResult) {
1316
+ log.info("AgentNode yielding dynamic output tool result", {
1317
+ nodeId: this.__node_id ?? null,
1318
+ keys: Object.keys(structuredResult)
1319
+ });
1320
+ yield structuredResult;
1321
+ }
1322
+ log.info("AgentNode completed", {
1323
+ nodeId: this.__node_id ?? null,
1324
+ providerId,
1325
+ modelId,
1326
+ finalTextLength: finalText.value?.length ?? 0,
1327
+ returnedStructured: Boolean(structuredResult)
1328
+ });
1329
+ }
1330
+ async process(context) {
1331
+ let lastText = "";
1332
+ let lastAudio = null;
1333
+ let structuredResult = null;
1334
+ for await (const item of this.genProcess(context)) {
1335
+ if ("chunk" in item ||
1336
+ "thinking" in item ||
1337
+ "text" in item ||
1338
+ "audio" in item) {
1339
+ if (isString(item.text)) {
1340
+ lastText = item.text;
1341
+ }
1342
+ if (isObjectLike(item.audio)) {
1343
+ lastAudio = item.audio;
1344
+ }
1345
+ }
1346
+ else {
1347
+ structuredResult = item;
1348
+ }
1349
+ }
1350
+ if (structuredResult) {
1351
+ log.info("AgentNode process() returning structured result", {
1352
+ nodeId: this.__node_id ?? null,
1353
+ keys: Object.keys(structuredResult)
1354
+ });
1355
+ return structuredResult;
1356
+ }
1357
+ log.info("AgentNode process() returning aggregate result", {
1358
+ nodeId: this.__node_id ?? null,
1359
+ textLength: lastText.length,
1360
+ hasAudio: Boolean(lastAudio)
1361
+ });
1362
+ return {
1363
+ text: lastText,
1364
+ output: lastText,
1365
+ chunk: null,
1366
+ thinking: null,
1367
+ audio: lastAudio
1368
+ };
1369
+ }
1370
+ }
1371
+ __decorate([
1372
+ prop({
1373
+ type: "language_model",
1374
+ default: {
1375
+ type: "language_model",
1376
+ provider: "empty",
1377
+ id: "",
1378
+ name: "",
1379
+ path: null,
1380
+ supported_tasks: []
1381
+ },
1382
+ title: "Model",
1383
+ description: "Model to use for execution"
1384
+ })
1385
+ ], AgentNode.prototype, "model", void 0);
1386
+ __decorate([
1387
+ prop({
1388
+ type: "str",
1389
+ default: "You are a friendly assistant",
1390
+ title: "System",
1391
+ description: 'Instructions that define the agent\'s persona, role, tone, and global behaviour. Sent to the model as the system message at the start of every run, before any history or user prompt. Use it for things that should always hold (e.g. "You are a senior Python reviewer. Reply in Markdown."). Leave the prompt itself for the per‑run task.'
1392
+ })
1393
+ ], AgentNode.prototype, "system", void 0);
1394
+ __decorate([
1395
+ prop({
1396
+ type: "str",
1397
+ default: "",
1398
+ title: "Prompt",
1399
+ description: "The user message for this run — the actual question, task, or content the agent should act on. Appended after the system prompt and conversation history as the latest user turn. Any connected Image or Audio inputs are attached to this message."
1400
+ })
1401
+ ], AgentNode.prototype, "prompt", void 0);
1402
+ __decorate([
1403
+ prop({
1404
+ type: "list[tool_name]",
1405
+ default: [],
1406
+ title: "Tools",
1407
+ description: "Tools to enable for the agent. Select workspace tools (read_file, write_file, list_directory) to enable file operations."
1408
+ })
1409
+ ], AgentNode.prototype, "tools", void 0);
1410
+ __decorate([
1411
+ prop({
1412
+ type: "list[image]",
1413
+ default: [],
1414
+ title: "Images",
1415
+ description: "Images to attach to the prompt. Wire a list[image] source to send several at once, or a single Image (auto-wrapped into a one-item list). Each image becomes a separate block in the user message sent to the provider."
1416
+ })
1417
+ ], AgentNode.prototype, "image", void 0);
1418
+ __decorate([
1419
+ prop({
1420
+ type: "list[audio]",
1421
+ default: [],
1422
+ title: "Audio",
1423
+ description: "Audio clips to attach to the prompt. Wire a list[audio] source to send several at once, or a single Audio (auto-wrapped into a one-item list). Each clip becomes a separate block in the user message sent to the provider."
1424
+ })
1425
+ ], AgentNode.prototype, "audio", void 0);
1426
+ __decorate([
1427
+ prop({
1428
+ type: "list[message]",
1429
+ default: [],
1430
+ title: "Messages",
1431
+ description: "Prior conversation turns to include before the current prompt, in chronological order (oldest first). Each item is a Message with a role (user/assistant/tool) and content. Use this to supply ad‑hoc context — for example, few‑shot examples, a previous chat transcript piped in from another node, or the messages output of an upstream Agent. Inserted between the system prompt and the new user prompt. If a Thread ID is also set, history loaded from the thread comes first, then this list, then the current prompt."
1432
+ })
1433
+ ], AgentNode.prototype, "history", void 0);
1434
+ __decorate([
1435
+ prop({
1436
+ type: "str",
1437
+ default: "",
1438
+ title: "Thread ID",
1439
+ description: "Identifier for a persistent conversation thread. When set, the agent loads all earlier messages stored under this ID before this turn and saves the new user message, assistant reply, and any tool messages back to it — giving the agent long‑term memory across runs and across nodes that share the same ID. Leave empty for a stateless one‑shot call. Use the Create Thread node to mint a fresh ID, or wire in the same string from upstream to continue an existing conversation."
1440
+ })
1441
+ ], AgentNode.prototype, "thread_id", void 0);
1442
+ __decorate([
1443
+ prop({
1444
+ type: "int",
1445
+ default: AGENT_DEFAULT_MAX_TOKENS,
1446
+ title: "Max Tokens",
1447
+ description: "Upper bound on generated tokens per response, including visible output and any reasoning/thinking tokens used by reasoning models. Higher values allow longer answers and more thinking headroom but cost more and take longer; very low values may truncate reasoning or the final answer. Typical values: 1024 for short answers, 8192–16384 for normal agent use, 32k+ for long-form or heavy reasoning. Must be within the chosen model's context window.",
1448
+ min: 1,
1449
+ max: 100000
1450
+ })
1451
+ ], AgentNode.prototype, "max_tokens", void 0);
1452
+ __decorate([
1453
+ prop({
1454
+ type: "int",
1455
+ default: 100,
1456
+ title: "Max Turns",
1457
+ description: "Upper bound on agentic turns — one turn is a model call plus any tool execution it triggers. Caps both the AgentNode tool-loop iteration count and the provider's internal multi-turn budget (e.g. Claude Agent SDK). Raise for long sandbox sessions; lower to fail fast on runaway loops.",
1458
+ min: 1,
1459
+ max: 1000
1460
+ })
1461
+ ], AgentNode.prototype, "max_turns", void 0);
1462
+ __decorate([
1463
+ prop({
1464
+ type: "float",
1465
+ default: 5.0,
1466
+ title: "Cost Cap (USD)",
1467
+ description: "Ceiling on provider spend for this run, in US dollars. A turn whose worst case (this prompt plus Max Tokens of output) would cross it is refused before the call is made, and the node fails with an error naming the cap — so the ceiling holds instead of being noticed after the money is gone. Zero or less means no cap, which is what a local-only install wants. Matches the install-wide default in NODETOOL_AGENT_TURN_COST_CAP_USD. Ignored when the node runs inside an agent run that already has a budget (a chat turn calling run_node): it reserves against that run's cap instead of opening a second allowance.",
1468
+ min: 0,
1469
+ max: 1000
1470
+ })
1471
+ ], AgentNode.prototype, "cost_cap_usd", void 0);
1472
+ __decorate([
1473
+ prop({
1474
+ type: "int",
1475
+ default: 1200,
1476
+ title: "Timeout (s)",
1477
+ description: "Wall-clock bound on this run, in seconds (default 1200, i.e. 20 minutes). Checked before every model turn, so a run stalled on a slow provider or a tool loop ends with an error naming the timeout instead of hanging until the workflow is cancelled. Ignored when the node runs inside an agent run that already has a budget — that run's deadline applies.",
1478
+ min: 1,
1479
+ max: 86400
1480
+ })
1481
+ ], AgentNode.prototype, "timeout_s", void 0);
1482
+ export const AGENT_NODES = tagAsServer([
1483
+ SummarizerNode,
1484
+ EnhancePromptNode,
1485
+ CreateThreadNode,
1486
+ ExtractorNode,
1487
+ ClassifierNode,
1488
+ DecisionNode,
1489
+ AgentNode
1490
+ ]);
1491
+ //# sourceMappingURL=agents.js.map