@tbrandenburg/node-red-agents 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/nodes/agent/agent.html +1017 -575
- package/nodes/agent/agent.js +353 -44
- package/nodes/agent/lib/agents/capabilities.js +23 -0
- package/nodes/agent/lib/agents/opencode.js +158 -26
- package/nodes/agent/lib/agents/pi.js +32 -1
- package/nodes/agent/lib/execution/inputs.js +22 -0
- package/nodes/agent/lib/execution/lifecycle.js +7 -1
- package/nodes/agent/lib/execution/retry.js +61 -0
- package/nodes/agent/lib/execution/structured-output.js +111 -0
- package/nodes/agent-server/agent-server.html +52 -3
- package/nodes/agent-server/lib/model.js +3 -5
- package/nodes/gh/gh.js +4 -1
- package/package.json +5 -2
- package/shared/model-format.js +34 -0
package/nodes/agent/agent.js
CHANGED
|
@@ -7,6 +7,16 @@ const { writeInlineSettingsFile } = require("../../shared/srt-settings");
|
|
|
7
7
|
const { runAgent } = require("./lib/execution/lifecycle");
|
|
8
8
|
const { ExecutionScheduler } = require("./lib/execution/scheduler");
|
|
9
9
|
const { computeNodeStatus } = require("./lib/execution/status");
|
|
10
|
+
const { getCapabilities } = require("./lib/agents/capabilities");
|
|
11
|
+
const { shouldRetry } = require("./lib/execution/retry");
|
|
12
|
+
const { substituteInputs } = require("./lib/execution/inputs");
|
|
13
|
+
const {
|
|
14
|
+
STRUCTURED_OUTPUT_MAX_REASKS,
|
|
15
|
+
compileOutputFormat,
|
|
16
|
+
tryParseStructuredOutput,
|
|
17
|
+
augmentPromptForSchema,
|
|
18
|
+
buildReaskPrompt,
|
|
19
|
+
} = require("./lib/execution/structured-output");
|
|
10
20
|
|
|
11
21
|
// Registries. Adding a future adapter/runtime is just one more entry here --
|
|
12
22
|
// nothing else in this file (or in lib/execution/lifecycle.js) needs to
|
|
@@ -73,6 +83,14 @@ module.exports = function (RED) {
|
|
|
73
83
|
node.arguments_ = config.arguments !== undefined ? config.arguments : "payload";
|
|
74
84
|
node.argumentsType = config.argumentsType || "msg";
|
|
75
85
|
|
|
86
|
+
// Named, multi-value $INPUTS.<name> templating (issue #20): a list of
|
|
87
|
+
// { name, value, valueType } typed-input entries, each resolved
|
|
88
|
+
// per-message via resolveTyped below and substituted into the
|
|
89
|
+
// resolved arguments string before it's handed to either adapter.
|
|
90
|
+
// Pure text templating, zero adapter-specific code -- see
|
|
91
|
+
// lib/execution/inputs.js. Default empty list = zero behavior change.
|
|
92
|
+
node.inputs = Array.isArray(config.inputs) ? config.inputs : [];
|
|
93
|
+
|
|
76
94
|
node.cwd = config.cwd !== undefined ? config.cwd : "cwd";
|
|
77
95
|
node.cwdType = config.cwdType || "msg";
|
|
78
96
|
|
|
@@ -83,6 +101,75 @@ module.exports = function (RED) {
|
|
|
83
101
|
|
|
84
102
|
node.mcpServers = Array.isArray(config.mcpServers) ? config.mcpServers : [];
|
|
85
103
|
|
|
104
|
+
// Optional per-adapter capability-gated fields (issue #25), modeled on
|
|
105
|
+
// Archon's DagNodeBase: systemPrompt override, effort (reasoning
|
|
106
|
+
// depth), and allowed/denied tool lists. Each is only ever honored by
|
|
107
|
+
// an adapter whose CAPABILITIES flag says it's actually wired up (see
|
|
108
|
+
// lib/agents/capabilities.js) -- otherwise startExecution below warns
|
|
109
|
+
// once and drops it, never a hard error.
|
|
110
|
+
node.systemPrompt = config.systemPrompt !== undefined ? config.systemPrompt : "";
|
|
111
|
+
node.systemPromptType = config.systemPromptType || "str";
|
|
112
|
+
|
|
113
|
+
node.effort = config.effort !== undefined ? config.effort : "";
|
|
114
|
+
node.effortType = config.effortType || "str";
|
|
115
|
+
|
|
116
|
+
node.allowedTools = Array.isArray(config.allowedTools) ? config.allowedTools : [];
|
|
117
|
+
node.deniedTools = Array.isArray(config.deniedTools) ? config.deniedTools : [];
|
|
118
|
+
|
|
119
|
+
// Node-level retry (issue #24): fully opt-in-by-default at a
|
|
120
|
+
// conservative setting (2 total attempts = 1 original + 1 retry, per
|
|
121
|
+
// the issue's "default 2 attempts" wording), gated by a small
|
|
122
|
+
// transient-vs-fatal string classifier (lib/execution/retry.js) so a
|
|
123
|
+
// fatal error (bad auth, quota exhaustion) never burns a retry. See
|
|
124
|
+
// startExecution below for the actual retry loop.
|
|
125
|
+
node.retryMaxAttempts = Number.isFinite(Number(config.retryMaxAttempts))
|
|
126
|
+
? Math.max(1, Number(config.retryMaxAttempts))
|
|
127
|
+
: 2;
|
|
128
|
+
node.retryDelayMs = Number.isFinite(Number(config.retryDelayMs))
|
|
129
|
+
? Math.max(0, Number(config.retryDelayMs))
|
|
130
|
+
: 3000;
|
|
131
|
+
node.retryOnError = config.retryOnError === "all" ? "all" : "transient";
|
|
132
|
+
|
|
133
|
+
// output_format (issue #23): fully opt-in, JSON-Schema-as-text config
|
|
134
|
+
// field. Compiled once here (deploy time), not per-execution -- a bad
|
|
135
|
+
// schema or an adapter that doesn't support structured output at all
|
|
136
|
+
// (capabilities.structuredOutput === false) sets node.outputFormatError,
|
|
137
|
+
// which the input handler below checks before ever building an
|
|
138
|
+
// execution, mirroring the existing srtSettingsError deploy-time-
|
|
139
|
+
// validation pattern above.
|
|
140
|
+
node.outputFormat = config.outputFormat !== undefined ? config.outputFormat : "";
|
|
141
|
+
node.compiledOutputFormat = undefined; // AJV validate fn, if configured+valid
|
|
142
|
+
node.outputFormatSchema = undefined; // parsed schema object, if configured+valid
|
|
143
|
+
node.outputFormatError = undefined;
|
|
144
|
+
|
|
145
|
+
if (node.outputFormat && String(node.outputFormat).trim()) {
|
|
146
|
+
let schema;
|
|
147
|
+
try {
|
|
148
|
+
schema = JSON.parse(node.outputFormat);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
node.outputFormatError = `invalid output_format schema: ${err.message}`;
|
|
151
|
+
}
|
|
152
|
+
if (!node.outputFormatError) {
|
|
153
|
+
const compiled = compileOutputFormat(schema);
|
|
154
|
+
if (compiled.error) {
|
|
155
|
+
node.outputFormatError = `invalid output_format schema: ${compiled.error}`;
|
|
156
|
+
} else {
|
|
157
|
+
node.compiledOutputFormat = compiled.validate;
|
|
158
|
+
node.outputFormatSchema = schema;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!node.outputFormatError && AGENTS[node.agent]) {
|
|
162
|
+
const capabilities = getCapabilities(AGENTS[node.agent]());
|
|
163
|
+
if (capabilities.structuredOutput === false) {
|
|
164
|
+
node.outputFormatError = `output_format not supported by ${node.agent}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (node.outputFormatError) {
|
|
168
|
+
node.error(`agent: ${node.outputFormatError}`);
|
|
169
|
+
node.status({ fill: "red", shape: "ring", text: node.outputFormatError });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
86
173
|
node.srtBinary = config.srtBinary || "";
|
|
87
174
|
node.srtSettingsMode = config.srtSettingsMode || "file";
|
|
88
175
|
node.srtSettingsPath = config.srtSettingsPath || "";
|
|
@@ -190,8 +277,15 @@ module.exports = function (RED) {
|
|
|
190
277
|
};
|
|
191
278
|
}
|
|
192
279
|
|
|
193
|
-
|
|
194
|
-
|
|
280
|
+
// `extra` (e.g. { costUsd, tokens } -- see startExecution/onSettled
|
|
281
|
+
// below) is merged into the { type } payload only when provided, so
|
|
282
|
+
// every other emitEvent call site (queued/cancelled/running/etc.)
|
|
283
|
+
// keeps its existing { type }-only payload shape unchanged.
|
|
284
|
+
function emitEvent(send, msg, executionId, type, agentName, cwd, extra) {
|
|
285
|
+
send([
|
|
286
|
+
null,
|
|
287
|
+
lifecycleEnvelope(msg, executionId, Object.assign({ type }, extra), agentName, cwd),
|
|
288
|
+
]);
|
|
195
289
|
}
|
|
196
290
|
|
|
197
291
|
// The actual work for one execution. Only ever invoked by the
|
|
@@ -201,37 +295,236 @@ module.exports = function (RED) {
|
|
|
201
295
|
const { executionId, msg, send, done, resolved } = item;
|
|
202
296
|
const adapter = AGENTS[node.agent]();
|
|
203
297
|
const runtime = buildRuntime(node);
|
|
298
|
+
const capabilities = getCapabilities(adapter);
|
|
299
|
+
|
|
300
|
+
// Capability-gated warn-and-drop (issue #25): a field the user
|
|
301
|
+
// configured but this adapter doesn't actually wire up gets exactly
|
|
302
|
+
// one node.warn per run here -- never a hard error, and never a
|
|
303
|
+
// silent no-op either. Adapters themselves only ever act on these
|
|
304
|
+
// fields when their own CAPABILITIES flag agrees (see opencode.js/
|
|
305
|
+
// pi.js), so this is the single place responsible for surfacing
|
|
306
|
+
// the "ignored" case to the flow author.
|
|
307
|
+
function warnUnsupported(field, isSet, supported) {
|
|
308
|
+
if (isSet && !supported) {
|
|
309
|
+
node.warn(`${field} is not supported by the ${node.agent} adapter and will be ignored`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
warnUnsupported("systemPrompt", !!resolved.systemPrompt, capabilities.systemPromptControl);
|
|
313
|
+
warnUnsupported("effort", !!resolved.effort, capabilities.effortControl);
|
|
314
|
+
warnUnsupported(
|
|
315
|
+
"allowed_tools",
|
|
316
|
+
Array.isArray(resolved.allowedTools) && resolved.allowedTools.length > 0,
|
|
317
|
+
capabilities.toolRestrictions,
|
|
318
|
+
);
|
|
319
|
+
warnUnsupported(
|
|
320
|
+
"denied_tools",
|
|
321
|
+
Array.isArray(resolved.deniedTools) && resolved.deniedTools.length > 0,
|
|
322
|
+
capabilities.toolRestrictions,
|
|
323
|
+
);
|
|
204
324
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
325
|
+
function invoke(currentResolved) {
|
|
326
|
+
return runAgent({
|
|
327
|
+
adapter,
|
|
328
|
+
runtime,
|
|
329
|
+
resolved: currentResolved,
|
|
330
|
+
executionId,
|
|
331
|
+
onEvent: (event) => {
|
|
332
|
+
send([
|
|
333
|
+
null,
|
|
334
|
+
lifecycleEnvelope(msg, executionId, event, resolved.agentName, resolved.cwd),
|
|
335
|
+
]);
|
|
336
|
+
},
|
|
337
|
+
onStatus: (status) => {
|
|
338
|
+
if (status === "running") {
|
|
339
|
+
emitEvent(send, msg, executionId, "running", resolved.agentName, resolved.cwd);
|
|
340
|
+
} else {
|
|
341
|
+
// Terminal (completed/failed/timeout): stash rather
|
|
342
|
+
// than emit immediately -- the scheduler hasn't
|
|
343
|
+
// removed this execution from `active` yet at this
|
|
344
|
+
// point, so the active/queued counts on the
|
|
345
|
+
// envelope would be stale by one. onSettled (below)
|
|
346
|
+
// emits it once the scheduler's own bookkeeping,
|
|
347
|
+
// including any newly-started queued item, is
|
|
348
|
+
// fully settled.
|
|
349
|
+
item.finalStatus = status;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// output_format (issue #23): only ever engaged for prompt invocation
|
|
356
|
+
// with a compiled schema (deploy-time-validated -- see the
|
|
357
|
+
// constructor above). Runs the adapter once with the prompt
|
|
358
|
+
// augmented to ask for schema-matching JSON, then parses+validates
|
|
359
|
+
// the result; on failure, best-effort adapters (capabilities.
|
|
360
|
+
// structuredOutput === "best-effort") get up to
|
|
361
|
+
// STRUCTURED_OUTPUT_MAX_REASKS additional turns (reusing the prior
|
|
362
|
+
// sessionID when capabilities.sessionResume is true, so context
|
|
363
|
+
// isn't lost) before the whole execution is reported as failed --
|
|
364
|
+
// it must never silently fall back to raw, unvalidated text.
|
|
365
|
+
const outputFormat = node.compiledOutputFormat
|
|
366
|
+
? { validate: node.compiledOutputFormat, schema: node.outputFormatSchema }
|
|
367
|
+
: undefined;
|
|
368
|
+
const structuredEnabled = !!outputFormat && resolved.invocation === "prompt";
|
|
369
|
+
|
|
370
|
+
async function executeWithStructuredOutput(execResolved) {
|
|
371
|
+
const firstResolved = structuredEnabled
|
|
372
|
+
? Object.assign({}, execResolved, {
|
|
373
|
+
prompt: augmentPromptForSchema(execResolved.prompt, outputFormat.schema),
|
|
374
|
+
})
|
|
375
|
+
: execResolved;
|
|
376
|
+
|
|
377
|
+
let result = await invoke(firstResolved);
|
|
378
|
+
if (!structuredEnabled || result.status !== "completed") return result;
|
|
379
|
+
|
|
380
|
+
let parsed = tryParseStructuredOutput(result.payload);
|
|
381
|
+
let valid = parsed !== undefined && outputFormat.validate(parsed);
|
|
382
|
+
let lastErrors = outputFormat.validate.errors;
|
|
383
|
+
let attempts = 0;
|
|
384
|
+
const maxReasks =
|
|
385
|
+
capabilities.structuredOutput === "best-effort" ? STRUCTURED_OUTPUT_MAX_REASKS : 0;
|
|
386
|
+
|
|
387
|
+
while (!valid && attempts < maxReasks) {
|
|
388
|
+
attempts += 1;
|
|
389
|
+
const reaskResolved = Object.assign({}, execResolved, {
|
|
390
|
+
prompt: buildReaskPrompt(execResolved.prompt, outputFormat.schema, lastErrors),
|
|
391
|
+
sessionID:
|
|
392
|
+
capabilities.sessionResume && result.sessionID
|
|
393
|
+
? result.sessionID
|
|
394
|
+
: execResolved.sessionID,
|
|
395
|
+
});
|
|
396
|
+
result = await invoke(reaskResolved);
|
|
397
|
+
if (result.status !== "completed") break;
|
|
398
|
+
parsed = tryParseStructuredOutput(result.payload);
|
|
399
|
+
valid = parsed !== undefined && outputFormat.validate(parsed);
|
|
400
|
+
lastErrors = outputFormat.validate.errors;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!valid) {
|
|
404
|
+
const errText =
|
|
405
|
+
lastErrors && lastErrors.length
|
|
406
|
+
? lastErrors.map((e) => `${e.instancePath || "(root)"} ${e.message}`).join("; ")
|
|
407
|
+
: "response was not valid JSON matching output_format";
|
|
408
|
+
return Object.assign({}, result, {
|
|
409
|
+
status: "failed",
|
|
410
|
+
errorMessage: `output_format validation failed after ${attempts} reask(s): ${errText}`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return Object.assign({}, result, { structuredOutput: parsed });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function delay(ms) {
|
|
418
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Node-level retry (issue #24): a whole executeWithStructuredOutput()
|
|
422
|
+
// call (first attempt + any reasks) counts as one "attempt" here --
|
|
423
|
+
// retries only kick in once that entire pipeline has settled on a
|
|
424
|
+
// final failed/timeout result. Runs inside this one scheduler slot
|
|
425
|
+
// (see the module header comment on ExecutionScheduler's contract)
|
|
426
|
+
// -- never re-`submit()`s to the scheduler, so no duplicate
|
|
427
|
+
// queued/running events and no extra slot churn.
|
|
428
|
+
async function executeWithRetry() {
|
|
429
|
+
let currentResolved = resolved;
|
|
430
|
+
let attempt = 1;
|
|
431
|
+
for (;;) {
|
|
432
|
+
const result = await executeWithStructuredOutput(currentResolved);
|
|
433
|
+
const isTerminalFailure = result.status === "failed" || result.status === "timeout";
|
|
434
|
+
if (
|
|
435
|
+
!isTerminalFailure ||
|
|
436
|
+
attempt >= node.retryMaxAttempts ||
|
|
437
|
+
!shouldRetry(result, node.retryOnError)
|
|
438
|
+
) {
|
|
439
|
+
return result;
|
|
226
440
|
}
|
|
227
|
-
|
|
228
|
-
|
|
441
|
+
|
|
442
|
+
attempt += 1;
|
|
443
|
+
emitEvent(send, msg, executionId, "retrying", resolved.agentName, resolved.cwd, {
|
|
444
|
+
attempt,
|
|
445
|
+
maxAttempts: node.retryMaxAttempts,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (node.retryDelayMs > 0) await delay(node.retryDelayMs);
|
|
449
|
+
|
|
450
|
+
// Session-reuse-on-retry: only when the adapter can actually
|
|
451
|
+
// resume a session (capabilities.sessionResume, e.g. opencode)
|
|
452
|
+
// and the failed attempt got far enough to mint one -- pi
|
|
453
|
+
// (sessionResume: false) always retries sessionless, unchanged.
|
|
454
|
+
currentResolved =
|
|
455
|
+
capabilities.sessionResume && result.sessionID
|
|
456
|
+
? Object.assign({}, currentResolved, { sessionID: result.sessionID })
|
|
457
|
+
: currentResolved;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return executeWithRetry()
|
|
229
462
|
.then((result) => {
|
|
230
463
|
node.lastTerminal = result.status;
|
|
231
464
|
node.lastText = undefined;
|
|
232
465
|
|
|
466
|
+
// output_format success (issue #23): canonicalize msg.payload to
|
|
467
|
+
// the parsed-then-restringified JSON text (not the adapter's
|
|
468
|
+
// possibly fence-wrapped/padded raw text), and stash the parsed
|
|
469
|
+
// object separately on agentExecution below.
|
|
470
|
+
if (
|
|
471
|
+
structuredEnabled &&
|
|
472
|
+
result.status === "completed" &&
|
|
473
|
+
result.structuredOutput !== undefined
|
|
474
|
+
) {
|
|
475
|
+
result = Object.assign({}, result, {
|
|
476
|
+
payload: JSON.stringify(result.structuredOutput),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const agentExecution = {
|
|
481
|
+
id: executionId,
|
|
482
|
+
status: result.status,
|
|
483
|
+
exitCode: result.exitCode,
|
|
484
|
+
signal: result.signal,
|
|
485
|
+
timedOut: result.timedOut,
|
|
486
|
+
durationMs: result.durationMs,
|
|
487
|
+
sessionID: result.sessionID,
|
|
488
|
+
// Raw error object from the adapter (e.g. opencode's full
|
|
489
|
+
// {"type":"error"} payload, or pi's failing assistant
|
|
490
|
+
// message) when the run failed -- the `done(err)` string
|
|
491
|
+
// below only carries a single summarized message/name, so
|
|
492
|
+
// anything needing the fuller detail (extra fields the
|
|
493
|
+
// adapter didn't fold into errorMessage) should wire a
|
|
494
|
+
// Debug node to output 1 and inspect this field.
|
|
495
|
+
errorDetail: result.errorDetail,
|
|
496
|
+
};
|
|
497
|
+
if (structuredEnabled && result.structuredOutput !== undefined) {
|
|
498
|
+
agentExecution.structuredOutput = result.structuredOutput;
|
|
499
|
+
agentExecution.declaredFields = Object.keys(
|
|
500
|
+
(outputFormat.schema && outputFormat.schema.properties) || {},
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Only adapters declaring costReporting (see
|
|
505
|
+
// lib/agents/capabilities.js) ever populate result.costUsd/
|
|
506
|
+
// .tokens (e.g. opencode.js's parseResult) -- checking the
|
|
507
|
+
// capability first, rather than just `!== undefined`, means an
|
|
508
|
+
// adapter that isn't wired for this can never leak a stray
|
|
509
|
+
// key even if its result object happens to carry one.
|
|
510
|
+
let usage;
|
|
511
|
+
if (capabilities.costReporting) {
|
|
512
|
+
if (result.costUsd !== undefined) agentExecution.costUsd = result.costUsd;
|
|
513
|
+
if (result.tokens !== undefined) agentExecution.tokens = result.tokens;
|
|
514
|
+
if (agentExecution.costUsd !== undefined || agentExecution.tokens !== undefined) {
|
|
515
|
+
usage = {};
|
|
516
|
+
if (agentExecution.costUsd !== undefined) usage.costUsd = agentExecution.costUsd;
|
|
517
|
+
if (agentExecution.tokens !== undefined) usage.tokens = agentExecution.tokens;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Stashed for onSettled below (the deferred terminal lifecycle
|
|
521
|
+
// event on output 2, see the onStatus comment above) -- by the
|
|
522
|
+
// time onSettled fires this Promise has already resolved, so
|
|
523
|
+
// item.finalUsage is guaranteed to be set.
|
|
524
|
+
item.finalUsage = usage;
|
|
525
|
+
|
|
233
526
|
const resultMsg = Object.assign({}, msg, {
|
|
234
|
-
payload: result.payload,
|
|
527
|
+
payload: result.status === "completed" ? result.payload : null,
|
|
235
528
|
agent: node.agent,
|
|
236
529
|
runtime: node.runtime,
|
|
237
530
|
agentId: node.id,
|
|
@@ -242,23 +535,7 @@ module.exports = function (RED) {
|
|
|
242
535
|
// (default) Session ID field -- msg.sessionID -- of
|
|
243
536
|
// this or another agent node with no extra wiring.
|
|
244
537
|
sessionID: result.sessionID,
|
|
245
|
-
agentExecution
|
|
246
|
-
id: executionId,
|
|
247
|
-
status: result.status,
|
|
248
|
-
exitCode: result.exitCode,
|
|
249
|
-
signal: result.signal,
|
|
250
|
-
timedOut: result.timedOut,
|
|
251
|
-
durationMs: result.durationMs,
|
|
252
|
-
sessionID: result.sessionID,
|
|
253
|
-
// Raw error object from the adapter (e.g. opencode's full
|
|
254
|
-
// {"type":"error"} payload, or pi's failing assistant
|
|
255
|
-
// message) when the run failed -- the `done(err)` string
|
|
256
|
-
// below only carries a single summarized message/name, so
|
|
257
|
-
// anything needing the fuller detail (extra fields the
|
|
258
|
-
// adapter didn't fold into errorMessage) should wire a
|
|
259
|
-
// Debug node to output 1 and inspect this field.
|
|
260
|
-
errorDetail: result.errorDetail,
|
|
261
|
-
},
|
|
538
|
+
agentExecution,
|
|
262
539
|
});
|
|
263
540
|
send([resultMsg, null]);
|
|
264
541
|
|
|
@@ -306,6 +583,7 @@ module.exports = function (RED) {
|
|
|
306
583
|
item.finalStatus,
|
|
307
584
|
item.resolved.agentName,
|
|
308
585
|
item.resolved.cwd,
|
|
586
|
+
item.finalUsage,
|
|
309
587
|
);
|
|
310
588
|
}
|
|
311
589
|
updateStatus();
|
|
@@ -388,6 +666,14 @@ module.exports = function (RED) {
|
|
|
388
666
|
return;
|
|
389
667
|
}
|
|
390
668
|
|
|
669
|
+
if (node.outputFormatError) {
|
|
670
|
+
node.lastTerminal = "failed";
|
|
671
|
+
node.lastText = "bad output_format";
|
|
672
|
+
updateStatus();
|
|
673
|
+
done(new Error(`agent: ${node.outputFormatError}`));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
391
677
|
if (msg.operation === "terminate") {
|
|
392
678
|
handleTerminateOperation(msg, send, done);
|
|
393
679
|
return;
|
|
@@ -417,7 +703,20 @@ module.exports = function (RED) {
|
|
|
417
703
|
: undefined,
|
|
418
704
|
args:
|
|
419
705
|
node.invocation !== "prompt"
|
|
420
|
-
?
|
|
706
|
+
? (() => {
|
|
707
|
+
const raw = resolveTyped(node.arguments_, node.argumentsType, msg, msg.payload);
|
|
708
|
+
if (typeof raw !== "string" || node.inputs.length === 0) return raw;
|
|
709
|
+
const inputsMap = {};
|
|
710
|
+
node.inputs.forEach((entry) => {
|
|
711
|
+
inputsMap[entry.name] = resolveTyped(
|
|
712
|
+
entry.value,
|
|
713
|
+
entry.valueType || "msg",
|
|
714
|
+
msg,
|
|
715
|
+
"",
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
return substituteInputs(raw, inputsMap);
|
|
719
|
+
})()
|
|
421
720
|
: undefined,
|
|
422
721
|
cwd: (() => {
|
|
423
722
|
const v = resolveTyped(node.cwd, node.cwdType, msg, "");
|
|
@@ -440,6 +739,16 @@ module.exports = function (RED) {
|
|
|
440
739
|
: num * 1000;
|
|
441
740
|
})(),
|
|
442
741
|
mcpServers: node.mcpServers,
|
|
742
|
+
systemPrompt: (() => {
|
|
743
|
+
const v = resolveTyped(node.systemPrompt, node.systemPromptType, msg, "");
|
|
744
|
+
return v === undefined || v === null ? "" : String(v).trim();
|
|
745
|
+
})(),
|
|
746
|
+
effort: (() => {
|
|
747
|
+
const v = resolveTyped(node.effort, node.effortType, msg, "");
|
|
748
|
+
return v === undefined || v === null ? "" : String(v).trim();
|
|
749
|
+
})(),
|
|
750
|
+
allowedTools: node.allowedTools,
|
|
751
|
+
deniedTools: node.deniedTools,
|
|
443
752
|
};
|
|
444
753
|
} catch (err) {
|
|
445
754
|
node.lastTerminal = "failed";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Fixed set of capability flags an AgentAdapter subclass may declare via
|
|
4
|
+
// a static `CAPABILITIES` object exported alongside the class (see
|
|
5
|
+
// opencode.js / pi.js). A flag must reflect wired-up behavior in this
|
|
6
|
+
// adapter's buildExecution()/parseResult(), never what the underlying
|
|
7
|
+
// CLI could theoretically support if we wired more of it up -- see
|
|
8
|
+
// Archon's ProviderCapabilities convention. Missing flags default to
|
|
9
|
+
// their listed default (all "unsupported").
|
|
10
|
+
const DEFAULT_CAPABILITIES = {
|
|
11
|
+
sessionResume: false,
|
|
12
|
+
structuredOutput: false, // false | "best-effort" | "enforced"
|
|
13
|
+
toolRestrictions: false,
|
|
14
|
+
effortControl: false,
|
|
15
|
+
systemPromptControl: false,
|
|
16
|
+
costReporting: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getCapabilities(adapter) {
|
|
20
|
+
return Object.assign({}, DEFAULT_CAPABILITIES, adapter.constructor.CAPABILITIES || {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { DEFAULT_CAPABILITIES, getCapabilities };
|