@tbrandenburg/node-red-agents 0.3.8 → 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/nodes/agent/agent.html +1017 -627
- package/nodes/agent/agent.js +353 -47
- package/nodes/agent/lib/agents/capabilities.js +23 -0
- package/nodes/agent/lib/agents/opencode.js +133 -20
- package/nodes/agent/lib/agents/pi.js +32 -1
- package/nodes/agent/lib/execution/inputs.js +22 -0
- package/nodes/agent/lib/execution/retry.js +61 -0
- package/nodes/agent/lib/execution/structured-output.js +111 -0
- package/package.json +4 -1
package/nodes/agent/agent.html
CHANGED
|
@@ -1,641 +1,1031 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const MODEL_SUGGESTIONS = [
|
|
6
|
+
"opencode/big-pickle",
|
|
7
|
+
"opencode/deepseek-v4-flash-free",
|
|
8
|
+
"opencode/hy3-free",
|
|
9
|
+
"opencode/laguna-s-2.1-free",
|
|
10
|
+
"opencode/mimo-v2.5-free",
|
|
11
|
+
"anthropic/claude-sonnet-5",
|
|
12
|
+
"anthropic/claude-opus-4-8",
|
|
13
|
+
"anthropic/claude-haiku-4-5",
|
|
14
|
+
"github-copilot/claude-sonnet-5",
|
|
15
|
+
"github-copilot/gpt-5.4",
|
|
16
|
+
"openai/gpt-5.4",
|
|
17
|
+
"openai/gpt-4.1",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function mcpServerRow(container, data) {
|
|
21
|
+
data = data || { name: "", type: "remote", url: "", command: "", args: [] };
|
|
22
|
+
const row = $("<div/>", {
|
|
23
|
+
style: "display:flex; gap:4px; align-items:flex-start; width:100%;",
|
|
24
|
+
}).appendTo(container);
|
|
25
|
+
|
|
26
|
+
const nameInput = $("<input/>", { type: "text", placeholder: "name", style: "width:110px" })
|
|
27
|
+
.val(data.name || "")
|
|
28
|
+
.appendTo(row);
|
|
29
|
+
|
|
30
|
+
const typeSelect = $("<select/>", { style: "width:90px" })
|
|
31
|
+
.append('<option value="remote">Remote</option>')
|
|
32
|
+
.append('<option value="local">Local</option>')
|
|
33
|
+
.val(data.type || "remote")
|
|
34
|
+
.appendTo(row);
|
|
35
|
+
|
|
36
|
+
const fieldsContainer = $("<div/>", { style: "flex:1; display:flex; gap:4px;" }).appendTo(
|
|
37
|
+
row,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function renderFields() {
|
|
41
|
+
fieldsContainer.empty();
|
|
42
|
+
if (typeSelect.val() === "remote") {
|
|
43
|
+
$("<input/>", {
|
|
44
|
+
type: "text",
|
|
45
|
+
placeholder: "https://mcp.example.com/...",
|
|
46
|
+
class: "mcp-url",
|
|
47
|
+
style: "width:100%",
|
|
48
|
+
})
|
|
49
|
+
.val(data.url || "")
|
|
50
|
+
.appendTo(fieldsContainer);
|
|
51
|
+
} else {
|
|
52
|
+
$("<input/>", {
|
|
53
|
+
type: "text",
|
|
54
|
+
placeholder: "command (e.g. npx)",
|
|
55
|
+
class: "mcp-command",
|
|
56
|
+
style: "width:35%",
|
|
57
|
+
})
|
|
58
|
+
.val(data.command || "")
|
|
59
|
+
.appendTo(fieldsContainer);
|
|
60
|
+
$("<input/>", {
|
|
61
|
+
type: "text",
|
|
62
|
+
placeholder: "args (space separated)",
|
|
63
|
+
class: "mcp-args",
|
|
64
|
+
style: "width:65%",
|
|
65
|
+
})
|
|
66
|
+
.val(Array.isArray(data.args) ? data.args.join(" ") : data.args || "")
|
|
67
|
+
.appendTo(fieldsContainer);
|
|
66
68
|
}
|
|
69
|
+
}
|
|
70
|
+
typeSelect.on("change", renderFields);
|
|
71
|
+
renderFields();
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
row.data("mcpGetValue", function () {
|
|
74
|
+
const type = typeSelect.val();
|
|
75
|
+
const out = { name: nameInput.val().trim(), type: type };
|
|
76
|
+
if (type === "remote") {
|
|
77
|
+
out.url = fieldsContainer.find(".mcp-url").val().trim();
|
|
78
|
+
} else {
|
|
79
|
+
out.command = fieldsContainer.find(".mcp-command").val().trim();
|
|
80
|
+
const argsRaw = fieldsContainer.find(".mcp-args").val().trim();
|
|
81
|
+
out.args = argsRaw ? argsRaw.split(/\s+/) : [];
|
|
76
82
|
}
|
|
83
|
+
return out;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Much simpler than mcpServerRow: one plain text input per row, no
|
|
88
|
+
// per-row type switch. Used for the SRT "Allowed domains" and
|
|
89
|
+
// "Allowed write directories" lists.
|
|
90
|
+
let inputsRowCounter = 0;
|
|
91
|
+
function inputsRow(container, data) {
|
|
92
|
+
data = data || { name: "", value: "", valueType: "msg" };
|
|
93
|
+
inputsRowCounter += 1;
|
|
94
|
+
const rowId = `agent-inputs-row-${inputsRowCounter}`;
|
|
95
|
+
const row = $("<div/>", {
|
|
96
|
+
style: "display:flex; gap:4px; align-items:center; width:100%;",
|
|
97
|
+
}).appendTo(container);
|
|
98
|
+
|
|
99
|
+
const nameInput = $("<input/>", { type: "text", placeholder: "name", style: "width:110px" })
|
|
100
|
+
.val(data.name || "")
|
|
101
|
+
.appendTo(row);
|
|
102
|
+
|
|
103
|
+
const valueInput = $("<input/>", { type: "text", id: rowId, style: "flex:1" })
|
|
104
|
+
.val(data.value || "")
|
|
105
|
+
.appendTo(row);
|
|
106
|
+
const valueType = $("<input/>", { type: "hidden", id: `${rowId}-type` }).appendTo(row);
|
|
107
|
+
valueInput.typedInput({
|
|
108
|
+
typeField: `#${rowId}-type`,
|
|
109
|
+
types: ["msg", "str", "flow", "global", "env"],
|
|
110
|
+
});
|
|
111
|
+
valueInput.typedInput("type", data.valueType || "msg");
|
|
112
|
+
|
|
113
|
+
row.data("inputsGetValue", function () {
|
|
114
|
+
return {
|
|
115
|
+
name: nameInput.val().trim(),
|
|
116
|
+
value: valueInput.typedInput("value"),
|
|
117
|
+
valueType: valueInput.typedInput("type"),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
row.data("inputsValueType", valueType);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function simpleStringRow(container, value) {
|
|
124
|
+
const input = $("<input/>", { type: "text", style: "width:100%" })
|
|
125
|
+
.val(value || "")
|
|
126
|
+
.appendTo(container);
|
|
127
|
+
container.data("getValue", function () {
|
|
128
|
+
return input.val().trim();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
RED.nodes.registerType("agent", {
|
|
133
|
+
category: "Agent",
|
|
134
|
+
color: "#8B5CF6",
|
|
135
|
+
defaults: {
|
|
136
|
+
name: { value: "" },
|
|
137
|
+
agent: { value: "opencode" },
|
|
138
|
+
runtime: { value: "direct" },
|
|
139
|
+
invocation: { value: "prompt" },
|
|
140
|
+
|
|
141
|
+
agentName: { value: "" },
|
|
142
|
+
agentNameType: { value: "str" },
|
|
143
|
+
|
|
144
|
+
model: { value: "" },
|
|
145
|
+
modelType: { value: "str" },
|
|
146
|
+
|
|
147
|
+
prompt: { value: "payload" },
|
|
148
|
+
promptType: { value: "msg" },
|
|
149
|
+
|
|
150
|
+
sessionIdProp: { value: "sessionID" },
|
|
151
|
+
sessionIdPropType: { value: "msg" },
|
|
152
|
+
|
|
153
|
+
invocationName: { value: "" },
|
|
154
|
+
invocationNameType: { value: "str" },
|
|
155
|
+
|
|
156
|
+
arguments: { value: "payload" },
|
|
157
|
+
argumentsType: { value: "msg" },
|
|
158
|
+
|
|
159
|
+
inputs: { value: [] },
|
|
160
|
+
|
|
161
|
+
cwd: { value: "cwd" },
|
|
162
|
+
cwdType: { value: "msg" },
|
|
163
|
+
|
|
164
|
+
auto: { value: false },
|
|
165
|
+
|
|
166
|
+
concurrency: { value: 1, validate: RED.validators.number() },
|
|
167
|
+
|
|
168
|
+
timeout: { value: "" },
|
|
169
|
+
timeoutType: { value: "num" },
|
|
170
|
+
|
|
171
|
+
mcpServers: { value: [] },
|
|
172
|
+
|
|
173
|
+
systemPrompt: { value: "" },
|
|
174
|
+
systemPromptType: { value: "str" },
|
|
175
|
+
|
|
176
|
+
effort: { value: "" },
|
|
177
|
+
effortType: { value: "str" },
|
|
77
178
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
arguments: { value: 'payload' },
|
|
103
|
-
argumentsType: { value: 'msg' },
|
|
104
|
-
|
|
105
|
-
cwd: { value: 'cwd' },
|
|
106
|
-
cwdType: { value: 'msg' },
|
|
107
|
-
|
|
108
|
-
auto: { value: false },
|
|
109
|
-
|
|
110
|
-
concurrency: { value: 1, validate: RED.validators.number() },
|
|
111
|
-
|
|
112
|
-
timeout: { value: '' },
|
|
113
|
-
timeoutType: { value: 'num' },
|
|
114
|
-
|
|
115
|
-
mcpServers: { value: [] },
|
|
116
|
-
|
|
117
|
-
srtBinary: { value: '' },
|
|
118
|
-
srtSettingsMode: { value: 'file' },
|
|
119
|
-
srtSettingsPath: { value: '' },
|
|
120
|
-
srtAllowedDomains: { value: [] },
|
|
121
|
-
srtAllowedWriteDirs: { value: ['.', '/tmp', '~/.local/share/opencode'] },
|
|
122
|
-
srtStrictAllowlist: { value: true },
|
|
123
|
-
srtAdvancedJson: {
|
|
124
|
-
value: '',
|
|
125
|
-
validate: function (v) {
|
|
126
|
-
if (!v || !v.trim()) return true;
|
|
127
|
-
try {
|
|
128
|
-
JSON.parse(v);
|
|
129
|
-
return true;
|
|
130
|
-
} catch (err) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
inputs: 1,
|
|
137
|
-
outputs: 2,
|
|
138
|
-
outputLabels: ['result', 'events'],
|
|
139
|
-
icon: 'agent.svg',
|
|
140
|
-
paletteLabel: 'agent',
|
|
141
|
-
label: function () {
|
|
142
|
-
return this.name || (this.agent + ' (' + this.runtime + ')');
|
|
143
|
-
},
|
|
144
|
-
oneditprepare: function () {
|
|
145
|
-
const node = this;
|
|
146
|
-
|
|
147
|
-
$('#node-input-model').typedInput({
|
|
148
|
-
typeField: '#node-input-modelType',
|
|
149
|
-
types: ['str', 'msg', 'flow', 'global', 'env']
|
|
150
|
-
});
|
|
151
|
-
$('#node-input-model').attr('list', 'agent-model-suggestions');
|
|
152
|
-
if (!$('#agent-model-suggestions').length) {
|
|
153
|
-
const datalist = $('<datalist id="agent-model-suggestions"></datalist>').appendTo('body');
|
|
154
|
-
MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
$('#node-input-agentName').typedInput({
|
|
158
|
-
typeField: '#node-input-agentNameType',
|
|
159
|
-
types: ['str', 'msg', 'flow', 'global', 'env']
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
$('#node-input-prompt').typedInput({
|
|
163
|
-
typeField: '#node-input-promptType',
|
|
164
|
-
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
165
|
-
});
|
|
166
|
-
$('#node-input-sessionIdProp').typedInput({
|
|
167
|
-
typeField: '#node-input-sessionIdPropType',
|
|
168
|
-
types: ['msg', 'str', 'flow', 'global']
|
|
169
|
-
});
|
|
170
|
-
// RED.popover (Node-RED's own hover-tooltip system) rather
|
|
171
|
-
// than a plain HTML `title` attribute: `title` is the
|
|
172
|
-
// browser's native tooltip, whose appearance/delay/is even
|
|
173
|
-
// shown at all depends on OS/browser/accessibility settings
|
|
174
|
-
// outside our control (reported: not appearing at all in
|
|
175
|
-
// Firefox). RED.popover.create with trigger:'hover' is
|
|
176
|
-
// Node-RED's own JS-driven popover, used throughout its own
|
|
177
|
-
// editor (e.g. the debug sidebar's copy buttons) -- it
|
|
178
|
-
// doesn't depend on the browser's native tooltip behavior at
|
|
179
|
-
// all, so it renders identically regardless of browser.
|
|
180
|
-
RED.popover.create({
|
|
181
|
-
target: $('#agent-session-id-hint'),
|
|
182
|
-
direction: 'right',
|
|
183
|
-
trigger: 'hover',
|
|
184
|
-
size: 'small',
|
|
185
|
-
width: 320,
|
|
186
|
-
content:
|
|
187
|
-
'Empty/absent → starts a <b>new</b> session (today\'s default). A <b>present</b> id ' +
|
|
188
|
-
'resumes that session instead (<code>opencode run --session <id> ...</code>). The ' +
|
|
189
|
-
'result\'s <code>msg.sessionID</code> (also under <code>agentExecution.sessionID</code>) ' +
|
|
190
|
-
'can be fed straight back into this field on the next message to continue the ' +
|
|
191
|
-
'conversation. OpenCode only -- Pi does not support session continuation.'
|
|
192
|
-
});
|
|
193
|
-
$('#node-input-invocationName').typedInput({
|
|
194
|
-
typeField: '#node-input-invocationNameType',
|
|
195
|
-
types: ['str', 'msg', 'flow', 'global']
|
|
196
|
-
});
|
|
197
|
-
$('#node-input-arguments').typedInput({
|
|
198
|
-
typeField: '#node-input-argumentsType',
|
|
199
|
-
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
200
|
-
});
|
|
201
|
-
$('#node-input-cwd').typedInput({
|
|
202
|
-
typeField: '#node-input-cwdType',
|
|
203
|
-
types: ['msg', 'str', 'flow', 'global', 'env']
|
|
204
|
-
});
|
|
205
|
-
$('#node-input-timeout').typedInput({
|
|
206
|
-
typeField: '#node-input-timeoutType',
|
|
207
|
-
types: ['num', 'msg', 'flow', 'global', 'env']
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const NAME_PLACEHOLDERS = {
|
|
211
|
-
opencode: { skill: 'code-review', command: 'review' },
|
|
212
|
-
pi: { skill: 'code-review (looked up under .github/skills/)', command: 'review (looked up under .github/prompts/)' }
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
function updateInvocationRows() {
|
|
216
|
-
const invocation = $('#node-input-invocation').val();
|
|
217
|
-
const agent = $('#node-input-agent').val();
|
|
218
|
-
$('.agent-row-prompt').toggle(invocation === 'prompt');
|
|
219
|
-
$('.agent-row-name').toggle(invocation !== 'prompt');
|
|
220
|
-
$('.agent-row-arguments').toggle(invocation !== 'prompt');
|
|
221
|
-
const label = invocation === 'skill' ? 'Skill' : 'Command';
|
|
222
|
-
$('#agent-name-label-text').text(label);
|
|
223
|
-
const placeholders = NAME_PLACEHOLDERS[agent] || NAME_PLACEHOLDERS.opencode;
|
|
224
|
-
$('#node-input-invocationName').attr('placeholder', placeholders[invocation] || '');
|
|
225
|
-
}
|
|
226
|
-
$('#node-input-invocation').on('change', updateInvocationRows);
|
|
227
|
-
|
|
228
|
-
function updateRuntimeRows() {
|
|
229
|
-
$('.agent-row-srt').toggle($('#node-input-runtime').val() === 'srt');
|
|
230
|
-
}
|
|
231
|
-
$('#node-input-runtime').on('change', updateRuntimeRows);
|
|
232
|
-
|
|
233
|
-
function updateSrtModeRows() {
|
|
234
|
-
const mode = $('#node-input-srtSettingsMode').val();
|
|
235
|
-
$('.agent-row-srt-file').toggle(mode === 'file');
|
|
236
|
-
$('.agent-row-srt-inline').toggle(mode === 'inline');
|
|
237
|
-
}
|
|
238
|
-
$('#node-input-srtSettingsMode').on('change', updateSrtModeRows);
|
|
239
|
-
|
|
240
|
-
const AUTO_HINTS = {
|
|
241
|
-
opencode: 'Maps to --auto: skips permission prompts for actions not explicitly denied.',
|
|
242
|
-
pi: 'Pi has no permission prompts to bypass. Off = read-only tools (read/grep/find/ls). On = full tool access (bash/edit/write too).'
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
// Grey out (rather than hide) fields an agent doesn't
|
|
246
|
-
// support, so it's clear the setting exists but won't take
|
|
247
|
-
// effect for the currently selected agent -- instead of the
|
|
248
|
-
// section silently disappearing.
|
|
249
|
-
function setSectionSupported($section, supported) {
|
|
250
|
-
$section.css({
|
|
251
|
-
opacity: supported ? '' : 0.45,
|
|
252
|
-
pointerEvents: supported ? '' : 'none'
|
|
253
|
-
});
|
|
254
|
-
$section.find('input, select, button, textarea').prop('disabled', !supported);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const list = $('#node-input-mcpServers-list');
|
|
258
|
-
list.editableList({
|
|
259
|
-
addButton: 'Add MCP Server',
|
|
260
|
-
height: 150,
|
|
261
|
-
addItem: function (container, index, data) {
|
|
262
|
-
mcpServerRow(container, data);
|
|
263
|
-
},
|
|
264
|
-
removable: true,
|
|
265
|
-
sortable: true
|
|
266
|
-
});
|
|
267
|
-
(node.mcpServers || []).forEach((entry) => list.editableList('addItem', entry));
|
|
268
|
-
|
|
269
|
-
const domainsList = $('#node-input-srtAllowedDomains-list');
|
|
270
|
-
domainsList.editableList({
|
|
271
|
-
addButton: 'Add domain',
|
|
272
|
-
height: 100,
|
|
273
|
-
addItem: function (container, index, value) {
|
|
274
|
-
simpleStringRow(container, value);
|
|
275
|
-
},
|
|
276
|
-
removable: true,
|
|
277
|
-
sortable: true
|
|
278
|
-
});
|
|
279
|
-
(node.srtAllowedDomains || []).forEach((entry) => domainsList.editableList('addItem', entry));
|
|
280
|
-
|
|
281
|
-
const writeDirsList = $('#node-input-srtAllowedWriteDirs-list');
|
|
282
|
-
writeDirsList.editableList({
|
|
283
|
-
addButton: 'Add directory',
|
|
284
|
-
height: 100,
|
|
285
|
-
addItem: function (container, index, value) {
|
|
286
|
-
simpleStringRow(container, value);
|
|
287
|
-
},
|
|
288
|
-
removable: true,
|
|
289
|
-
sortable: true
|
|
290
|
-
});
|
|
291
|
-
(node.srtAllowedWriteDirs || []).forEach((entry) => writeDirsList.editableList('addItem', entry));
|
|
292
|
-
|
|
293
|
-
function updateAgentRows() {
|
|
294
|
-
const agent = $('#node-input-agent').val();
|
|
295
|
-
$('#agent-auto-hint').attr('title', AUTO_HINTS[agent] || '');
|
|
296
|
-
const mcpSupported = agent !== 'pi';
|
|
297
|
-
setSectionSupported($('.agent-row-mcp'), mcpSupported);
|
|
298
|
-
$('.agent-row-mcp-hint').toggle(!mcpSupported);
|
|
299
|
-
const sessionSupported = agent !== 'pi';
|
|
300
|
-
setSectionSupported($('.agent-row-session'), sessionSupported);
|
|
301
|
-
$('.agent-row-session-hint').toggle(!sessionSupported);
|
|
302
|
-
updateInvocationRows();
|
|
303
|
-
}
|
|
304
|
-
$('#node-input-agent').on('change', updateAgentRows);
|
|
305
|
-
updateAgentRows();
|
|
306
|
-
updateRuntimeRows();
|
|
307
|
-
updateSrtModeRows();
|
|
308
|
-
},
|
|
309
|
-
oneditsave: function () {
|
|
310
|
-
const list = $('#node-input-mcpServers-list');
|
|
311
|
-
const items = list.editableList('items');
|
|
312
|
-
const out = [];
|
|
313
|
-
items.each(function () {
|
|
314
|
-
out.push($(this).data('mcpGetValue')());
|
|
315
|
-
});
|
|
316
|
-
this.mcpServers = out;
|
|
317
|
-
|
|
318
|
-
function collectStrings(listEl) {
|
|
319
|
-
const values = [];
|
|
320
|
-
listEl.editableList('items').each(function () {
|
|
321
|
-
const v = $(this).data('getValue')();
|
|
322
|
-
if (v) values.push(v);
|
|
323
|
-
});
|
|
324
|
-
return values;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Only persist the SRT inline-settings lists/fields while
|
|
328
|
-
// they're actually the active runtime+mode -- otherwise
|
|
329
|
-
// reset them to their defaults. Without this, switching
|
|
330
|
-
// Runtime back to Direct (or SRT settings back to File
|
|
331
|
-
// path) silently keeps whatever domains/directories/JSON
|
|
332
|
-
// were entered earlier, still saved into the flow even
|
|
333
|
-
// though the now-hidden fields are irrelevant to the
|
|
334
|
-
// deployed config -- confusing at best, and a footgun if
|
|
335
|
-
// the runtime is ever switched back to SRT without
|
|
336
|
-
// re-checking these stale values.
|
|
337
|
-
//
|
|
338
|
-
// Note: Node-RED's own core copies each defaults-bound
|
|
339
|
-
// plain input's *current DOM value* into `this[key]`
|
|
340
|
-
// right after oneditsave returns, so setting e.g.
|
|
341
|
-
// `this.srtSettingsMode` here alone is not enough -- a
|
|
342
|
-
// hidden <select>/<input>/<textarea> still holds its old
|
|
343
|
-
// value and would silently overwrite it straight back.
|
|
344
|
-
// The reset has to touch the DOM element itself for any
|
|
345
|
-
// field with a plain `#node-input-<name>` binding;
|
|
346
|
-
// srtAllowedDomains/srtAllowedWriteDirs are the exception
|
|
347
|
-
// (no such element exists -- only the "-list" editableList
|
|
348
|
-
// container), so those are fine set directly on `this`.
|
|
349
|
-
const runtime = $('#node-input-runtime').val();
|
|
350
|
-
const srtSettingsMode = $('#node-input-srtSettingsMode').val();
|
|
351
|
-
|
|
352
|
-
if (runtime !== 'srt') {
|
|
353
|
-
$('#node-input-srtSettingsMode').val('file');
|
|
354
|
-
$('#node-input-srtSettingsPath').val('');
|
|
355
|
-
$('#node-input-srtBinary').val('');
|
|
356
|
-
$('#node-input-srtStrictAllowlist').prop('checked', true);
|
|
357
|
-
$('#node-input-srtAdvancedJson').val('');
|
|
358
|
-
this.srtAllowedDomains = [];
|
|
359
|
-
this.srtAllowedWriteDirs = ['.', '/tmp', '~/.local/share/opencode'];
|
|
360
|
-
} else if (srtSettingsMode !== 'inline') {
|
|
361
|
-
$('#node-input-srtStrictAllowlist').prop('checked', true);
|
|
362
|
-
$('#node-input-srtAdvancedJson').val('');
|
|
363
|
-
this.srtAllowedDomains = [];
|
|
364
|
-
this.srtAllowedWriteDirs = ['.', '/tmp', '~/.local/share/opencode'];
|
|
365
|
-
} else {
|
|
366
|
-
this.srtAllowedDomains = collectStrings($('#node-input-srtAllowedDomains-list'));
|
|
367
|
-
this.srtAllowedWriteDirs = collectStrings($('#node-input-srtAllowedWriteDirs-list'));
|
|
368
|
-
}
|
|
179
|
+
allowedTools: { value: [] },
|
|
180
|
+
deniedTools: { value: [] },
|
|
181
|
+
|
|
182
|
+
// Node-level retry (issue #24): opt-in-by-default at a
|
|
183
|
+
// conservative setting. retryMaxAttempts counts total attempts
|
|
184
|
+
// (1 original + retries), e.g. 2 = one retry.
|
|
185
|
+
retryMaxAttempts: { value: 2, validate: RED.validators.number() },
|
|
186
|
+
retryDelayMs: { value: 3000, validate: RED.validators.number() },
|
|
187
|
+
retryOnError: { value: "transient" },
|
|
188
|
+
|
|
189
|
+
// output_format (issue #23): raw JSON-Schema text, fully opt-in
|
|
190
|
+
// (default empty/disabled). Only checked for well-formed JSON
|
|
191
|
+
// here (client-side) -- the actual AJV schema compile + capability
|
|
192
|
+
// gate happens server-side at deploy time (see agent.js), same
|
|
193
|
+
// division of labor as srtAdvancedJson below.
|
|
194
|
+
outputFormat: {
|
|
195
|
+
value: "",
|
|
196
|
+
validate: function (v) {
|
|
197
|
+
if (!v || !v.trim()) return true;
|
|
198
|
+
try {
|
|
199
|
+
JSON.parse(v);
|
|
200
|
+
return true;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return false;
|
|
369
203
|
}
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
srtBinary: { value: "" },
|
|
209
|
+
srtSettingsMode: { value: "file" },
|
|
210
|
+
srtSettingsPath: { value: "" },
|
|
211
|
+
srtAllowedDomains: { value: [] },
|
|
212
|
+
srtAllowedWriteDirs: { value: [".", "/tmp", "~/.local/share/opencode"] },
|
|
213
|
+
srtStrictAllowlist: { value: true },
|
|
214
|
+
srtAdvancedJson: {
|
|
215
|
+
value: "",
|
|
216
|
+
validate: function (v) {
|
|
217
|
+
if (!v || !v.trim()) return true;
|
|
218
|
+
try {
|
|
219
|
+
JSON.parse(v);
|
|
220
|
+
return true;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
inputs: 1,
|
|
228
|
+
outputs: 2,
|
|
229
|
+
outputLabels: ["result", "events"],
|
|
230
|
+
icon: "agent.svg",
|
|
231
|
+
paletteLabel: "agent",
|
|
232
|
+
label: function () {
|
|
233
|
+
return this.name || this.agent + " (" + this.runtime + ")";
|
|
234
|
+
},
|
|
235
|
+
oneditprepare: function () {
|
|
236
|
+
const node = this;
|
|
237
|
+
|
|
238
|
+
$("#node-input-model").typedInput({
|
|
239
|
+
typeField: "#node-input-modelType",
|
|
240
|
+
types: ["str", "msg", "flow", "global", "env"],
|
|
241
|
+
});
|
|
242
|
+
$("#node-input-model").attr("list", "agent-model-suggestions");
|
|
243
|
+
if (!$("#agent-model-suggestions").length) {
|
|
244
|
+
const datalist = $('<datalist id="agent-model-suggestions"></datalist>').appendTo("body");
|
|
245
|
+
MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$("#node-input-agentName").typedInput({
|
|
249
|
+
typeField: "#node-input-agentNameType",
|
|
250
|
+
types: ["str", "msg", "flow", "global", "env"],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
$("#node-input-prompt").typedInput({
|
|
254
|
+
typeField: "#node-input-promptType",
|
|
255
|
+
types: ["msg", "str", "flow", "global", "env"],
|
|
256
|
+
});
|
|
257
|
+
$("#node-input-sessionIdProp").typedInput({
|
|
258
|
+
typeField: "#node-input-sessionIdPropType",
|
|
259
|
+
types: ["msg", "str", "flow", "global"],
|
|
260
|
+
});
|
|
261
|
+
// RED.popover (Node-RED's own hover-tooltip system) rather
|
|
262
|
+
// than a plain HTML `title` attribute: `title` is the
|
|
263
|
+
// browser's native tooltip, whose appearance/delay/is even
|
|
264
|
+
// shown at all depends on OS/browser/accessibility settings
|
|
265
|
+
// outside our control (reported: not appearing at all in
|
|
266
|
+
// Firefox). RED.popover.create with trigger:'hover' is
|
|
267
|
+
// Node-RED's own JS-driven popover, used throughout its own
|
|
268
|
+
// editor (e.g. the debug sidebar's copy buttons) -- it
|
|
269
|
+
// doesn't depend on the browser's native tooltip behavior at
|
|
270
|
+
// all, so it renders identically regardless of browser.
|
|
271
|
+
RED.popover.create({
|
|
272
|
+
target: $("#agent-session-id-hint"),
|
|
273
|
+
direction: "right",
|
|
274
|
+
trigger: "hover",
|
|
275
|
+
size: "small",
|
|
276
|
+
width: 320,
|
|
277
|
+
content:
|
|
278
|
+
"Empty/absent → starts a <b>new</b> session (today's default). A <b>present</b> id " +
|
|
279
|
+
"resumes that session instead (<code>opencode run --session <id> ...</code>). The " +
|
|
280
|
+
"result's <code>msg.sessionID</code> (also under <code>agentExecution.sessionID</code>) " +
|
|
281
|
+
"can be fed straight back into this field on the next message to continue the " +
|
|
282
|
+
"conversation. OpenCode only -- Pi does not support session continuation.",
|
|
283
|
+
});
|
|
284
|
+
$("#node-input-invocationName").typedInput({
|
|
285
|
+
typeField: "#node-input-invocationNameType",
|
|
286
|
+
types: ["str", "msg", "flow", "global"],
|
|
287
|
+
});
|
|
288
|
+
$("#node-input-arguments").typedInput({
|
|
289
|
+
typeField: "#node-input-argumentsType",
|
|
290
|
+
types: ["msg", "str", "flow", "global", "env"],
|
|
291
|
+
});
|
|
292
|
+
$("#node-input-cwd").typedInput({
|
|
293
|
+
typeField: "#node-input-cwdType",
|
|
294
|
+
types: ["msg", "str", "flow", "global", "env"],
|
|
295
|
+
});
|
|
296
|
+
$("#node-input-timeout").typedInput({
|
|
297
|
+
typeField: "#node-input-timeoutType",
|
|
298
|
+
types: ["num", "msg", "flow", "global", "env"],
|
|
299
|
+
});
|
|
300
|
+
$("#node-input-systemPrompt").typedInput({
|
|
301
|
+
typeField: "#node-input-systemPromptType",
|
|
302
|
+
types: ["str", "msg", "flow", "global", "env"],
|
|
303
|
+
});
|
|
304
|
+
$("#node-input-effort").typedInput({
|
|
305
|
+
typeField: "#node-input-effortType",
|
|
306
|
+
types: ["str", "msg", "flow", "global", "env"],
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const NAME_PLACEHOLDERS = {
|
|
310
|
+
opencode: { skill: "code-review", command: "review" },
|
|
311
|
+
pi: {
|
|
312
|
+
skill: "code-review (looked up under .github/skills/)",
|
|
313
|
+
command: "review (looked up under .github/prompts/)",
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function updateInvocationRows() {
|
|
318
|
+
const invocation = $("#node-input-invocation").val();
|
|
319
|
+
const agent = $("#node-input-agent").val();
|
|
320
|
+
$(".agent-row-prompt").toggle(invocation === "prompt");
|
|
321
|
+
$(".agent-row-name").toggle(invocation !== "prompt");
|
|
322
|
+
$(".agent-row-arguments").toggle(invocation !== "prompt");
|
|
323
|
+
const label = invocation === "skill" ? "Skill" : "Command";
|
|
324
|
+
$("#agent-name-label-text").text(label);
|
|
325
|
+
const placeholders = NAME_PLACEHOLDERS[agent] || NAME_PLACEHOLDERS.opencode;
|
|
326
|
+
$("#node-input-invocationName").attr("placeholder", placeholders[invocation] || "");
|
|
327
|
+
}
|
|
328
|
+
$("#node-input-invocation").on("change", updateInvocationRows);
|
|
329
|
+
|
|
330
|
+
function updateRuntimeRows() {
|
|
331
|
+
$(".agent-row-srt").toggle($("#node-input-runtime").val() === "srt");
|
|
332
|
+
}
|
|
333
|
+
$("#node-input-runtime").on("change", updateRuntimeRows);
|
|
334
|
+
|
|
335
|
+
function updateSrtModeRows() {
|
|
336
|
+
const mode = $("#node-input-srtSettingsMode").val();
|
|
337
|
+
$(".agent-row-srt-file").toggle(mode === "file");
|
|
338
|
+
$(".agent-row-srt-inline").toggle(mode === "inline");
|
|
339
|
+
}
|
|
340
|
+
$("#node-input-srtSettingsMode").on("change", updateSrtModeRows);
|
|
341
|
+
|
|
342
|
+
const AUTO_HINTS = {
|
|
343
|
+
opencode: "Maps to --auto: skips permission prompts for actions not explicitly denied.",
|
|
344
|
+
pi: "Pi has no permission prompts to bypass. Off = read-only tools (read/grep/find/ls). On = full tool access (bash/edit/write too).",
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Grey out (rather than hide) fields an agent doesn't
|
|
348
|
+
// support, so it's clear the setting exists but won't take
|
|
349
|
+
// effect for the currently selected agent -- instead of the
|
|
350
|
+
// section silently disappearing.
|
|
351
|
+
function setSectionSupported($section, supported) {
|
|
352
|
+
$section.css({
|
|
353
|
+
opacity: supported ? "" : 0.45,
|
|
354
|
+
pointerEvents: supported ? "" : "none",
|
|
355
|
+
});
|
|
356
|
+
$section.find("input, select, button, textarea").prop("disabled", !supported);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const list = $("#node-input-mcpServers-list");
|
|
360
|
+
list.editableList({
|
|
361
|
+
addButton: "Add MCP Server",
|
|
362
|
+
height: 150,
|
|
363
|
+
addItem: function (container, index, data) {
|
|
364
|
+
mcpServerRow(container, data);
|
|
365
|
+
},
|
|
366
|
+
removable: true,
|
|
367
|
+
sortable: true,
|
|
368
|
+
});
|
|
369
|
+
(node.mcpServers || []).forEach((entry) => list.editableList("addItem", entry));
|
|
370
|
+
|
|
371
|
+
const allowedToolsList = $("#node-input-allowedTools-list");
|
|
372
|
+
allowedToolsList.editableList({
|
|
373
|
+
addButton: "Add allowed tool",
|
|
374
|
+
height: 100,
|
|
375
|
+
addItem: function (container, index, value) {
|
|
376
|
+
simpleStringRow(container, value);
|
|
377
|
+
},
|
|
378
|
+
removable: true,
|
|
379
|
+
sortable: true,
|
|
380
|
+
});
|
|
381
|
+
(node.allowedTools || []).forEach((entry) =>
|
|
382
|
+
allowedToolsList.editableList("addItem", entry),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const inputsList = $("#node-input-inputs-list");
|
|
386
|
+
inputsList.editableList({
|
|
387
|
+
addButton: "Add input",
|
|
388
|
+
height: 120,
|
|
389
|
+
addItem: function (container, index, data) {
|
|
390
|
+
inputsRow(container, data);
|
|
391
|
+
},
|
|
392
|
+
removable: true,
|
|
393
|
+
sortable: true,
|
|
394
|
+
});
|
|
395
|
+
(node.inputs || []).forEach((entry) => inputsList.editableList("addItem", entry));
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
const deniedToolsList = $("#node-input-deniedTools-list");
|
|
399
|
+
deniedToolsList.editableList({
|
|
400
|
+
addButton: "Add denied tool",
|
|
401
|
+
height: 100,
|
|
402
|
+
addItem: function (container, index, value) {
|
|
403
|
+
simpleStringRow(container, value);
|
|
404
|
+
},
|
|
405
|
+
removable: true,
|
|
406
|
+
sortable: true,
|
|
407
|
+
});
|
|
408
|
+
(node.deniedTools || []).forEach((entry) => deniedToolsList.editableList("addItem", entry));
|
|
409
|
+
|
|
410
|
+
const domainsList = $("#node-input-srtAllowedDomains-list");
|
|
411
|
+
domainsList.editableList({
|
|
412
|
+
addButton: "Add domain",
|
|
413
|
+
height: 100,
|
|
414
|
+
addItem: function (container, index, value) {
|
|
415
|
+
simpleStringRow(container, value);
|
|
416
|
+
},
|
|
417
|
+
removable: true,
|
|
418
|
+
sortable: true,
|
|
370
419
|
});
|
|
371
|
-
|
|
420
|
+
(node.srtAllowedDomains || []).forEach((entry) =>
|
|
421
|
+
domainsList.editableList("addItem", entry),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const writeDirsList = $("#node-input-srtAllowedWriteDirs-list");
|
|
425
|
+
writeDirsList.editableList({
|
|
426
|
+
addButton: "Add directory",
|
|
427
|
+
height: 100,
|
|
428
|
+
addItem: function (container, index, value) {
|
|
429
|
+
simpleStringRow(container, value);
|
|
430
|
+
},
|
|
431
|
+
removable: true,
|
|
432
|
+
sortable: true,
|
|
433
|
+
});
|
|
434
|
+
(node.srtAllowedWriteDirs || []).forEach((entry) =>
|
|
435
|
+
writeDirsList.editableList("addItem", entry),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
function updateAgentRows() {
|
|
439
|
+
const agent = $("#node-input-agent").val();
|
|
440
|
+
$("#agent-auto-hint").attr("title", AUTO_HINTS[agent] || "");
|
|
441
|
+
const mcpSupported = agent !== "pi";
|
|
442
|
+
setSectionSupported($(".agent-row-mcp"), mcpSupported);
|
|
443
|
+
$(".agent-row-mcp-hint").toggle(!mcpSupported);
|
|
444
|
+
const sessionSupported = agent !== "pi";
|
|
445
|
+
setSectionSupported($(".agent-row-session"), sessionSupported);
|
|
446
|
+
$(".agent-row-session-hint").toggle(!sessionSupported);
|
|
447
|
+
const effortSupported = agent === "opencode";
|
|
448
|
+
setSectionSupported($(".agent-row-effort"), effortSupported);
|
|
449
|
+
$(".agent-row-effort-hint").toggle(!effortSupported);
|
|
450
|
+
updateInvocationRows();
|
|
451
|
+
}
|
|
452
|
+
$("#node-input-agent").on("change", updateAgentRows);
|
|
453
|
+
updateAgentRows();
|
|
454
|
+
updateRuntimeRows();
|
|
455
|
+
updateSrtModeRows();
|
|
456
|
+
},
|
|
457
|
+
oneditsave: function () {
|
|
458
|
+
const list = $("#node-input-mcpServers-list");
|
|
459
|
+
const items = list.editableList("items");
|
|
460
|
+
const out = [];
|
|
461
|
+
items.each(function () {
|
|
462
|
+
out.push($(this).data("mcpGetValue")());
|
|
463
|
+
});
|
|
464
|
+
this.mcpServers = out;
|
|
465
|
+
|
|
466
|
+
function collectStrings(listEl) {
|
|
467
|
+
const values = [];
|
|
468
|
+
listEl.editableList("items").each(function () {
|
|
469
|
+
const v = $(this).data("getValue")();
|
|
470
|
+
if (v) values.push(v);
|
|
471
|
+
});
|
|
472
|
+
return values;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.allowedTools = collectStrings($("#node-input-allowedTools-list"));
|
|
476
|
+
this.deniedTools = collectStrings($("#node-input-deniedTools-list"));
|
|
477
|
+
|
|
478
|
+
const inputsOut = [];
|
|
479
|
+
$("#node-input-inputs-list")
|
|
480
|
+
.editableList("items")
|
|
481
|
+
.each(function () {
|
|
482
|
+
const v = $(this).data("inputsGetValue")();
|
|
483
|
+
if (v && v.name) inputsOut.push(v);
|
|
484
|
+
});
|
|
485
|
+
this.inputs = inputsOut;
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
// Only persist the SRT inline-settings lists/fields while
|
|
489
|
+
// they're actually the active runtime+mode -- otherwise
|
|
490
|
+
// reset them to their defaults. Without this, switching
|
|
491
|
+
// Runtime back to Direct (or SRT settings back to File
|
|
492
|
+
// path) silently keeps whatever domains/directories/JSON
|
|
493
|
+
// were entered earlier, still saved into the flow even
|
|
494
|
+
// though the now-hidden fields are irrelevant to the
|
|
495
|
+
// deployed config -- confusing at best, and a footgun if
|
|
496
|
+
// the runtime is ever switched back to SRT without
|
|
497
|
+
// re-checking these stale values.
|
|
498
|
+
//
|
|
499
|
+
// Note: Node-RED's own core copies each defaults-bound
|
|
500
|
+
// plain input's *current DOM value* into `this[key]`
|
|
501
|
+
// right after oneditsave returns, so setting e.g.
|
|
502
|
+
// `this.srtSettingsMode` here alone is not enough -- a
|
|
503
|
+
// hidden <select>/<input>/<textarea> still holds its old
|
|
504
|
+
// value and would silently overwrite it straight back.
|
|
505
|
+
// The reset has to touch the DOM element itself for any
|
|
506
|
+
// field with a plain `#node-input-<name>` binding;
|
|
507
|
+
// srtAllowedDomains/srtAllowedWriteDirs are the exception
|
|
508
|
+
// (no such element exists -- only the "-list" editableList
|
|
509
|
+
// container), so those are fine set directly on `this`.
|
|
510
|
+
const runtime = $("#node-input-runtime").val();
|
|
511
|
+
const srtSettingsMode = $("#node-input-srtSettingsMode").val();
|
|
512
|
+
|
|
513
|
+
if (runtime !== "srt") {
|
|
514
|
+
$("#node-input-srtSettingsMode").val("file");
|
|
515
|
+
$("#node-input-srtSettingsPath").val("");
|
|
516
|
+
$("#node-input-srtBinary").val("");
|
|
517
|
+
$("#node-input-srtStrictAllowlist").prop("checked", true);
|
|
518
|
+
$("#node-input-srtAdvancedJson").val("");
|
|
519
|
+
this.srtAllowedDomains = [];
|
|
520
|
+
this.srtAllowedWriteDirs = [".", "/tmp", "~/.local/share/opencode"];
|
|
521
|
+
} else if (srtSettingsMode !== "inline") {
|
|
522
|
+
$("#node-input-srtStrictAllowlist").prop("checked", true);
|
|
523
|
+
$("#node-input-srtAdvancedJson").val("");
|
|
524
|
+
this.srtAllowedDomains = [];
|
|
525
|
+
this.srtAllowedWriteDirs = [".", "/tmp", "~/.local/share/opencode"];
|
|
526
|
+
} else {
|
|
527
|
+
this.srtAllowedDomains = collectStrings($("#node-input-srtAllowedDomains-list"));
|
|
528
|
+
this.srtAllowedWriteDirs = collectStrings($("#node-input-srtAllowedWriteDirs-list"));
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
})();
|
|
372
533
|
</script>
|
|
373
534
|
|
|
374
535
|
<script type="text/html" data-template-name="agent">
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
</
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
</
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
</
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
</
|
|
496
|
-
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
536
|
+
<div class="form-row">
|
|
537
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
538
|
+
<input type="text" id="node-input-name" placeholder="Agent" />
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<div class="form-row">
|
|
542
|
+
<label for="node-input-agentName"
|
|
543
|
+
><i class="fa fa-id-badge"></i> Agent name
|
|
544
|
+
<i
|
|
545
|
+
class="fa fa-info-circle"
|
|
546
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
547
|
+
title="Reported as msg.agentName on the result/event outputs. Leave blank (str) to use the Name field above; set to msg/flow/global to make it dynamic per message."
|
|
548
|
+
></i>
|
|
549
|
+
</label>
|
|
550
|
+
<input
|
|
551
|
+
type="text"
|
|
552
|
+
id="node-input-agentName"
|
|
553
|
+
placeholder="leave blank to use the Name field above"
|
|
554
|
+
style="width:60%"
|
|
555
|
+
/>
|
|
556
|
+
<input type="hidden" id="node-input-agentNameType" />
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div class="form-row">
|
|
560
|
+
<label for="node-input-agent"><i class="fa fa-cube"></i> Agent</label>
|
|
561
|
+
<select id="node-input-agent" style="width:70%">
|
|
562
|
+
<option value="opencode">OpenCode</option>
|
|
563
|
+
<option value="pi">Pi</option>
|
|
564
|
+
</select>
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
<div class="form-row">
|
|
568
|
+
<label for="node-input-runtime"><i class="fa fa-cogs"></i> Runtime</label>
|
|
569
|
+
<select id="node-input-runtime" style="width:70%">
|
|
570
|
+
<option value="direct">Direct</option>
|
|
571
|
+
<option value="srt">SRT (sandboxed)</option>
|
|
572
|
+
</select>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="form-row agent-row-srt">
|
|
575
|
+
<label for="node-input-srtSettingsMode"><i class="fa fa-shield"></i> SRT settings</label>
|
|
576
|
+
<select id="node-input-srtSettingsMode" style="width:60%">
|
|
577
|
+
<option value="file">File path</option>
|
|
578
|
+
<option value="inline">Inline</option>
|
|
579
|
+
</select>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="form-row agent-row-srt agent-row-srt-file">
|
|
582
|
+
<label for="node-input-srtSettingsPath"> </label>
|
|
583
|
+
<input
|
|
584
|
+
type="text"
|
|
585
|
+
id="node-input-srtSettingsPath"
|
|
586
|
+
placeholder="~/.srt-settings.json (srt's own default)"
|
|
587
|
+
style="width:60%"
|
|
588
|
+
/>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
592
|
+
<label>
|
|
593
|
+
<i class="fa fa-globe"></i> Allowed domains
|
|
594
|
+
<i
|
|
595
|
+
class="fa fa-info-circle"
|
|
596
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
597
|
+
title="e.g. api.githubcopilot.com, opencode.ai -- the domain your configured model's API lives on."
|
|
598
|
+
></i>
|
|
599
|
+
</label>
|
|
600
|
+
<ol id="node-input-srtAllowedDomains-list"></ol>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
604
|
+
<label><i class="fa fa-folder"></i> Allowed write dirs</label>
|
|
605
|
+
<ol id="node-input-srtAllowedWriteDirs-list"></ol>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
609
|
+
<input
|
|
610
|
+
type="checkbox"
|
|
611
|
+
id="node-input-srtStrictAllowlist"
|
|
612
|
+
style="display:inline-block; width:auto; vertical-align:top;"
|
|
613
|
+
/>
|
|
614
|
+
<label for="node-input-srtStrictAllowlist" style="width:auto;">Strict allowlist</label>
|
|
615
|
+
<i
|
|
616
|
+
class="fa fa-info-circle"
|
|
617
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
618
|
+
title="Deny any host not in the allowed domains list. There is no interactive prompt to fall back on in non-interactive mode, so this should normally stay on."
|
|
619
|
+
></i>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div class="form-row agent-row-srt agent-row-srt-inline">
|
|
623
|
+
<details>
|
|
624
|
+
<summary style="cursor:pointer;">Advanced (raw JSON)</summary>
|
|
625
|
+
<textarea
|
|
626
|
+
id="node-input-srtAdvancedJson"
|
|
627
|
+
rows="4"
|
|
628
|
+
style="width:100%; font-family:monospace;"
|
|
629
|
+
placeholder='If set, replaces the domains/directories above entirely, e.g. {"network":{"allowedDomains":[],"deniedDomains":[]},"filesystem":{"allowWrite":[],"denyRead":[],"denyWrite":[]}}'
|
|
630
|
+
></textarea>
|
|
631
|
+
</details>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<div class="form-row agent-row-srt">
|
|
635
|
+
<label for="node-input-srtBinary"><i class="fa fa-terminal"></i> SRT binary</label>
|
|
636
|
+
<input type="text" id="node-input-srtBinary" placeholder="srt" style="width:60%" />
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<div class="form-row">
|
|
640
|
+
<label for="node-input-invocation"><i class="fa fa-play"></i> Invocation</label>
|
|
641
|
+
<select id="node-input-invocation" style="width:70%">
|
|
642
|
+
<option value="prompt">Prompt</option>
|
|
643
|
+
<option value="skill">Skill</option>
|
|
644
|
+
<option value="command">Command / Template</option>
|
|
645
|
+
</select>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div class="form-row agent-row-session">
|
|
649
|
+
<label for="node-input-sessionIdProp"
|
|
650
|
+
><i class="fa fa-key"></i> Session ID
|
|
651
|
+
<i
|
|
652
|
+
id="agent-session-id-hint"
|
|
653
|
+
class="fa fa-info-circle"
|
|
654
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
655
|
+
></i>
|
|
656
|
+
</label>
|
|
657
|
+
<input type="text" id="node-input-sessionIdProp" style="width:60%" />
|
|
658
|
+
<input type="hidden" id="node-input-sessionIdPropType" />
|
|
659
|
+
</div>
|
|
660
|
+
<div class="form-tips agent-row-session-hint" style="display:none;">
|
|
661
|
+
Pi does not support session continuation; switch Agent to OpenCode to use it.
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<div class="form-row agent-row-prompt">
|
|
665
|
+
<label for="node-input-prompt"><i class="fa fa-comment"></i> Prompt</label>
|
|
666
|
+
<input
|
|
667
|
+
type="text"
|
|
668
|
+
id="node-input-prompt"
|
|
669
|
+
placeholder="leave blank to use msg.payload"
|
|
670
|
+
style="width:60%"
|
|
671
|
+
/>
|
|
672
|
+
<input type="hidden" id="node-input-promptType" />
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<div class="form-row agent-row-name">
|
|
676
|
+
<label for="node-input-invocationName"
|
|
677
|
+
><i class="fa fa-magic"></i> <span id="agent-name-label-text">Command</span></label
|
|
678
|
+
>
|
|
679
|
+
<input type="text" id="node-input-invocationName" style="width:60%" />
|
|
680
|
+
<input type="hidden" id="node-input-invocationNameType" />
|
|
681
|
+
</div>
|
|
682
|
+
<div class="form-row agent-row-arguments">
|
|
683
|
+
<label for="node-input-arguments"><i class="fa fa-list"></i> Arguments</label>
|
|
684
|
+
<input
|
|
685
|
+
type="text"
|
|
686
|
+
id="node-input-arguments"
|
|
687
|
+
placeholder="leave blank to use msg.payload"
|
|
688
|
+
style="width:60%"
|
|
689
|
+
/>
|
|
690
|
+
<input type="hidden" id="node-input-argumentsType" />
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<div class="form-row agent-row-arguments">
|
|
694
|
+
<label
|
|
695
|
+
><i class="fa fa-tags"></i> Inputs
|
|
696
|
+
<i
|
|
697
|
+
class="fa fa-info-circle"
|
|
698
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
699
|
+
title="Named values substituted into Arguments as $INPUTS.<name> before invocation. Pure text templating -- unmatched tokens are left as literal text."
|
|
700
|
+
></i>
|
|
701
|
+
</label>
|
|
702
|
+
<ol id="node-input-inputs-list"></ol>
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
<div class="form-row">
|
|
706
|
+
<label for="node-input-model"><i class="fa fa-microchip"></i> Model</label>
|
|
707
|
+
<input
|
|
708
|
+
type="text"
|
|
709
|
+
id="node-input-model"
|
|
710
|
+
placeholder="leave blank for agent default"
|
|
711
|
+
style="width:60%"
|
|
712
|
+
/>
|
|
713
|
+
<input type="hidden" id="node-input-modelType" />
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<div class="form-row">
|
|
717
|
+
<label for="node-input-cwd"><i class="fa fa-folder-open"></i> Working directory</label>
|
|
718
|
+
<input
|
|
719
|
+
type="text"
|
|
720
|
+
id="node-input-cwd"
|
|
721
|
+
placeholder="leave blank for Node-RED's own working directory"
|
|
722
|
+
style="width:60%"
|
|
723
|
+
/>
|
|
724
|
+
<input type="hidden" id="node-input-cwdType" />
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
<div class="form-row">
|
|
728
|
+
<input
|
|
729
|
+
type="checkbox"
|
|
730
|
+
id="node-input-auto"
|
|
731
|
+
style="display:inline-block; width:auto; vertical-align:top;"
|
|
732
|
+
/>
|
|
733
|
+
<label for="node-input-auto" style="width:auto;">Auto approve (dangerous)</label>
|
|
734
|
+
<i
|
|
735
|
+
id="agent-auto-hint"
|
|
736
|
+
class="fa fa-info-circle"
|
|
737
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
738
|
+
></i>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<div class="form-row">
|
|
742
|
+
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
743
|
+
<input
|
|
744
|
+
type="text"
|
|
745
|
+
id="node-input-timeout"
|
|
746
|
+
placeholder="seconds, leave empty for no timeout"
|
|
747
|
+
style="width:60%"
|
|
748
|
+
/>
|
|
749
|
+
<input type="hidden" id="node-input-timeoutType" />
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
<div class="form-row">
|
|
753
|
+
<label for="node-input-concurrency"><i class="fa fa-tasks"></i> Concurrency</label>
|
|
754
|
+
<input type="text" id="node-input-concurrency" placeholder="1" style="width:80px" />
|
|
755
|
+
<i
|
|
756
|
+
class="fa fa-info-circle"
|
|
757
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
758
|
+
title="Maximum parallel executions for this node. Additional incoming messages queue (FIFO) until a slot frees up. 1 = sequential."
|
|
759
|
+
></i>
|
|
760
|
+
</div>
|
|
761
|
+
|
|
762
|
+
<div class="form-row agent-row-mcp">
|
|
763
|
+
<label><i class="fa fa-plug"></i> MCP Servers</label>
|
|
764
|
+
<ol id="node-input-mcpServers-list"></ol>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="form-tips agent-row-mcp-hint" style="display:none;">
|
|
767
|
+
Pi does not support MCP servers; switch Agent to OpenCode to use them.
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<div class="form-row">
|
|
771
|
+
<label for="node-input-retryMaxAttempts"
|
|
772
|
+
><i class="fa fa-refresh"></i> Retry attempts
|
|
773
|
+
<i
|
|
774
|
+
class="fa fa-info-circle"
|
|
775
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
776
|
+
title="Total attempts for this execution, including the first (2 = one retry). Retries only happen for failed/timeout results the classifier considers retryable -- see On error below."
|
|
777
|
+
></i>
|
|
778
|
+
</label>
|
|
779
|
+
<input
|
|
780
|
+
type="text"
|
|
781
|
+
id="node-input-retryMaxAttempts"
|
|
782
|
+
placeholder="2"
|
|
783
|
+
style="width:80px"
|
|
784
|
+
/>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
<div class="form-row">
|
|
788
|
+
<label for="node-input-retryDelayMs"><i class="fa fa-clock-o"></i> Retry delay (ms)</label>
|
|
789
|
+
<input type="text" id="node-input-retryDelayMs" placeholder="3000" style="width:80px" />
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
<div class="form-row">
|
|
793
|
+
<label for="node-input-retryOnError"><i class="fa fa-exclamation-triangle"></i> On error</label>
|
|
794
|
+
<select id="node-input-retryOnError" style="width:60%">
|
|
795
|
+
<option value="transient">Retry transient errors only (default)</option>
|
|
796
|
+
<option value="all">Retry any non-fatal error</option>
|
|
797
|
+
</select>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<div class="form-row">
|
|
801
|
+
<label for="node-input-systemPrompt"
|
|
802
|
+
><i class="fa fa-file-text-o"></i> System prompt
|
|
803
|
+
<i
|
|
804
|
+
class="fa fa-info-circle"
|
|
805
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
806
|
+
title="Not currently supported by either adapter's CLI -- always ignored with a warning. Kept as a config field for forward-compatibility."
|
|
807
|
+
></i>
|
|
808
|
+
</label>
|
|
809
|
+
<input
|
|
810
|
+
type="text"
|
|
811
|
+
id="node-input-systemPrompt"
|
|
812
|
+
placeholder="not currently supported by any adapter"
|
|
813
|
+
style="width:60%"
|
|
814
|
+
disabled
|
|
815
|
+
/>
|
|
816
|
+
<input type="hidden" id="node-input-systemPromptType" />
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
<div class="form-row agent-row-effort">
|
|
820
|
+
<label for="node-input-effort"
|
|
821
|
+
><i class="fa fa-tachometer"></i> Effort
|
|
822
|
+
<i
|
|
823
|
+
class="fa fa-info-circle"
|
|
824
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
825
|
+
title="Reasoning depth, e.g. low/medium/high/max -- maps to OpenCode's --variant. Not validated against a fixed enum; whatever string is entered is passed through."
|
|
826
|
+
></i>
|
|
827
|
+
</label>
|
|
828
|
+
<input
|
|
829
|
+
type="text"
|
|
830
|
+
id="node-input-effort"
|
|
831
|
+
placeholder="e.g. low, medium, high"
|
|
832
|
+
style="width:60%"
|
|
833
|
+
/>
|
|
834
|
+
<input type="hidden" id="node-input-effortType" />
|
|
835
|
+
</div>
|
|
836
|
+
<div class="form-tips agent-row-effort-hint" style="display:none;">
|
|
837
|
+
Pi has no verified effort/reasoning-depth flag; switch Agent to OpenCode to use it.
|
|
838
|
+
</div>
|
|
839
|
+
|
|
840
|
+
<div class="form-row">
|
|
841
|
+
<label
|
|
842
|
+
><i class="fa fa-check-square-o"></i> Allowed tools
|
|
843
|
+
<i
|
|
844
|
+
class="fa fa-info-circle"
|
|
845
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
846
|
+
title="Restricts which tools the agent may use. OpenCode: materialized as a temp agent config (tools map) selected via --agent. Pi: passed as --tools (overrides the auto-derived default)."
|
|
847
|
+
></i>
|
|
848
|
+
</label>
|
|
849
|
+
<ol id="node-input-allowedTools-list"></ol>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="form-row">
|
|
852
|
+
<label
|
|
853
|
+
><i class="fa fa-ban"></i> Denied tools
|
|
854
|
+
<i
|
|
855
|
+
class="fa fa-info-circle"
|
|
856
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
857
|
+
title="OpenCode only: tools explicitly set to false in the materialized agent config. Pi's --tools flag is allow-list-only, so this list is not used for Pi."
|
|
858
|
+
></i>
|
|
859
|
+
</label>
|
|
860
|
+
<ol id="node-input-deniedTools-list"></ol>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
<div class="form-row">
|
|
864
|
+
<label
|
|
865
|
+
><i class="fa fa-code"></i> Output format (JSON Schema)
|
|
866
|
+
<i
|
|
867
|
+
class="fa fa-info-circle"
|
|
868
|
+
style="color:#888; cursor:help; margin-left:4px;"
|
|
869
|
+
title="Optional. If set, the prompt is augmented to ask for JSON matching this schema, the response is validated with AJV, and (both adapters today) up to 3 reask turns are attempted on failure. Leave blank to disable (default)."
|
|
870
|
+
></i>
|
|
871
|
+
</label>
|
|
872
|
+
<textarea
|
|
873
|
+
id="node-input-outputFormat"
|
|
874
|
+
rows="4"
|
|
875
|
+
style="width:100%; font-family:monospace;"
|
|
876
|
+
placeholder='e.g. {"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}'
|
|
877
|
+
></textarea>
|
|
878
|
+
</div>
|
|
521
879
|
</script>
|
|
522
880
|
|
|
523
881
|
<script type="text/html" data-help-name="agent">
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
<
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
<
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
882
|
+
<p>
|
|
883
|
+
Runs a coding agent (<b>OpenCode</b> or <b>Pi</b>) non-interactively, either directly on this
|
|
884
|
+
host or sandboxed through <code>srt</code>.
|
|
885
|
+
</p>
|
|
886
|
+
<h3>Inputs</h3>
|
|
887
|
+
<dl class="message-properties">
|
|
888
|
+
<dt class="optional">payload <span class="property-type">string</span></dt>
|
|
889
|
+
<dd>
|
|
890
|
+
Used as the prompt/arguments if the corresponding field is left at its default
|
|
891
|
+
(<code>msg.payload</code>).
|
|
892
|
+
</dd>
|
|
893
|
+
<dt class="optional">cwd <span class="property-type">string</span></dt>
|
|
894
|
+
<dd>
|
|
895
|
+
Used as the working directory if the Working directory field is left at its default
|
|
896
|
+
(<code>msg.cwd</code>).
|
|
897
|
+
</dd>
|
|
898
|
+
<dt class="optional">sessionID <span class="property-type">string</span></dt>
|
|
899
|
+
<dd>
|
|
900
|
+
Used to resume a previous session if the Session ID field is left at its default
|
|
901
|
+
(<code>msg.sessionID</code>). Empty/absent starts a new session. OpenCode only.
|
|
902
|
+
</dd>
|
|
903
|
+
<dt class="optional">operation <span class="property-type">string</span></dt>
|
|
904
|
+
<dd>
|
|
905
|
+
Set to <code>"terminate"</code> to kill an in-flight (or still- queued) execution of this node
|
|
906
|
+
instance instead of starting a new one -- see <code>executionId</code> below.
|
|
907
|
+
</dd>
|
|
908
|
+
<dt class="optional">executionId <span class="property-type">string</span></dt>
|
|
909
|
+
<dd>
|
|
910
|
+
Required when <code>operation</code> is <code>"terminate"</code>: the id of the execution to
|
|
911
|
+
kill, as returned by an earlier run in <code>msg.agentExecution.id</code> or an Events
|
|
912
|
+
message's <code>executionId</code>.
|
|
913
|
+
</dd>
|
|
914
|
+
<dt class="optional">concurrency <span class="property-type">number</span></dt>
|
|
915
|
+
<dd>
|
|
916
|
+
Overrides the Concurrency field at runtime, applied before this message is submitted -- no
|
|
917
|
+
redeploy needed. Raising it starts any already-queued messages immediately; lowering it only
|
|
918
|
+
affects future scheduling (already-running executions are never cancelled).
|
|
919
|
+
Non-numeric/non-positive values are ignored.
|
|
920
|
+
</dd>
|
|
921
|
+
</dl>
|
|
922
|
+
<h3>Outputs</h3>
|
|
923
|
+
<ol class="node-ports">
|
|
924
|
+
<li>
|
|
925
|
+
Result
|
|
926
|
+
<dl class="message-properties">
|
|
927
|
+
<dt>payload <span class="property-type">string</span></dt>
|
|
928
|
+
<dd>The agent's final text reply.</dd>
|
|
929
|
+
<dt>sessionID <span class="property-type">string</span></dt>
|
|
930
|
+
<dd>
|
|
931
|
+
The session this execution ran in (new or resumed). Feed this back into the Session ID
|
|
932
|
+
field to continue the conversation on the next message.
|
|
933
|
+
</dd>
|
|
934
|
+
<dt>agentExecution <span class="property-type">object</span></dt>
|
|
935
|
+
<dd>id, status, exitCode, signal, timedOut, durationMs, sessionID.</dd>
|
|
936
|
+
<dt>agentId <span class="property-type">string</span></dt>
|
|
937
|
+
<dd>
|
|
938
|
+
This node instance's own Node-RED id, so a flow with several agent nodes can tell which
|
|
939
|
+
one produced a given message.
|
|
940
|
+
</dd>
|
|
941
|
+
<dt>agentName <span class="property-type">string</span></dt>
|
|
942
|
+
<dd>
|
|
943
|
+
The resolved Agent name field, falling back to this node instance's configured Name if the
|
|
944
|
+
Agent name field is blank/unresolved for this message (e.g. via
|
|
945
|
+
<code>msg.agentName</code> when configured as a msg property).
|
|
946
|
+
</dd>
|
|
947
|
+
</dl>
|
|
948
|
+
</li>
|
|
949
|
+
<li>
|
|
950
|
+
Events
|
|
951
|
+
<dl class="message-properties">
|
|
952
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
953
|
+
<dd>
|
|
954
|
+
One structured event per message as the agent runs
|
|
955
|
+
(queued/started/tool/agent/progress/completed/failed/timeout/cancelled).
|
|
956
|
+
</dd>
|
|
957
|
+
<dt>executionId <span class="property-type">string</span></dt>
|
|
958
|
+
<dd>
|
|
959
|
+
Correlates events (and the eventual result) belonging to the same execution when several
|
|
960
|
+
run concurrently.
|
|
961
|
+
</dd>
|
|
962
|
+
<dt>agentId <span class="property-type">string</span></dt>
|
|
963
|
+
<dd>This node instance's own Node-RED id (see Result output).</dd>
|
|
964
|
+
<dt>agentName <span class="property-type">string</span></dt>
|
|
965
|
+
<dd>The resolved Agent name for this execution (see Result output).</dd>
|
|
966
|
+
</dl>
|
|
967
|
+
</li>
|
|
968
|
+
</ol>
|
|
969
|
+
<p>
|
|
970
|
+
<b>Concurrency</b> bounds how many executions this node runs at once (default 1 = sequential).
|
|
971
|
+
Extra incoming messages wait in an internal FIFO queue -- each waiting message emits a
|
|
972
|
+
<code>queued</code> event immediately, then <code>started</code> once a slot frees up. The node
|
|
973
|
+
status shows e.g. <code>2 running · 3 queued</code>. Results may complete in a different
|
|
974
|
+
order than the messages arrived; ordering is not guaranteed above concurrency 1. On
|
|
975
|
+
redeploy/removal, queued messages are cancelled (a <code>cancelled</code> event plus a Node-RED
|
|
976
|
+
error) and any still-running agent processes are terminated. Send
|
|
977
|
+
<code>msg.concurrency</code> to change the bound at runtime without a redeploy -- see Inputs
|
|
978
|
+
below.
|
|
979
|
+
</p>
|
|
980
|
+
<p>
|
|
981
|
+
<b>Terminating a specific execution on demand</b>: send a message with
|
|
982
|
+
<code>msg.operation = "terminate"</code> and <code>msg.executionId</code> set to the id to kill
|
|
983
|
+
(from a prior run's <code>msg.agentExecution.id</code> or an Events message's
|
|
984
|
+
<code>executionId</code>). A still-queued execution is cancelled immediately (never spawned); an
|
|
985
|
+
active one gets SIGTERM to its whole process group, escalating to SIGKILL after a grace period
|
|
986
|
+
-- this works the same way for both the Direct and SRT runtimes. The terminated execution's own
|
|
987
|
+
original trigger still completes on its own (status <code>failed</code>, with the kill signal)
|
|
988
|
+
once the process actually exits; this terminate trigger's own output is only the immediate "kill
|
|
989
|
+
requested" acknowledgement.
|
|
990
|
+
</p>
|
|
991
|
+
<p>
|
|
992
|
+
<b>Auto approve</b> maps to OpenCode's <code>--auto</code> (off by default -- without it, any
|
|
993
|
+
action gated by a permission policy not already set to "allow" is auto-rejected, but the run
|
|
994
|
+
still completes). Pi has no equivalent permission-prompt system to bypass in non-interactive
|
|
995
|
+
mode -- for Pi this instead restricts the available tools to read-only
|
|
996
|
+
(<code>read/grep/find/ls</code>) when off, and allows everything (including
|
|
997
|
+
<code>bash/edit/write</code>) when on.
|
|
998
|
+
</p>
|
|
999
|
+
<p>
|
|
1000
|
+
<b>Skill</b>/<b>Command</b> invocation: for OpenCode this is the name of a discovered skill or a
|
|
1001
|
+
<code>.opencode/command(s)/<name>.md</code> file, dispatched deterministically via
|
|
1002
|
+
<code>--command</code>. Pi has no equivalent dispatch mechanism -- the name is resolved to a
|
|
1003
|
+
file under <code>.github/skills/</code> (skill) or <code>.github/prompts/</code> (command),
|
|
1004
|
+
passed via <code>--skill</code>/<code>--prompt-template</code>, and the model is explicitly told
|
|
1005
|
+
in the prompt to use it. Pi also does not support configuring MCP servers through this node.
|
|
1006
|
+
</p>
|
|
1007
|
+
<p>
|
|
1008
|
+
<b>Session ID</b>: leave blank to always start a fresh session (the default). Set it (typically
|
|
1009
|
+
from the previous result's <code>msg.sessionID</code>) to continue that conversation instead --
|
|
1010
|
+
maps to OpenCode's <code>opencode run --session <id> ...</code>. Only the OpenCode agent
|
|
1011
|
+
supports this; Pi rejects a non-empty Session ID (every Pi run uses <code>--no-session</code>).
|
|
1012
|
+
</p>
|
|
1013
|
+
<p>
|
|
1014
|
+
<b>SRT</b> runtime shells out to the <code>srt</code> sandbox-runtime CLI already installed on
|
|
1015
|
+
this host; its network/filesystem policy is configured via its own settings file (default
|
|
1016
|
+
<code>~/.srt-settings.json</code>), not through this node.
|
|
1017
|
+
</p>
|
|
1018
|
+
<p>
|
|
1019
|
+
<b>System prompt</b>, <b>Effort</b>, and <b>Allowed/Denied tools</b> are forwarded to the
|
|
1020
|
+
underlying CLI only where that adapter actually supports it -- a field set for an adapter that
|
|
1021
|
+
doesn't will be silently dropped with a Node-RED warning, never a hard error.
|
|
1022
|
+
<b>System prompt</b> has no verified CLI flag for either adapter yet and is always dropped.
|
|
1023
|
+
<b>Effort</b> (reasoning depth, e.g. <code>low</code>/<code>medium</code>/<code>high</code>)
|
|
1024
|
+
maps to OpenCode's <code>--variant</code>; Pi has no equivalent.
|
|
1025
|
+
<b>Allowed/ Denied tools</b> are supported by both: for OpenCode they're materialized into a
|
|
1026
|
+
temporary, per-run agent config (delivered the same way as MCP servers, via
|
|
1027
|
+
<code>OPENCODE_CONFIG_CONTENT</code>) selected with <code>--agent</code>; for Pi, a non-empty
|
|
1028
|
+
Allowed tools list is passed as <code>--tools</code> (Pi has no deny-list mechanism, so Denied
|
|
1029
|
+
tools has no effect for Pi).
|
|
1030
|
+
</p>
|
|
641
1031
|
</script>
|