@tbrandenburg/node-red-agents 0.1.2 → 0.1.4
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.js +401 -368
- package/nodes/agent/lib/agents/base.js +31 -31
- package/nodes/agent/lib/agents/opencode.js +127 -120
- package/nodes/agent/lib/agents/pi.js +195 -175
- package/nodes/agent/lib/execution/lifecycle.js +41 -41
- package/nodes/agent/lib/execution/scheduler.js +74 -73
- package/nodes/agent/lib/execution/status.js +10 -10
- package/nodes/agent/lib/mcp/normalize.js +16 -16
- package/nodes/agent/lib/runtimes/base.js +10 -10
- package/nodes/agent/lib/runtimes/direct.js +19 -19
- package/nodes/agent/lib/runtimes/process-exec.js +71 -71
- package/nodes/agent/lib/runtimes/srt.js +40 -40
- package/nodes/agent-server/agent-server.js +495 -458
- package/nodes/agent-server/lib/daemon.js +99 -84
- package/nodes/agent-server/lib/http.js +45 -43
- package/nodes/agent-server/lib/model.js +10 -8
- package/nodes/agent-server/lib/port.js +14 -14
- package/nodes/agent-server/lib/registry.js +54 -54
- package/nodes/agent-server/lib/status.js +6 -6
- package/nodes/gh/README.md +11 -11
- package/nodes/gh/examples/list-pull-requests.json +46 -46
- package/nodes/gh/examples/run-workflow.json +40 -40
- package/nodes/gh/gh.js +220 -211
- package/nodes/gh/lib/parse-args.js +43 -43
- package/package.json +1 -1
- package/shared/srt-settings.js +32 -29
|
@@ -1,481 +1,518 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const { findFreePort } = require(
|
|
5
|
-
const { InstanceRegistry } = require(
|
|
6
|
-
const { spawnDaemon, waitForHealthy, killDaemon } = require(
|
|
7
|
-
const { computeNodeStatus } = require(
|
|
8
|
-
const { writeInlineSettingsFile } = require(
|
|
9
|
-
const { request } = require(
|
|
10
|
-
const { parseModel } = require(
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { findFreePort } = require("./lib/port");
|
|
5
|
+
const { InstanceRegistry } = require("./lib/registry");
|
|
6
|
+
const { spawnDaemon, waitForHealthy, killDaemon } = require("./lib/daemon");
|
|
7
|
+
const { computeNodeStatus } = require("./lib/status");
|
|
8
|
+
const { writeInlineSettingsFile } = require("../../shared/srt-settings");
|
|
9
|
+
const { request } = require("./lib/http");
|
|
10
|
+
const { parseModel } = require("./lib/model");
|
|
11
11
|
|
|
12
12
|
module.exports = function (RED) {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
function AgentServerNode(config) {
|
|
16
|
+
RED.nodes.createNode(this, config);
|
|
17
|
+
const node = this;
|
|
18
|
+
|
|
19
|
+
node.hostname = config.hostname || "127.0.0.1";
|
|
20
|
+
node.opencodeBinary = config.opencodeBinary || "";
|
|
21
|
+
|
|
22
|
+
node.operation = config.operation || "message";
|
|
23
|
+
|
|
24
|
+
node.sessionIdProp = config.sessionIdProp !== undefined ? config.sessionIdProp : "sessionID";
|
|
25
|
+
node.sessionIdPropType = config.sessionIdPropType || "msg";
|
|
26
|
+
|
|
27
|
+
node.promptProp = config.promptProp !== undefined ? config.promptProp : "payload";
|
|
28
|
+
node.promptPropType = config.promptPropType || "msg";
|
|
29
|
+
|
|
30
|
+
node.model = config.model || "";
|
|
31
|
+
node.modelType = config.modelType || "str";
|
|
32
|
+
|
|
33
|
+
node.startupTimeoutMs = Number(config.startupTimeoutMs) || 15000;
|
|
34
|
+
node.requestTimeoutMs = Number(config.requestTimeoutMs) || 120000;
|
|
35
|
+
|
|
36
|
+
const maxInstancesNum = parseInt(config.maxInstances, 10);
|
|
37
|
+
// 0 (or invalid/blank) means unlimited -- not everyone needs a cap,
|
|
38
|
+
// and 0 reads more naturally as "no limit" than as "allow zero".
|
|
39
|
+
node.maxInstances =
|
|
40
|
+
Number.isFinite(maxInstancesNum) && maxInstancesNum > 0 ? maxInstancesNum : 0;
|
|
41
|
+
|
|
42
|
+
node.authUsername = config.authUsername || "";
|
|
43
|
+
node.authPassword = config.authPassword || "";
|
|
44
|
+
|
|
45
|
+
node.runtime = config.runtime || "direct";
|
|
46
|
+
node.srtBinary = config.srtBinary || "";
|
|
47
|
+
node.srtSettingsMode = config.srtSettingsMode || "file";
|
|
48
|
+
node.srtSettingsPath = config.srtSettingsPath || "";
|
|
49
|
+
node.srtAllowedDomains = Array.isArray(config.srtAllowedDomains)
|
|
50
|
+
? config.srtAllowedDomains
|
|
51
|
+
: [];
|
|
52
|
+
node.srtAllowedWriteDirs = Array.isArray(config.srtAllowedWriteDirs)
|
|
53
|
+
? config.srtAllowedWriteDirs
|
|
54
|
+
: [];
|
|
55
|
+
node.srtStrictAllowlist = config.srtStrictAllowlist !== false;
|
|
56
|
+
node.srtAdvancedJson = config.srtAdvancedJson || "";
|
|
57
|
+
|
|
58
|
+
// Resolved once at construction time, same pattern as the `agent`
|
|
59
|
+
// node -- these settings don't change without a redeploy.
|
|
60
|
+
node.resolvedSrtSettingsPath = undefined;
|
|
61
|
+
node.srtTempSettingsFile = undefined;
|
|
62
|
+
node.srtSettingsError = undefined;
|
|
63
|
+
|
|
64
|
+
if (node.runtime === "srt") {
|
|
65
|
+
if (node.srtSettingsMode === "inline") {
|
|
66
|
+
try {
|
|
67
|
+
node.resolvedSrtSettingsPath = writeInlineSettingsFile(
|
|
68
|
+
node.id,
|
|
69
|
+
{
|
|
70
|
+
allowedDomains: node.srtAllowedDomains,
|
|
71
|
+
allowedWriteDirs: node.srtAllowedWriteDirs,
|
|
72
|
+
strictAllowlist: node.srtStrictAllowlist,
|
|
73
|
+
advancedJson: node.srtAdvancedJson,
|
|
74
|
+
},
|
|
75
|
+
"agent-server-srt-settings",
|
|
76
|
+
);
|
|
77
|
+
node.srtTempSettingsFile = node.resolvedSrtSettingsPath;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
node.srtSettingsError = `invalid inline SRT settings JSON: ${err.message}`;
|
|
80
|
+
node.error(`agent-server: ${node.srtSettingsError}`);
|
|
81
|
+
node.status({ fill: "red", shape: "ring", text: "bad srt settings" });
|
|
77
82
|
}
|
|
83
|
+
} else {
|
|
84
|
+
node.resolvedSrtSettingsPath = node.srtSettingsPath || undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
node.registry = new InstanceRegistry();
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
function updateStatus() {
|
|
91
|
+
node.status(computeNodeStatus(node.registry.summary()));
|
|
92
|
+
}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
// Envelope shared by every message on output 2 -- same shape/intent
|
|
95
|
+
// as the `agent` node's lifecycle envelope (topic correlation + live
|
|
96
|
+
// counts + timestamp), just counting daemons/sessions instead of
|
|
97
|
+
// executions. This is what a downstream ui-table/context-aggregator
|
|
98
|
+
// uses to build a "what has been running" history.
|
|
99
|
+
function emitEvent(sessionID, type, msg) {
|
|
100
|
+
const summary = node.registry.summary();
|
|
101
|
+
node.send([
|
|
102
|
+
null,
|
|
103
|
+
{
|
|
104
|
+
_msgid: msg && msg._msgid,
|
|
105
|
+
topic: msg && msg.topic,
|
|
106
|
+
payload: { type },
|
|
107
|
+
sessionID,
|
|
108
|
+
active: summary.busy,
|
|
109
|
+
tracked: summary.total,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
function resolveTyped(prop, type, msg, fallback) {
|
|
116
|
+
if (prop === "") return fallback;
|
|
117
|
+
try {
|
|
118
|
+
const value = RED.util.evaluateNodeProperty(prop, type, node, msg);
|
|
119
|
+
return value === undefined || value === null || value === "" ? fallback : value;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new Error(`invalid ${type} property "${prop}": ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
function authOptions() {
|
|
126
|
+
return node.authUsername || node.authPassword
|
|
127
|
+
? { username: node.authUsername, password: node.authPassword }
|
|
128
|
+
: {};
|
|
129
|
+
}
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// Extracts the assistant's text reply the same way the `agent`
|
|
132
|
+
// node's OpenCodeAdapter does: join every text-type part.
|
|
133
|
+
function extractText(messageResponse) {
|
|
134
|
+
const parts = (messageResponse && messageResponse.parts) || [];
|
|
135
|
+
return parts
|
|
136
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
137
|
+
.map((p) => p.text)
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
132
141
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
142
|
+
// Spawns a brand-new daemon + session, registers it, and resolves
|
|
143
|
+
// with { sessionID, baseUrl }. Cleans up (kills the half-started
|
|
144
|
+
// process) and rejects on any failure along the way -- callers
|
|
145
|
+
// never have to clean up a partially-started daemon themselves.
|
|
146
|
+
async function spawnNewInstance(msg) {
|
|
147
|
+
if (node.maxInstances > 0 && node.registry.size() >= node.maxInstances) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`agent-server: max instances (${node.maxInstances}) reached, cannot spawn a new daemon`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const port = await findFreePort(node.hostname);
|
|
154
|
+
const baseUrl = `http://${node.hostname}:${port}`;
|
|
155
|
+
const auth = authOptions();
|
|
156
|
+
|
|
157
|
+
const env = Object.assign({}, process.env);
|
|
158
|
+
if (node.authUsername || node.authPassword) {
|
|
159
|
+
env.OPENCODE_SERVER_PASSWORD = node.authPassword;
|
|
160
|
+
if (node.authUsername) env.OPENCODE_SERVER_USERNAME = node.authUsername;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { child, diagnostics } = spawnDaemon({
|
|
164
|
+
binary: node.opencodeBinary || undefined,
|
|
165
|
+
hostname: node.hostname,
|
|
166
|
+
port,
|
|
167
|
+
env,
|
|
168
|
+
srt: {
|
|
169
|
+
enabled: node.runtime === "srt",
|
|
170
|
+
binary: node.srtBinary || undefined,
|
|
171
|
+
settingsPath: node.resolvedSrtSettingsPath,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await waitForHealthy(baseUrl, {
|
|
177
|
+
timeoutMs: node.startupTimeoutMs,
|
|
178
|
+
diagnostics,
|
|
179
|
+
...auth,
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
await killDaemon(child);
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let session;
|
|
187
|
+
try {
|
|
188
|
+
session = await request(`${baseUrl}/session`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
body: { title: (msg && msg.topic) || "agent-server" },
|
|
191
|
+
timeoutMs: node.requestTimeoutMs,
|
|
192
|
+
...auth,
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
await killDaemon(child);
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sessionID = session.id;
|
|
200
|
+
node.registry.register(sessionID, { child, host: node.hostname, port, baseUrl });
|
|
201
|
+
emitEvent(sessionID, "spawned", msg);
|
|
202
|
+
updateStatus();
|
|
203
|
+
return { sessionID, baseUrl };
|
|
204
|
+
}
|
|
194
205
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
node.registry.setBusy(sessionID, false);
|
|
223
|
-
const record = node.registry.get(sessionID);
|
|
224
|
-
const durationMs = Number((process.hrtime.bigint() - startedAtNs) / 1000000n);
|
|
225
|
-
const resultMsg = Object.assign({}, msg, {
|
|
226
|
-
payload: extractText(response),
|
|
227
|
-
sessionID,
|
|
228
|
-
agentServer: {
|
|
229
|
-
sessionID,
|
|
230
|
-
host: record && record.host,
|
|
231
|
-
port: record && record.port,
|
|
232
|
-
durationMs
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
send([resultMsg, null]);
|
|
236
|
-
emitEvent(sessionID, 'completed', msg);
|
|
237
|
-
updateStatus();
|
|
238
|
-
done();
|
|
239
|
-
} catch (err) {
|
|
240
|
-
node.registry.setBusy(sessionID, false);
|
|
241
|
-
emitEvent(sessionID, 'failed', msg);
|
|
242
|
-
updateStatus();
|
|
243
|
-
done(new Error(`agent-server: message to session "${sessionID}" failed: ${err.message}`));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
206
|
+
// Shared by both the "spawn new" and "reuse existing" paths.
|
|
207
|
+
async function sendMessage(sessionID, baseUrl, prompt, model, msg, send, done) {
|
|
208
|
+
node.registry.setBusy(sessionID, true);
|
|
209
|
+
emitEvent(sessionID, "running", msg);
|
|
210
|
+
updateStatus();
|
|
211
|
+
|
|
212
|
+
const auth = authOptions();
|
|
213
|
+
// process.hrtime.bigint() rather than Date.now() for the
|
|
214
|
+
// duration measurement specifically: it's monotonic, so it
|
|
215
|
+
// can't ever go negative from a wall-clock adjustment mid-call
|
|
216
|
+
// (observed once in this sandbox's WSL2 VM). timestamp fields
|
|
217
|
+
// elsewhere deliberately stay Date.now() -- those are for
|
|
218
|
+
// human-readable/correlatable wall-clock history, not duration.
|
|
219
|
+
const startedAtNs = process.hrtime.bigint();
|
|
220
|
+
try {
|
|
221
|
+
const body = {
|
|
222
|
+
agent: msg.agent || "build",
|
|
223
|
+
parts: [{ type: "text", text: String(prompt) }],
|
|
224
|
+
};
|
|
225
|
+
if (model) body.model = model;
|
|
226
|
+
const response = await request(`${baseUrl}/session/${sessionID}/message`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
body,
|
|
229
|
+
timeoutMs: node.requestTimeoutMs,
|
|
230
|
+
...auth,
|
|
231
|
+
});
|
|
246
232
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
} catch (err) {
|
|
272
|
-
done(new Error(`agent-server: ${err.message}`));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (sessionID) {
|
|
277
|
-
// Foreign sessionID -- not tracked by this node instance --
|
|
278
|
-
// is always an error, never a fallback spawn/resume.
|
|
279
|
-
if (!node.registry.has(sessionID)) {
|
|
280
|
-
done(new Error(`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const record = node.registry.get(sessionID);
|
|
284
|
-
if (record.busy) {
|
|
285
|
-
done(new Error(`agent-server: session "${sessionID}" is already processing a message`));
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
sendMessage(sessionID, record.baseUrl, prompt, model, msg, send, done);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
spawnNewInstance(msg)
|
|
293
|
-
.then(({ sessionID: newSessionID, baseUrl }) => sendMessage(newSessionID, baseUrl, prompt, model, msg, send, done))
|
|
294
|
-
.catch((err) => {
|
|
295
|
-
updateStatus();
|
|
296
|
-
done(err);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
233
|
+
node.registry.setBusy(sessionID, false);
|
|
234
|
+
const record = node.registry.get(sessionID);
|
|
235
|
+
const durationMs = Number((process.hrtime.bigint() - startedAtNs) / 1000000n);
|
|
236
|
+
const resultMsg = Object.assign({}, msg, {
|
|
237
|
+
payload: extractText(response),
|
|
238
|
+
sessionID,
|
|
239
|
+
agentServer: {
|
|
240
|
+
sessionID,
|
|
241
|
+
host: record && record.host,
|
|
242
|
+
port: record && record.port,
|
|
243
|
+
durationMs,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
send([resultMsg, null]);
|
|
247
|
+
emitEvent(sessionID, "completed", msg);
|
|
248
|
+
updateStatus();
|
|
249
|
+
done();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
node.registry.setBusy(sessionID, false);
|
|
252
|
+
emitEvent(sessionID, "failed", msg);
|
|
253
|
+
updateStatus();
|
|
254
|
+
done(new Error(`agent-server: message to session "${sessionID}" failed: ${err.message}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
299
257
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
258
|
+
function handleMessageOperation(msg, send, done) {
|
|
259
|
+
let sessionID;
|
|
260
|
+
try {
|
|
261
|
+
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
done(err);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let prompt;
|
|
268
|
+
try {
|
|
269
|
+
prompt = resolveTyped(node.promptProp, node.promptPropType, msg, undefined);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
done(err);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (prompt === undefined || prompt === null || String(prompt).trim() === "") {
|
|
275
|
+
done(
|
|
276
|
+
new Error(
|
|
277
|
+
"agent-server: no prompt (msg.payload or the configured Prompt field is empty)",
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let model;
|
|
284
|
+
try {
|
|
285
|
+
model = parseModel(resolveTyped(node.model, node.modelType, msg, ""));
|
|
286
|
+
} catch (err) {
|
|
287
|
+
done(new Error(`agent-server: ${err.message}`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (sessionID) {
|
|
292
|
+
// Foreign sessionID -- not tracked by this node instance --
|
|
293
|
+
// is always an error, never a fallback spawn/resume.
|
|
294
|
+
if (!node.registry.has(sessionID)) {
|
|
295
|
+
done(
|
|
296
|
+
new Error(
|
|
297
|
+
`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
return;
|
|
337
301
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
done(err);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (!sessionID || !node.registry.has(sessionID)) {
|
|
348
|
-
done(new Error(`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`));
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const record = node.registry.get(sessionID);
|
|
353
|
-
request(`${record.baseUrl}/session/${sessionID}/abort`, {
|
|
354
|
-
method: 'POST',
|
|
355
|
-
timeoutMs: node.requestTimeoutMs,
|
|
356
|
-
...authOptions()
|
|
357
|
-
})
|
|
358
|
-
.then(() => {
|
|
359
|
-
node.registry.setBusy(sessionID, false);
|
|
360
|
-
updateStatus();
|
|
361
|
-
send([Object.assign({}, msg, { payload: true, sessionID }), null]);
|
|
362
|
-
done();
|
|
363
|
-
})
|
|
364
|
-
.catch((err) => done(new Error(`agent-server: abort failed: ${err.message}`)));
|
|
302
|
+
const record = node.registry.get(sessionID);
|
|
303
|
+
if (record.busy) {
|
|
304
|
+
done(new Error(`agent-server: session "${sessionID}" is already processing a message`));
|
|
305
|
+
return;
|
|
365
306
|
}
|
|
307
|
+
sendMessage(sessionID, record.baseUrl, prompt, model, msg, send, done);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
spawnNewInstance(msg)
|
|
312
|
+
.then(({ sessionID: newSessionID, baseUrl }) =>
|
|
313
|
+
sendMessage(newSessionID, baseUrl, prompt, model, msg, send, done),
|
|
314
|
+
)
|
|
315
|
+
.catch((err) => {
|
|
316
|
+
updateStatus();
|
|
317
|
+
done(err);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
366
320
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
321
|
+
function handleStatusOperation(msg, send, done) {
|
|
322
|
+
let sessionID;
|
|
323
|
+
try {
|
|
324
|
+
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
done(err);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!sessionID) {
|
|
331
|
+
// Aggregate across every daemon this node instance is
|
|
332
|
+
// tracking -- purely local, no network calls.
|
|
333
|
+
send([Object.assign({}, msg, { payload: node.registry.summary() }), null]);
|
|
334
|
+
done();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!node.registry.has(sessionID)) {
|
|
339
|
+
done(
|
|
340
|
+
new Error(
|
|
341
|
+
`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`,
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const record = node.registry.get(sessionID);
|
|
348
|
+
send([
|
|
349
|
+
Object.assign({}, msg, {
|
|
350
|
+
payload: {
|
|
351
|
+
sessionID,
|
|
352
|
+
busy: record.busy,
|
|
353
|
+
host: record.host,
|
|
354
|
+
port: record.port,
|
|
355
|
+
startedAt: record.startedAt,
|
|
356
|
+
lastUsed: record.lastUsed,
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
null,
|
|
360
|
+
]);
|
|
361
|
+
done();
|
|
362
|
+
}
|
|
391
363
|
|
|
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
|
-
}
|
|
364
|
+
function handleAbortOperation(msg, send, done) {
|
|
365
|
+
let sessionID;
|
|
366
|
+
try {
|
|
367
|
+
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
done(err);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!sessionID || !node.registry.has(sessionID)) {
|
|
373
|
+
done(
|
|
374
|
+
new Error(
|
|
375
|
+
`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`,
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const record = node.registry.get(sessionID);
|
|
382
|
+
request(`${record.baseUrl}/session/${sessionID}/abort`, {
|
|
383
|
+
method: "POST",
|
|
384
|
+
timeoutMs: node.requestTimeoutMs,
|
|
385
|
+
...authOptions(),
|
|
386
|
+
})
|
|
387
|
+
.then(() => {
|
|
388
|
+
node.registry.setBusy(sessionID, false);
|
|
389
|
+
updateStatus();
|
|
390
|
+
send([Object.assign({}, msg, { payload: true, sessionID }), null]);
|
|
391
|
+
done();
|
|
392
|
+
})
|
|
393
|
+
.catch((err) => done(new Error(`agent-server: abort failed: ${err.message}`)));
|
|
394
|
+
}
|
|
421
395
|
|
|
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
|
-
case 'terminate':
|
|
451
|
-
handleTerminateOperation(msg, send, done);
|
|
452
|
-
return;
|
|
453
|
-
case 'message':
|
|
454
|
-
default:
|
|
455
|
-
handleMessageOperation(msg, send, done);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
});
|
|
396
|
+
function handleHistoryOperation(msg, send, done) {
|
|
397
|
+
let sessionID;
|
|
398
|
+
try {
|
|
399
|
+
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
done(err);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!sessionID || !node.registry.has(sessionID)) {
|
|
405
|
+
done(
|
|
406
|
+
new Error(
|
|
407
|
+
`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`,
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const record = node.registry.get(sessionID);
|
|
414
|
+
request(`${record.baseUrl}/session/${sessionID}/message`, {
|
|
415
|
+
timeoutMs: node.requestTimeoutMs,
|
|
416
|
+
...authOptions(),
|
|
417
|
+
})
|
|
418
|
+
.then((history) => {
|
|
419
|
+
send([Object.assign({}, msg, { payload: history, sessionID }), null]);
|
|
420
|
+
done();
|
|
421
|
+
})
|
|
422
|
+
.catch((err) => done(new Error(`agent-server: history fetch failed: ${err.message}`)));
|
|
423
|
+
}
|
|
459
424
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
})
|
|
477
|
-
|
|
425
|
+
// Kills the daemon process itself, unlike `abort` (which only
|
|
426
|
+
// HTTP-cancels the in-flight message and leaves the daemon running,
|
|
427
|
+
// reusable for a later message). After this, the sessionID is no
|
|
428
|
+
// longer tracked -- a later message with this sessionID is a foreign/
|
|
429
|
+
// unknown id, same as if it had never been spawned.
|
|
430
|
+
function handleTerminateOperation(msg, send, done) {
|
|
431
|
+
let sessionID;
|
|
432
|
+
try {
|
|
433
|
+
sessionID = resolveTyped(node.sessionIdProp, node.sessionIdPropType, msg, undefined);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
done(err);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!sessionID || !node.registry.has(sessionID)) {
|
|
439
|
+
done(
|
|
440
|
+
new Error(
|
|
441
|
+
`agent-server: unknown sessionID "${sessionID}" (not tracked by this node instance)`,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const record = node.registry.get(sessionID);
|
|
448
|
+
node.registry.delete(sessionID);
|
|
449
|
+
killDaemon(record.child)
|
|
450
|
+
.then(() => {
|
|
451
|
+
emitEvent(sessionID, "terminated", msg);
|
|
452
|
+
updateStatus();
|
|
453
|
+
send([Object.assign({}, msg, { payload: true, sessionID }), null]);
|
|
454
|
+
done();
|
|
455
|
+
})
|
|
456
|
+
.catch((err) => done(new Error(`agent-server: terminate failed: ${err.message}`)));
|
|
478
457
|
}
|
|
479
458
|
|
|
480
|
-
|
|
459
|
+
const VALID_OPERATIONS = ["message", "status", "abort", "history", "terminate"];
|
|
460
|
+
|
|
461
|
+
node.on("input", function (msg, send, done) {
|
|
462
|
+
if (node.srtSettingsError) {
|
|
463
|
+
done(new Error(`agent-server: ${node.srtSettingsError}`));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// msg.operation can override the configured default for this one
|
|
468
|
+
// trigger. This matters because each node instance's registry is
|
|
469
|
+
// private (per-node-instance scoping, same as the `agent` node's
|
|
470
|
+
// active/queued counts) -- a *separate* node configured with
|
|
471
|
+
// operation 'status' would only ever see its own (always empty)
|
|
472
|
+
// registry, never another node's spawned daemons. Overriding lets
|
|
473
|
+
// the same node instance that does the spawning also be queried
|
|
474
|
+
// for its own status/history/abort on demand.
|
|
475
|
+
const operation = VALID_OPERATIONS.includes(msg.operation) ? msg.operation : node.operation;
|
|
476
|
+
|
|
477
|
+
switch (operation) {
|
|
478
|
+
case "status":
|
|
479
|
+
handleStatusOperation(msg, send, done);
|
|
480
|
+
return;
|
|
481
|
+
case "abort":
|
|
482
|
+
handleAbortOperation(msg, send, done);
|
|
483
|
+
return;
|
|
484
|
+
case "history":
|
|
485
|
+
handleHistoryOperation(msg, send, done);
|
|
486
|
+
return;
|
|
487
|
+
case "terminate":
|
|
488
|
+
handleTerminateOperation(msg, send, done);
|
|
489
|
+
return;
|
|
490
|
+
case "message":
|
|
491
|
+
default:
|
|
492
|
+
handleMessageOperation(msg, send, done);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
node.on("close", function (done) {
|
|
498
|
+
const entries = Array.from(node.registry.list());
|
|
499
|
+
Promise.all(
|
|
500
|
+
entries.map((sessionID) => {
|
|
501
|
+
const record = node.registry.get(sessionID);
|
|
502
|
+
node.registry.delete(sessionID);
|
|
503
|
+
return killDaemon(record.child).then(() => {
|
|
504
|
+
emitEvent(sessionID, "closed", {});
|
|
505
|
+
});
|
|
506
|
+
}),
|
|
507
|
+
).then(() => {
|
|
508
|
+
if (node.srtTempSettingsFile) {
|
|
509
|
+
fs.unlink(node.srtTempSettingsFile, () => {});
|
|
510
|
+
}
|
|
511
|
+
node.status({});
|
|
512
|
+
done();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
RED.nodes.registerType("agent-server", AgentServerNode);
|
|
481
518
|
};
|