@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,42 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Generic interface every agent harness adapter (OpenCode, and later
|
|
4
4
|
// Pi/Claude Code) must implement. The Agent node core only ever talks to
|
|
5
5
|
// this interface -- it must never know about a specific agent's CLI flags.
|
|
6
6
|
class AgentAdapter {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
// Throws a descriptive Error if `resolved` (the already-typed-input
|
|
8
|
+
// -resolved config, see lib/execution/lifecycle.js) is not runnable.
|
|
9
|
+
// Must not have side effects beyond validation.
|
|
10
|
+
//
|
|
11
|
+
// `resolved.sessionID` (string, may be '' or undefined) is an optional
|
|
12
|
+
// continuation id: '' / undefined means "start a new session" (today's
|
|
13
|
+
// only behavior); a non-empty value asks the adapter to resume that
|
|
14
|
+
// session instead. An adapter that has no way to honor this (no
|
|
15
|
+
// underlying CLI/session concept, or one that's deliberately disabled
|
|
16
|
+
// -- see the Pi adapter) must throw here rather than silently ignoring
|
|
17
|
+
// it or starting a new session anyway.
|
|
18
|
+
validate(_resolved) {}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// Returns { command, args, env } -- a normalized, adapter-specific
|
|
21
|
+
// execution request. `args` must always be a plain array of strings;
|
|
22
|
+
// never a shell command string (see spec: spawn over exec).
|
|
23
|
+
buildExecution(_resolved) {
|
|
24
|
+
throw new Error("AgentAdapter.buildExecution() not implemented");
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
// Parses one line of stdout. Returns a normalized event object
|
|
28
|
+
// { type, sessionID, data } or null if the line should be ignored
|
|
29
|
+
// (blank, or malformed -- must never throw).
|
|
30
|
+
parseEvent(_line) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
// Given all accumulated parsed events plus the process exit info,
|
|
35
|
+
// returns { payload, sessionID, status, errorMessage? }.
|
|
36
|
+
// status is one of "completed" | "failed".
|
|
37
|
+
parseResult(_events, _exitCode, _signal, _stderr) {
|
|
38
|
+
return { payload: "", status: "completed" };
|
|
39
|
+
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
module.exports = { AgentAdapter };
|
|
@@ -1,141 +1,148 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const { AgentAdapter } = require(
|
|
5
|
-
const { toOpenCodeMcp } = require(
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { AgentAdapter } = require("./base");
|
|
5
|
+
const { toOpenCodeMcp } = require("../mcp/normalize");
|
|
6
6
|
|
|
7
7
|
// Maps opencode's real `--format json` event stream types (verified against
|
|
8
8
|
// packages/opencode/src/cli/cmd/run.ts) onto the Agent node's generic event
|
|
9
9
|
// vocabulary (spec section "Event output").
|
|
10
10
|
const TYPE_MAP = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
step_start: "started",
|
|
12
|
+
step_finish: "progress",
|
|
13
|
+
tool_use: "tool",
|
|
14
|
+
text: "agent",
|
|
15
|
+
reasoning: "agent",
|
|
16
|
+
error: "failed",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
class OpenCodeAdapter extends AgentAdapter {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (resolved.invocation === 'prompt') {
|
|
34
|
-
if (!resolved.prompt || !String(resolved.prompt).trim()) {
|
|
35
|
-
throw new Error('prompt invocation requires a non-empty prompt (msg.payload or the Prompt field)');
|
|
36
|
-
}
|
|
37
|
-
} else if (resolved.invocation === 'skill' || resolved.invocation === 'command') {
|
|
38
|
-
if (!resolved.invocationName || !String(resolved.invocationName).trim()) {
|
|
39
|
-
throw new Error(`${resolved.invocation} invocation requires a non-empty name`);
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
throw new Error(`unknown invocation mode: ${resolved.invocation}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const server of resolved.mcpServers || []) {
|
|
46
|
-
if (!server || !server.name) {
|
|
47
|
-
throw new Error('mcpServers entries require a name');
|
|
48
|
-
}
|
|
49
|
-
if (server.type === 'remote' && !server.url) {
|
|
50
|
-
throw new Error(`mcp server "${server.name}" (remote) requires a url`);
|
|
51
|
-
}
|
|
52
|
-
if (server.type === 'local' && !server.command) {
|
|
53
|
-
throw new Error(`mcp server "${server.name}" (local) requires a command`);
|
|
54
|
-
}
|
|
55
|
-
if (server.type !== 'remote' && server.type !== 'local') {
|
|
56
|
-
throw new Error(`mcp server "${server.name}" has unknown type: ${server.type}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
20
|
+
validate(resolved) {
|
|
21
|
+
if (resolved.cwd) {
|
|
22
|
+
let stat;
|
|
23
|
+
try {
|
|
24
|
+
stat = fs.statSync(resolved.cwd);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(`cwd does not exist: ${resolved.cwd}`);
|
|
27
|
+
}
|
|
28
|
+
if (!stat.isDirectory()) {
|
|
29
|
+
throw new Error(`cwd is not a directory: ${resolved.cwd}`);
|
|
30
|
+
}
|
|
59
31
|
}
|
|
60
32
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Skill and Command/Template invocation share the same underlying
|
|
74
|
-
// opencode mechanism: skills are registered internally as commands
|
|
75
|
-
// (source:"skill"), so `--command <name>` handles both -- verified
|
|
76
|
-
// against packages/opencode/src/command/index.ts.
|
|
77
|
-
if (resolved.invocation === 'skill' || resolved.invocation === 'command') {
|
|
78
|
-
args.push('--command', String(resolved.invocationName));
|
|
79
|
-
args.push(resolved.args !== undefined && resolved.args !== null ? String(resolved.args) : '');
|
|
80
|
-
} else {
|
|
81
|
-
args.push(String(resolved.prompt));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const env = {};
|
|
85
|
-
if (Array.isArray(resolved.mcpServers) && resolved.mcpServers.length > 0) {
|
|
86
|
-
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ mcp: toOpenCodeMcp(resolved.mcpServers) });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return { command: 'opencode', args, env };
|
|
33
|
+
if (resolved.invocation === "prompt") {
|
|
34
|
+
if (!resolved.prompt || !String(resolved.prompt).trim()) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"prompt invocation requires a non-empty prompt (msg.payload or the Prompt field)",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} else if (resolved.invocation === "skill" || resolved.invocation === "command") {
|
|
40
|
+
if (!resolved.invocationName || !String(resolved.invocationName).trim()) {
|
|
41
|
+
throw new Error(`${resolved.invocation} invocation requires a non-empty name`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(`unknown invocation mode: ${resolved.invocation}`);
|
|
90
45
|
}
|
|
91
46
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
47
|
+
for (const server of resolved.mcpServers || []) {
|
|
48
|
+
if (!server || !server.name) {
|
|
49
|
+
throw new Error("mcpServers entries require a name");
|
|
50
|
+
}
|
|
51
|
+
if (server.type === "remote" && !server.url) {
|
|
52
|
+
throw new Error(`mcp server "${server.name}" (remote) requires a url`);
|
|
53
|
+
}
|
|
54
|
+
if (server.type === "local" && !server.command) {
|
|
55
|
+
throw new Error(`mcp server "${server.name}" (local) requires a command`);
|
|
56
|
+
}
|
|
57
|
+
if (server.type !== "remote" && server.type !== "local") {
|
|
58
|
+
throw new Error(`mcp server "${server.name}" has unknown type: ${server.type}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
buildExecution(resolved) {
|
|
64
|
+
const args = ["run", "--format", "json"];
|
|
65
|
+
|
|
66
|
+
// Resuming an existing session (opencode run -s <id> ...) rather
|
|
67
|
+
// than always starting a new one -- verified against `opencode run
|
|
68
|
+
// --help`: -s/--session takes the id to continue.
|
|
69
|
+
if (resolved.sessionID) args.push("--session", String(resolved.sessionID));
|
|
70
|
+
|
|
71
|
+
if (resolved.cwd) args.push("--dir", resolved.cwd);
|
|
72
|
+
if (resolved.model) args.push("--model", resolved.model);
|
|
73
|
+
if (resolved.auto) args.push("--auto");
|
|
74
|
+
|
|
75
|
+
// Skill and Command/Template invocation share the same underlying
|
|
76
|
+
// opencode mechanism: skills are registered internally as commands
|
|
77
|
+
// (source:"skill"), so `--command <name>` handles both -- verified
|
|
78
|
+
// against packages/opencode/src/command/index.ts.
|
|
79
|
+
if (resolved.invocation === "skill" || resolved.invocation === "command") {
|
|
80
|
+
args.push("--command", String(resolved.invocationName));
|
|
81
|
+
args.push(resolved.args !== undefined && resolved.args !== null ? String(resolved.args) : "");
|
|
82
|
+
} else {
|
|
83
|
+
args.push(String(resolved.prompt));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const env = {};
|
|
87
|
+
if (Array.isArray(resolved.mcpServers) && resolved.mcpServers.length > 0) {
|
|
88
|
+
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({ mcp: toOpenCodeMcp(resolved.mcpServers) });
|
|
89
|
+
}
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
raw = JSON.parse(trimmed);
|
|
99
|
-
} catch (err) {
|
|
100
|
-
// Malformed/non-JSON diagnostic output must never crash Node-RED.
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
91
|
+
return { command: "opencode", args, env };
|
|
92
|
+
}
|
|
103
93
|
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
parseEvent(line) {
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
if (!trimmed) return null;
|
|
97
|
+
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = JSON.parse(trimmed);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Malformed/non-JSON diagnostic output must never crash Node-RED.
|
|
103
|
+
return null;
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
106
|
+
const type = TYPE_MAP[raw.type] || raw.type || "progress";
|
|
107
|
+
return { type, sessionID: raw.sessionID, data: raw };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parseResult(events, exitCode, signal, stderr) {
|
|
111
|
+
const raw = events.map((e) => e.data);
|
|
112
|
+
const errorEvent = raw.find((e) => e.type === "error");
|
|
113
|
+
const sessionID = raw.length ? raw[raw.length - 1].sessionID : undefined;
|
|
114
|
+
|
|
115
|
+
const payload = raw
|
|
116
|
+
.filter((e) => e.type === "text" && e.part && typeof e.part.text === "string")
|
|
117
|
+
.map((e) => e.part.text)
|
|
118
|
+
.join("\n")
|
|
119
|
+
.trim();
|
|
120
|
+
|
|
121
|
+
if (errorEvent) {
|
|
122
|
+
const message =
|
|
123
|
+
(errorEvent.error &&
|
|
124
|
+
((errorEvent.error.data && errorEvent.error.data.message) || errorEvent.error.name)) ||
|
|
125
|
+
"opencode reported an error";
|
|
126
|
+
return { payload, sessionID, status: "failed", errorMessage: message };
|
|
127
|
+
}
|
|
128
|
+
if (signal) {
|
|
129
|
+
return {
|
|
130
|
+
payload,
|
|
131
|
+
sessionID,
|
|
132
|
+
status: "failed",
|
|
133
|
+
errorMessage: `process killed by signal ${signal}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (exitCode !== 0) {
|
|
137
|
+
return {
|
|
138
|
+
payload,
|
|
139
|
+
sessionID,
|
|
140
|
+
status: "failed",
|
|
141
|
+
errorMessage: `exited with code ${exitCode}${stderr ? ": " + String(stderr).trim() : ""}`,
|
|
142
|
+
};
|
|
138
143
|
}
|
|
144
|
+
return { payload, sessionID, status: "completed" };
|
|
145
|
+
}
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
module.exports = { OpenCodeAdapter };
|