@themoltnet/node-red-contrib-core 0.1.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/LICENSE +235 -0
- package/README.md +267 -0
- package/dist/nodes/agent.html +82 -0
- package/dist/nodes/agent.js +28 -0
- package/dist/nodes/runtime-profile.html +156 -0
- package/dist/nodes/runtime-profile.js +40 -0
- package/dist/nodes/task-builder.html +175 -0
- package/dist/nodes/task-builder.js +110 -0
- package/dist/nodes/task-get.html +55 -0
- package/dist/nodes/task-get.js +60 -0
- package/dist/nodes/task-reader.html +64 -0
- package/dist/nodes/task-reader.js +57 -0
- package/dist/nodes/task-snapshot.js +32 -0
- package/dist/nodes/task-wait.html +166 -0
- package/dist/nodes/task-wait.js +133 -0
- package/dist/nodes/tasks-create.html +122 -0
- package/dist/nodes/tasks-create.js +64 -0
- package/dist/nodes/workflow-status.html +55 -0
- package/dist/nodes/workflow-status.js +62 -0
- package/examples/cockpit.flow.json +58 -0
- package/examples/issue-lifecycle.flow.json +196 -0
- package/examples/weather-advisor.flow.json +398 -0
- package/package.json +65 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { TaskResultError, createResultReader } from "@themoltnet/sdk";
|
|
2
|
+
//#region src/nodes/task-reader.ts
|
|
3
|
+
var init = (RED) => {
|
|
4
|
+
function TaskReaderNode(def) {
|
|
5
|
+
RED.nodes.createNode(this, def);
|
|
6
|
+
this.on("input", (msg, send, done) => {
|
|
7
|
+
try {
|
|
8
|
+
const sourcePath = def.source && def.source.length > 0 ? def.source : "payload";
|
|
9
|
+
const snapshot = RED.util.getMessageProperty(msg, sourcePath);
|
|
10
|
+
if (!snapshot || !snapshot.task || !snapshot.attempt) throw new TaskResultError([{
|
|
11
|
+
field: "payload",
|
|
12
|
+
message: "no task snapshot found on the message"
|
|
13
|
+
}]);
|
|
14
|
+
const reader = createResultReader(snapshot.task, snapshot.attempt);
|
|
15
|
+
const role = def.role ?? "context";
|
|
16
|
+
const filter = def.artifactKind ? def.artifactTitle ? {
|
|
17
|
+
kind: def.artifactKind,
|
|
18
|
+
title: def.artifactTitle
|
|
19
|
+
} : def.artifactKind : void 0;
|
|
20
|
+
let artifactBody;
|
|
21
|
+
const artifact = filter ? reader.artifact(filter) : void 0;
|
|
22
|
+
if (filter && artifact && typeof artifact.body === "string") try {
|
|
23
|
+
artifactBody = JSON.parse(artifact.body);
|
|
24
|
+
} catch {
|
|
25
|
+
artifactBody = void 0;
|
|
26
|
+
}
|
|
27
|
+
const out = RED.util.cloneMessage(msg);
|
|
28
|
+
out.payload = reader.output;
|
|
29
|
+
out.result = {
|
|
30
|
+
summary: reader.summary,
|
|
31
|
+
outputRef: reader.outputRef(role),
|
|
32
|
+
artifact,
|
|
33
|
+
artifactBody,
|
|
34
|
+
accepted: reader.accepted,
|
|
35
|
+
usage: reader.usage
|
|
36
|
+
};
|
|
37
|
+
this.status({
|
|
38
|
+
fill: "green",
|
|
39
|
+
shape: "dot",
|
|
40
|
+
text: "read"
|
|
41
|
+
});
|
|
42
|
+
send(out);
|
|
43
|
+
done();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
this.status({
|
|
46
|
+
fill: "red",
|
|
47
|
+
shape: "ring",
|
|
48
|
+
text: "error"
|
|
49
|
+
});
|
|
50
|
+
done(err instanceof TaskResultError ? new Error(err.message) : err instanceof Error ? err : new Error(String(err)));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
RED.nodes.registerType("moltnet-task-reader", TaskReaderNode);
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { init as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/nodes/task-snapshot.ts
|
|
2
|
+
/** Task statuses that mean the run will not progress further. */
|
|
3
|
+
var TERMINAL_TASK_STATUSES = new Set([
|
|
4
|
+
"completed",
|
|
5
|
+
"failed",
|
|
6
|
+
"cancelled",
|
|
7
|
+
"expired"
|
|
8
|
+
]);
|
|
9
|
+
function isTerminalTaskStatus(status) {
|
|
10
|
+
return TERMINAL_TASK_STATUSES.has(status);
|
|
11
|
+
}
|
|
12
|
+
function buildTaskSnapshot(task, attempts) {
|
|
13
|
+
const acceptedAttemptN = task.acceptedAttemptN;
|
|
14
|
+
const acceptedAttempt = acceptedAttemptN !== null ? attempts.find((a) => a.attemptN === acceptedAttemptN) ?? null : null;
|
|
15
|
+
const latestAttempt = attempts.length > 0 ? attempts.reduce((max, a) => a.attemptN > max.attemptN ? a : max) : null;
|
|
16
|
+
const attempt = acceptedAttempt ?? latestAttempt;
|
|
17
|
+
const accepted = acceptedAttempt !== null;
|
|
18
|
+
return {
|
|
19
|
+
taskId: task.id,
|
|
20
|
+
status: task.status,
|
|
21
|
+
terminal: isTerminalTaskStatus(task.status),
|
|
22
|
+
accepted,
|
|
23
|
+
acceptedAttemptN,
|
|
24
|
+
state: accepted ? acceptedAttempt?.output ?? null : null,
|
|
25
|
+
attempt,
|
|
26
|
+
attempts,
|
|
27
|
+
error: latestAttempt?.error ?? null,
|
|
28
|
+
task
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { isTerminalTaskStatus as n, buildTaskSnapshot as t };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
const MN_MESSAGE_KINDS = [
|
|
3
|
+
'text_delta',
|
|
4
|
+
'tool_call_start',
|
|
5
|
+
'tool_call_end',
|
|
6
|
+
'turn_end',
|
|
7
|
+
'error',
|
|
8
|
+
'info',
|
|
9
|
+
];
|
|
10
|
+
RED.nodes.registerType('moltnet-task-wait', {
|
|
11
|
+
category: 'moltnet',
|
|
12
|
+
color: '#00d4c8',
|
|
13
|
+
paletteLabel: 'task: wait',
|
|
14
|
+
defaults: {
|
|
15
|
+
name: { value: '' },
|
|
16
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
17
|
+
taskId: { value: '' },
|
|
18
|
+
pollIntervalSec: { value: 5, validate: RED.validators.number() },
|
|
19
|
+
timeoutSec: { value: 1800, validate: RED.validators.number() },
|
|
20
|
+
tail: { value: false },
|
|
21
|
+
kinds: { value: '' },
|
|
22
|
+
},
|
|
23
|
+
inputs: 1,
|
|
24
|
+
outputs: 2,
|
|
25
|
+
outputLabels: ['tail', 'result'],
|
|
26
|
+
icon: 'font-awesome/fa-hourglass-half',
|
|
27
|
+
label: function () {
|
|
28
|
+
return this.name || 'task: wait';
|
|
29
|
+
},
|
|
30
|
+
oneditprepare: function () {
|
|
31
|
+
// Render one checkbox per message kind. `kinds` is stored as a
|
|
32
|
+
// comma-separated string (the runtime contract); empty = all kinds.
|
|
33
|
+
const selected = (this.kinds || '')
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
const container = $('#node-input-kinds-list').empty();
|
|
38
|
+
MN_MESSAGE_KINDS.forEach((kind) => {
|
|
39
|
+
const checked = selected.length === 0 || selected.indexOf(kind) !== -1;
|
|
40
|
+
container.append(
|
|
41
|
+
'<label style="display:inline-block;width:auto;margin-right:12px;font-weight:normal">' +
|
|
42
|
+
'<input type="checkbox" class="mn-kind" value="' +
|
|
43
|
+
kind +
|
|
44
|
+
'" ' +
|
|
45
|
+
(checked ? 'checked' : '') +
|
|
46
|
+
' style="display:inline-block;width:auto;vertical-align:baseline;margin-right:4px"/>' +
|
|
47
|
+
kind +
|
|
48
|
+
'</label>',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
// Show the kinds row only when tailing is enabled.
|
|
52
|
+
const toggle = () => {
|
|
53
|
+
$('#node-row-kinds').toggle($('#node-input-tail').is(':checked'));
|
|
54
|
+
};
|
|
55
|
+
$('#node-input-tail').on('change', toggle);
|
|
56
|
+
toggle();
|
|
57
|
+
},
|
|
58
|
+
oneditsave: function () {
|
|
59
|
+
const all = $('.mn-kind').length;
|
|
60
|
+
const checked = $('.mn-kind:checked')
|
|
61
|
+
.map(function () {
|
|
62
|
+
return this.value;
|
|
63
|
+
})
|
|
64
|
+
.get();
|
|
65
|
+
// All (or none) checked = no filter → empty string forwards every kind.
|
|
66
|
+
this.kinds = checked.length === all ? '' : checked.join(',');
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script type="text/html" data-template-name="moltnet-task-wait">
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
74
|
+
<input type="text" id="node-input-name" />
|
|
75
|
+
</div>
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
78
|
+
<input type="text" id="node-input-agent" />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="form-row">
|
|
81
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
82
|
+
<input
|
|
83
|
+
type="text"
|
|
84
|
+
id="node-input-taskId"
|
|
85
|
+
placeholder="msg.payload.taskId"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="form-row">
|
|
89
|
+
<label for="node-input-pollIntervalSec"
|
|
90
|
+
><i class="fa fa-clock-o"></i> Poll (s)</label
|
|
91
|
+
>
|
|
92
|
+
<input type="text" id="node-input-pollIntervalSec" placeholder="5" />
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-row">
|
|
95
|
+
<label for="node-input-timeoutSec"
|
|
96
|
+
><i class="fa fa-hourglass-end"></i> Timeout (s)</label
|
|
97
|
+
>
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
id="node-input-timeoutSec"
|
|
101
|
+
placeholder="1800 (0 = none)"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="form-row">
|
|
105
|
+
<label for="node-input-tail"><i class="fa fa-stream"></i> Tail</label>
|
|
106
|
+
<input
|
|
107
|
+
type="checkbox"
|
|
108
|
+
id="node-input-tail"
|
|
109
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
110
|
+
/>
|
|
111
|
+
<span>Emit live task messages on output 1</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="form-row" id="node-row-kinds" style="margin-bottom: 0">
|
|
114
|
+
<label style="vertical-align: top"
|
|
115
|
+
><i class="fa fa-filter"></i> Kinds</label
|
|
116
|
+
>
|
|
117
|
+
<div
|
|
118
|
+
id="node-input-kinds-list"
|
|
119
|
+
style="display: inline-block; width: 70%; vertical-align: top"
|
|
120
|
+
></div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="form-tips" id="node-tip-kinds">
|
|
123
|
+
Tail forwards only the checked message kinds. All checked = forward every
|
|
124
|
+
kind.
|
|
125
|
+
</div>
|
|
126
|
+
<input type="hidden" id="node-input-kinds" />
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<script type="text/html" data-help-name="moltnet-task-wait">
|
|
130
|
+
<p>
|
|
131
|
+
Polls a MoltNet task until it reaches a terminal status, in one loop that
|
|
132
|
+
does double duty (like the CLI's <code>task tail</code>).
|
|
133
|
+
</p>
|
|
134
|
+
<h3>Inputs</h3>
|
|
135
|
+
<dl class="message-properties">
|
|
136
|
+
<dt>taskId<span class="property-type">string</span></dt>
|
|
137
|
+
<dd>
|
|
138
|
+
From <code>msg.taskId</code>, <code>msg.payload.taskId</code>, or
|
|
139
|
+
<code>msg.payload.id</code>, falling back to the configured Task ID.
|
|
140
|
+
</dd>
|
|
141
|
+
</dl>
|
|
142
|
+
<h3>Outputs</h3>
|
|
143
|
+
<ol class="node-ports">
|
|
144
|
+
<li>
|
|
145
|
+
<b>tail</b> — each new task message (<code>text_delta</code>,
|
|
146
|
+
<code>tool_call_start/end</code>, <code>turn_end</code>,
|
|
147
|
+
<code>error</code>, <code>info</code>) as it arrives, on
|
|
148
|
+
<code>msg.payload</code>. Only emitted when <b>Tail</b> is enabled; the
|
|
149
|
+
optional <b>Kinds</b> filter forwards just those kinds (cursor still
|
|
150
|
+
advances past the rest). Fires many times.
|
|
151
|
+
</li>
|
|
152
|
+
<li>
|
|
153
|
+
<b>result</b> — the terminal snapshot on <code>msg.payload</code>:
|
|
154
|
+
<code
|
|
155
|
+
>{ status, terminal, accepted, state, attempt, attempts, error, task
|
|
156
|
+
}</code
|
|
157
|
+
>. Fires once. On failure, <code>error</code> carries the last attempt's
|
|
158
|
+
error for an agent/human to interpret.
|
|
159
|
+
</li>
|
|
160
|
+
</ol>
|
|
161
|
+
<p>
|
|
162
|
+
Terminal status is checked after draining each message page, so trailing
|
|
163
|
+
messages land on the tail output before the result fires. A
|
|
164
|
+
<b>Timeout</b> of <code>0</code> waits indefinitely.
|
|
165
|
+
</p>
|
|
166
|
+
</script>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { n as isTerminalTaskStatus, t as buildTaskSnapshot } from "./task-snapshot.js";
|
|
2
|
+
//#region src/nodes/task-wait.ts
|
|
3
|
+
var DEFAULT_POLL_SEC = 5;
|
|
4
|
+
var DEFAULT_TIMEOUT_SEC = 1800;
|
|
5
|
+
var init = (RED) => {
|
|
6
|
+
function TaskWaitNode(def) {
|
|
7
|
+
RED.nodes.createNode(this, def);
|
|
8
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
9
|
+
const pollMs = Math.max(1, def.pollIntervalSec || DEFAULT_POLL_SEC) * 1e3;
|
|
10
|
+
const timeoutMs = (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) > 0 ? (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3 : 0;
|
|
11
|
+
const kindAllow = parseKinds(def.kinds);
|
|
12
|
+
const pending = /* @__PURE__ */ new Set();
|
|
13
|
+
this.on("close", () => {
|
|
14
|
+
for (const t of pending) clearTimeout(t);
|
|
15
|
+
pending.clear();
|
|
16
|
+
});
|
|
17
|
+
this.on("input", (msg, send, done) => {
|
|
18
|
+
const run = async () => {
|
|
19
|
+
try {
|
|
20
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-wait: no moltnet-agent configured");
|
|
21
|
+
const taskId = resolveTaskId(msg, def.taskId);
|
|
22
|
+
if (!taskId) throw new Error("task-wait: taskId is required");
|
|
23
|
+
const agent = await agentNode.getAgent();
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
let afterSeq;
|
|
26
|
+
let polls = 0;
|
|
27
|
+
this.status({
|
|
28
|
+
fill: "blue",
|
|
29
|
+
shape: "dot",
|
|
30
|
+
text: "waiting…"
|
|
31
|
+
});
|
|
32
|
+
for (;;) {
|
|
33
|
+
if (def.tail) afterSeq = await drainMessages({
|
|
34
|
+
agent,
|
|
35
|
+
taskId,
|
|
36
|
+
afterSeq,
|
|
37
|
+
kindAllow,
|
|
38
|
+
emit: (m) => {
|
|
39
|
+
const tailMsg = RED.util.cloneMessage(msg);
|
|
40
|
+
tailMsg.payload = m;
|
|
41
|
+
tailMsg.taskId = taskId;
|
|
42
|
+
send([tailMsg, null]);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const task = await agent.tasks.get(taskId);
|
|
46
|
+
if (isTerminalTaskStatus(task.status)) {
|
|
47
|
+
const snapshot = buildTaskSnapshot(task, await agent.tasks.listAttempts(taskId));
|
|
48
|
+
const resultMsg = RED.util.cloneMessage(msg);
|
|
49
|
+
resultMsg.payload = snapshot;
|
|
50
|
+
this.status({
|
|
51
|
+
fill: snapshot.accepted ? "green" : "red",
|
|
52
|
+
shape: "dot",
|
|
53
|
+
text: `${snapshot.status}${snapshot.accepted ? " ✓" : ""}`
|
|
54
|
+
});
|
|
55
|
+
send([null, resultMsg]);
|
|
56
|
+
done();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
polls += 1;
|
|
60
|
+
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) throw new Error(`task-wait: timed out after ${Math.round((Date.now() - startedAt) / 1e3)}s (${polls} polls) waiting for task ${taskId} to settle`);
|
|
61
|
+
this.status({
|
|
62
|
+
fill: "blue",
|
|
63
|
+
shape: "ring",
|
|
64
|
+
text: `${task.status} · ${polls}×`
|
|
65
|
+
});
|
|
66
|
+
await sleep(pollMs, pending);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.status({
|
|
70
|
+
fill: "red",
|
|
71
|
+
shape: "ring",
|
|
72
|
+
text: "error"
|
|
73
|
+
});
|
|
74
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
run();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
RED.nodes.registerType("moltnet-task-wait", TaskWaitNode);
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Fetch the latest attempt's new messages and emit the allowed ones. Returns
|
|
84
|
+
* the advanced cursor. The cursor advances for EVERY message seen (even ones the
|
|
85
|
+
* kind filter drops), otherwise a fully-filtered page would re-fetch forever.
|
|
86
|
+
*/
|
|
87
|
+
async function drainMessages(args) {
|
|
88
|
+
const { agent, taskId, kindAllow, emit } = args;
|
|
89
|
+
let afterSeq = args.afterSeq;
|
|
90
|
+
const attempts = await agent.tasks.listAttempts(taskId);
|
|
91
|
+
if (attempts.length === 0) return afterSeq;
|
|
92
|
+
const latest = attempts.reduce((max, a) => a.attemptN > max.attemptN ? a : max);
|
|
93
|
+
const limit = 100;
|
|
94
|
+
for (;;) {
|
|
95
|
+
const messages = await agent.tasks.listMessages(taskId, latest.attemptN, {
|
|
96
|
+
afterSeq,
|
|
97
|
+
limit
|
|
98
|
+
});
|
|
99
|
+
for (const m of messages) {
|
|
100
|
+
if (afterSeq === void 0 || m.seq > afterSeq) afterSeq = m.seq;
|
|
101
|
+
if (kindAllow && !kindAllow.has(m.kind)) continue;
|
|
102
|
+
emit(m);
|
|
103
|
+
}
|
|
104
|
+
if (messages.length < limit) break;
|
|
105
|
+
}
|
|
106
|
+
return afterSeq;
|
|
107
|
+
}
|
|
108
|
+
function parseKinds(raw) {
|
|
109
|
+
if (!raw || !raw.trim()) return null;
|
|
110
|
+
const kinds = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
111
|
+
return kinds.length > 0 ? new Set(kinds) : null;
|
|
112
|
+
}
|
|
113
|
+
function resolveTaskId(msg, configured) {
|
|
114
|
+
if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
|
|
115
|
+
const payload = msg.payload;
|
|
116
|
+
if (payload && typeof payload === "object") {
|
|
117
|
+
const p = payload;
|
|
118
|
+
if (typeof p.taskId === "string" && p.taskId) return p.taskId;
|
|
119
|
+
if (typeof p.id === "string" && p.id) return p.id;
|
|
120
|
+
}
|
|
121
|
+
return configured && configured.length > 0 ? configured : void 0;
|
|
122
|
+
}
|
|
123
|
+
function sleep(ms, pending) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const t = setTimeout(() => {
|
|
126
|
+
pending.delete(t);
|
|
127
|
+
resolve();
|
|
128
|
+
}, ms);
|
|
129
|
+
pending.add(t);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { init as default };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-tasks-create', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'tasks: create',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
runtimeProfile: {
|
|
10
|
+
value: '',
|
|
11
|
+
type: 'moltnet-runtime-profile',
|
|
12
|
+
// Optional: a config-typed field is implicitly required unless we
|
|
13
|
+
// explicitly allow the empty value.
|
|
14
|
+
required: false,
|
|
15
|
+
validate: function (v) {
|
|
16
|
+
return !v || v === '_ADD_' || typeof v === 'string';
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
maxAttempts: { value: null, validate: RED.validators.number(true) },
|
|
20
|
+
generateCorrelationId: { value: false },
|
|
21
|
+
},
|
|
22
|
+
inputs: 1,
|
|
23
|
+
outputs: 1,
|
|
24
|
+
icon: 'font-awesome/fa-plus-square',
|
|
25
|
+
label: function () {
|
|
26
|
+
return this.name || 'tasks: create';
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script type="text/html" data-template-name="moltnet-tasks-create">
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
34
|
+
<input type="text" id="node-input-name" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
38
|
+
<input type="text" id="node-input-agent" />
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-runtimeProfile"
|
|
42
|
+
><i class="fa fa-microchip"></i> Profile</label
|
|
43
|
+
>
|
|
44
|
+
<input type="text" id="node-input-runtimeProfile" />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-maxAttempts"
|
|
48
|
+
><i class="fa fa-repeat"></i> Max attempts</label
|
|
49
|
+
>
|
|
50
|
+
<input type="text" id="node-input-maxAttempts" placeholder="(optional)" />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<label for="node-input-generateCorrelationId"
|
|
54
|
+
><i class="fa fa-link"></i> Correlation</label
|
|
55
|
+
>
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
id="node-input-generateCorrelationId"
|
|
59
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
60
|
+
/>
|
|
61
|
+
<span>Mint a new correlationId if the message has none</span>
|
|
62
|
+
</div>
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<script type="text/html" data-help-name="moltnet-tasks-create">
|
|
66
|
+
<p>
|
|
67
|
+
Creates a MoltNet task as the referenced <b>agent</b>. The created task is
|
|
68
|
+
returned on <code>msg.payload</code>.
|
|
69
|
+
</p>
|
|
70
|
+
<h3>Building the request</h3>
|
|
71
|
+
<p>
|
|
72
|
+
The task body — <b>task type</b>, <b>title</b>, <b>tags</b>,
|
|
73
|
+
<code>input</code>, <code>references</code>, gates — is composed upstream by
|
|
74
|
+
a <code>task: build</code> node and arrives on <code>msg.payload</code>.
|
|
75
|
+
This node submits it and fills only the dispatch-level gaps:
|
|
76
|
+
</p>
|
|
77
|
+
<ul>
|
|
78
|
+
<li>
|
|
79
|
+
<b>Profile</b> — a <code>moltnet-runtime-profile</code> config node sets
|
|
80
|
+
<code>allowedProfiles</code> (a routing gate pairing the task with the
|
|
81
|
+
daemon that claims it). Set here, not in the builder.
|
|
82
|
+
<code>msg.payload.allowedProfiles</code> overrides it.
|
|
83
|
+
</li>
|
|
84
|
+
<li><b>Max attempts</b> — per-task retry budget.</li>
|
|
85
|
+
<li>
|
|
86
|
+
<b>Correlation</b> — mint/thread a <code>correlationId</code> (below).
|
|
87
|
+
</li>
|
|
88
|
+
</ul>
|
|
89
|
+
<p>
|
|
90
|
+
If <code>msg.payload</code> carries no <code>taskType</code>, it defaults to
|
|
91
|
+
<code>freeform</code>. For ad-hoc flows you can still set the whole body on
|
|
92
|
+
<code>msg.payload</code> with a <code>function</code>/<code>change</code>
|
|
93
|
+
node instead of <code>task: build</code>; the <code>input</code> shape per
|
|
94
|
+
task type comes from <code>GET /tasks/schemas</code> and the
|
|
95
|
+
<a href="https://api.themolt.net/openapi.json" target="_blank"
|
|
96
|
+
>OpenAPI spec</a
|
|
97
|
+
>
|
|
98
|
+
(<code>CreateTask</code>).
|
|
99
|
+
</p>
|
|
100
|
+
<p>
|
|
101
|
+
<code>teamId</code>, <code>diaryId</code> and <code>correlationId</code> are
|
|
102
|
+
filled automatically (see below) — you rarely set them by hand.
|
|
103
|
+
</p>
|
|
104
|
+
<h3>Team & diary context</h3>
|
|
105
|
+
<p>
|
|
106
|
+
<code>teamId</code> and <code>diaryId</code> come from the
|
|
107
|
+
<b>agent</b> config node, not this node — the agent establishes which team
|
|
108
|
+
and diary you act in. A <code>msg.payload.teamId</code> /
|
|
109
|
+
<code>msg.payload.diaryId</code> overrides the agent default.
|
|
110
|
+
</p>
|
|
111
|
+
<h3>correlationId (workflow runs)</h3>
|
|
112
|
+
<p>
|
|
113
|
+
A <code>correlationId</code> threads a workflow run across tasks (mandatory
|
|
114
|
+
for a workflow). It is resolved from
|
|
115
|
+
<code>msg.payload.correlationId</code> then <code>msg.correlationId</code>.
|
|
116
|
+
Enable <b>Correlation</b> on the first <code>tasks: create</code> of a run
|
|
117
|
+
to mint a fresh id when the message has none. The resolved id is written
|
|
118
|
+
back to <code>msg.correlationId</code>, so downstream
|
|
119
|
+
<code>task: wait</code> / <code>tasks: create</code> /
|
|
120
|
+
<code>workflow: status</code> nodes inherit the same run.
|
|
121
|
+
</p>
|
|
122
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
//#region src/nodes/tasks-create.ts
|
|
3
|
+
var init = (RED) => {
|
|
4
|
+
function TasksCreateNode(def) {
|
|
5
|
+
RED.nodes.createNode(this, def);
|
|
6
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
7
|
+
const profileNode = def.runtimeProfile ? RED.nodes.getNode(def.runtimeProfile) : null;
|
|
8
|
+
this.on("input", (msg, send, done) => {
|
|
9
|
+
const run = async () => {
|
|
10
|
+
try {
|
|
11
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("tasks-create: no moltnet-agent configured");
|
|
12
|
+
this.status({
|
|
13
|
+
fill: "blue",
|
|
14
|
+
shape: "dot",
|
|
15
|
+
text: "creating…"
|
|
16
|
+
});
|
|
17
|
+
const agent = await agentNode.getAgent();
|
|
18
|
+
const base = msg.payload && typeof msg.payload === "object" ? { ...msg.payload } : {};
|
|
19
|
+
if (!base.taskType) base.taskType = "freeform";
|
|
20
|
+
if (!base.allowedProfiles && profileNode?.profileId) base.allowedProfiles = [{ profileId: profileNode.profileId }];
|
|
21
|
+
if (base.maxAttempts === void 0 && typeof def.maxAttempts === "number") base.maxAttempts = def.maxAttempts;
|
|
22
|
+
if (!base.teamId && agentNode.teamId) base.teamId = agentNode.teamId;
|
|
23
|
+
if (!base.diaryId && agentNode.diaryId) base.diaryId = agentNode.diaryId;
|
|
24
|
+
const correlationId = resolveCorrelationId(msg, base.correlationId, def.generateCorrelationId === true);
|
|
25
|
+
if (correlationId) base.correlationId = correlationId;
|
|
26
|
+
const { teamId, ...createBody } = base;
|
|
27
|
+
const task = await agent.tasks.create(createBody, { teamId });
|
|
28
|
+
const out = RED.util.cloneMessage(msg);
|
|
29
|
+
if (correlationId) out.correlationId = correlationId;
|
|
30
|
+
out.payload = task;
|
|
31
|
+
this.status({
|
|
32
|
+
fill: "green",
|
|
33
|
+
shape: "dot",
|
|
34
|
+
text: `task ${task.id ?? "created"}`
|
|
35
|
+
});
|
|
36
|
+
send(out);
|
|
37
|
+
done();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
this.status({
|
|
40
|
+
fill: "red",
|
|
41
|
+
shape: "ring",
|
|
42
|
+
text: "error"
|
|
43
|
+
});
|
|
44
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
run();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
RED.nodes.registerType("moltnet-tasks-create", TasksCreateNode);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the workflow correlationId in priority order: explicit payload value,
|
|
54
|
+
* then `msg.correlationId`, then mint a fresh UUID when generation is enabled.
|
|
55
|
+
* Returns `undefined` when none is available and generation is off (the task is
|
|
56
|
+
* then created without one — valid for ad-hoc, non-workflow tasks).
|
|
57
|
+
*/
|
|
58
|
+
function resolveCorrelationId(msg, fromPayload, generate) {
|
|
59
|
+
if (typeof fromPayload === "string" && fromPayload) return fromPayload;
|
|
60
|
+
if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
|
|
61
|
+
return generate ? randomUUID() : void 0;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { init as default };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-workflow-status', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'workflow: status',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
correlationId: { value: '' },
|
|
10
|
+
limit: { value: 50, validate: RED.validators.number() },
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 1,
|
|
14
|
+
icon: 'font-awesome/fa-list',
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.name || 'workflow: status';
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script type="text/html" data-template-name="moltnet-workflow-status">
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
24
|
+
<input type="text" id="node-input-name" />
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
28
|
+
<input type="text" id="node-input-agent" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-input-correlationId"
|
|
32
|
+
><i class="fa fa-link"></i> Correlation ID</label
|
|
33
|
+
>
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
id="node-input-correlationId"
|
|
37
|
+
placeholder="msg.correlationId overrides"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
|
|
42
|
+
<input type="number" id="node-input-limit" />
|
|
43
|
+
</div>
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<script type="text/html" data-help-name="moltnet-workflow-status">
|
|
47
|
+
<p>
|
|
48
|
+
Reads the tasks of one workflow run (by <code>correlationId</code>) and
|
|
49
|
+
emits a table-shaped <code>msg.payload</code> (array of rows) for a stock
|
|
50
|
+
Dashboard 2.0 <code>ui-table</code>. The correlation id comes from
|
|
51
|
+
<code>msg.correlationId</code>, <code>msg.payload.correlationId</code>, or
|
|
52
|
+
the node config, in that order. <code>msg.workflow</code> carries
|
|
53
|
+
<code>{ correlationId, total }</code>.
|
|
54
|
+
</p>
|
|
55
|
+
</script>
|