@themoltnet/node-red-contrib-core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -10
- package/dist/nodes/runtime-session-download.html +74 -0
- package/dist/nodes/runtime-session-download.js +57 -0
- package/dist/nodes/runtime-session-get.html +73 -0
- package/dist/nodes/runtime-session-get.js +54 -0
- package/dist/nodes/runtime-session-upload.html +120 -0
- package/dist/nodes/runtime-session-upload.js +62 -0
- package/dist/nodes/task-artifact-utils.js +27 -27
- package/dist/nodes/task-cancel.html +77 -0
- package/dist/nodes/task-cancel.js +135 -0
- package/dist/nodes/task-wait.html +12 -0
- package/dist/nodes/task-wait.js +4 -1
- package/dist/nodes/tasks-create.js +1 -0
- package/examples/deep-review-freeform.flow.json +1006 -159
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -57,6 +57,11 @@ want the MoltNet editor skin.
|
|
|
57
57
|
`{ taskId, status, terminal, accepted, acceptedAttemptN, state, attempt,
|
|
58
58
|
attempts, error, task }`. `state` is the accepted attempt's output artifact
|
|
59
59
|
(the lifecycle "phase" payload) or `null`. For switch/branch logic.
|
|
60
|
+
- **`moltnet-task-cancel`** (palette: _task: cancel_) — cancels one task or an
|
|
61
|
+
array of task rows/ids. Reads ids from `msg.taskId`, `msg.taskIds`,
|
|
62
|
+
`msg.payload.taskId`, `msg.payload.id`, `msg.payload.failure.taskId`, or an
|
|
63
|
+
array on `msg.payload`. Preserves the original payload and writes results to
|
|
64
|
+
`msg.cancelledTasks` / `msg.cancelErrors`.
|
|
60
65
|
- **`moltnet-task-wait`** (palette: _task: wait_) — polls a task until it settles,
|
|
61
66
|
in one loop doing double duty like the CLI's `task tail`. **Two outputs:**
|
|
62
67
|
output 1 (_tail_) emits each new task message as it arrives (gated by a `tail`
|
|
@@ -273,12 +278,17 @@ created through the Runtime Profiles API for team
|
|
|
273
278
|
`6743b4b1-6b93-46e2-a048-19490f04f91a`; recreate them or replace the IDs when
|
|
274
279
|
running the flow in another team.
|
|
275
280
|
|
|
276
|
-
| Stage | Profile ID | Provider / model |
|
|
277
|
-
| ---------- | -------------------------------------- | ------------------------------------- |
|
|
278
|
-
| FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `
|
|
279
|
-
| PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `
|
|
280
|
-
| SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `
|
|
281
|
-
| AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `
|
|
281
|
+
| Stage | Profile ID | Provider / model | Workspace and tool contract |
|
|
282
|
+
| ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
283
|
+
| FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=dedicated_worktree`, `allowedWorkspaceModes=[dedicated_worktree]`, requires `git`, `gh`, `rg` |
|
|
284
|
+
| PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
|
|
285
|
+
| SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
|
|
286
|
+
| AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `defaultWorkspaceMode=none`, `allowedWorkspaceModes=[none]` |
|
|
287
|
+
|
|
288
|
+
The example intentionally avoids prescribing `maxTurns`, output-token caps,
|
|
289
|
+
sampling temperature, or other model-behavior tuning. Keep those profile values
|
|
290
|
+
at the platform/runtime defaults until a repeated run gives enough evidence to
|
|
291
|
+
set a limit deliberately.
|
|
282
292
|
|
|
283
293
|
All four profiles set `sandbox.env.NODE_OPTIONS=--dns-result-order=ipv4first`
|
|
284
294
|
to match the live Ollama daemon smoke pattern and require `OLLAMA_API_KEY`.
|
|
@@ -299,10 +309,10 @@ export OLLAMA_API_KEY=...
|
|
|
299
309
|
npx @themoltnet/agent-daemon@latest poll \
|
|
300
310
|
--agent <agent-name> \
|
|
301
311
|
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \
|
|
302
|
-
--profile deep-review-freeze-
|
|
303
|
-
--profile deep-review-preflight-
|
|
304
|
-
--profile deep-review-specialist-
|
|
305
|
-
--profile deep-review-aggregate-
|
|
312
|
+
--profile deep-review-freeze-v1 \
|
|
313
|
+
--profile deep-review-preflight-v1 \
|
|
314
|
+
--profile deep-review-specialist-v1 \
|
|
315
|
+
--profile deep-review-aggregate-v1
|
|
306
316
|
```
|
|
307
317
|
|
|
308
318
|
The daemon host must have matching Pi model registry entries. This repo's
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-runtime-session-download', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'runtime session: download',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
attemptN: { value: '', validate: RED.validators.number() },
|
|
13
|
+
maxBytes: { value: 26214400, validate: RED.validators.number() },
|
|
14
|
+
},
|
|
15
|
+
inputs: 1,
|
|
16
|
+
outputs: 1,
|
|
17
|
+
icon: 'font-awesome/fa-download',
|
|
18
|
+
label: function () {
|
|
19
|
+
return this.name || 'runtime session: download';
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script type="text/html" data-template-name="moltnet-runtime-session-download">
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
27
|
+
<input type="text" id="node-input-name" />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
31
|
+
<input type="text" id="node-input-agent" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
35
|
+
<input
|
|
36
|
+
type="text"
|
|
37
|
+
id="node-input-taskId"
|
|
38
|
+
placeholder="msg.payload.taskId"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
43
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
47
|
+
><i class="fa fa-random"></i> Team override</label
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
type="checkbox"
|
|
51
|
+
id="node-input-allowMsgTeamOverride"
|
|
52
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
53
|
+
/>
|
|
54
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="form-row">
|
|
57
|
+
<label for="node-input-attemptN"
|
|
58
|
+
><i class="fa fa-history"></i> Attempt</label
|
|
59
|
+
>
|
|
60
|
+
<input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-row">
|
|
63
|
+
<label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
|
|
64
|
+
<input type="number" id="node-input-maxBytes" />
|
|
65
|
+
</div>
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<script type="text/html" data-help-name="moltnet-runtime-session-download">
|
|
69
|
+
<p>
|
|
70
|
+
Downloads durable runtime session content for a task attempt. Emits session
|
|
71
|
+
bytes as a Buffer on <code>msg.payload</code> and context metadata on
|
|
72
|
+
<code>msg.runtimeSession</code>. The local byte limit defaults to 25 MiB.
|
|
73
|
+
</p>
|
|
74
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
2
|
+
import { t as bool } from "./query-utils.js";
|
|
3
|
+
import { a as requireAttemptContext, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
|
|
4
|
+
//#region src/nodes/runtime-session-download.ts
|
|
5
|
+
var init = (RED) => {
|
|
6
|
+
function RuntimeSessionDownloadNode(def) {
|
|
7
|
+
RED.nodes.createNode(this, def);
|
|
8
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
9
|
+
this.on("input", (msg, send, done) => {
|
|
10
|
+
const run = async () => {
|
|
11
|
+
try {
|
|
12
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("runtime-session-download: no moltnet-agent configured");
|
|
13
|
+
const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
14
|
+
this.status({
|
|
15
|
+
fill: "blue",
|
|
16
|
+
shape: "dot",
|
|
17
|
+
text: "downloading…"
|
|
18
|
+
});
|
|
19
|
+
const body = await collectArtifactBody(await withAgent(agentNode, (agent) => agent.runtimeSessions.download({
|
|
20
|
+
taskId,
|
|
21
|
+
attemptN
|
|
22
|
+
}, { teamId })), resolveMaxBytes(def.maxBytes), "runtime-session-download");
|
|
23
|
+
const out = RED.util.cloneMessage({
|
|
24
|
+
...msg,
|
|
25
|
+
payload: void 0
|
|
26
|
+
});
|
|
27
|
+
out.payload = body;
|
|
28
|
+
out.taskId = taskId;
|
|
29
|
+
out.runtimeSession = {
|
|
30
|
+
taskId,
|
|
31
|
+
teamId,
|
|
32
|
+
attemptN,
|
|
33
|
+
sizeBytes: body.byteLength
|
|
34
|
+
};
|
|
35
|
+
this.status({
|
|
36
|
+
fill: "green",
|
|
37
|
+
shape: "dot",
|
|
38
|
+
text: `${body.byteLength} byte(s)`
|
|
39
|
+
});
|
|
40
|
+
send(out);
|
|
41
|
+
done();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
this.status({
|
|
44
|
+
fill: "red",
|
|
45
|
+
shape: "ring",
|
|
46
|
+
text: "error"
|
|
47
|
+
});
|
|
48
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
run();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
RED.nodes.registerType("moltnet-runtime-session-download", RuntimeSessionDownloadNode);
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { init as default };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-runtime-session-get', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'runtime session: get',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
attemptN: { value: '', validate: RED.validators.number() },
|
|
13
|
+
},
|
|
14
|
+
inputs: 1,
|
|
15
|
+
outputs: 1,
|
|
16
|
+
icon: 'font-awesome/fa-info-circle',
|
|
17
|
+
label: function () {
|
|
18
|
+
return this.name || 'runtime session: get';
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script type="text/html" data-template-name="moltnet-runtime-session-get">
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
26
|
+
<input type="text" id="node-input-name" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="form-row">
|
|
29
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
30
|
+
<input type="text" id="node-input-agent" />
|
|
31
|
+
</div>
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
id="node-input-taskId"
|
|
37
|
+
placeholder="msg.payload.taskId"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
42
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
43
|
+
</div>
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
46
|
+
><i class="fa fa-random"></i> Team override</label
|
|
47
|
+
>
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
id="node-input-allowMsgTeamOverride"
|
|
51
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
52
|
+
/>
|
|
53
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="node-input-attemptN"
|
|
57
|
+
><i class="fa fa-history"></i> Attempt</label
|
|
58
|
+
>
|
|
59
|
+
<input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
|
|
60
|
+
</div>
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<script type="text/html" data-help-name="moltnet-runtime-session-get">
|
|
64
|
+
<p>
|
|
65
|
+
Fetches durable runtime session metadata for a task attempt. Team ID is
|
|
66
|
+
taken from this node or the configured agent unless Team override is
|
|
67
|
+
enabled.
|
|
68
|
+
</p>
|
|
69
|
+
<p>
|
|
70
|
+
Emits the metadata or <code>null</code> on <code>msg.payload</code> and a
|
|
71
|
+
context envelope on <code>msg.runtimeSession</code>.
|
|
72
|
+
</p>
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
2
|
+
import { t as bool } from "./query-utils.js";
|
|
3
|
+
import { a as requireAttemptContext } from "./task-artifact-utils.js";
|
|
4
|
+
//#region src/nodes/runtime-session-get.ts
|
|
5
|
+
var init = (RED) => {
|
|
6
|
+
function RuntimeSessionGetNode(def) {
|
|
7
|
+
RED.nodes.createNode(this, def);
|
|
8
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
9
|
+
this.on("input", (msg, send, done) => {
|
|
10
|
+
const run = async () => {
|
|
11
|
+
try {
|
|
12
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("runtime-session-get: no moltnet-agent configured");
|
|
13
|
+
const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-get", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
14
|
+
this.status({
|
|
15
|
+
fill: "blue",
|
|
16
|
+
shape: "dot",
|
|
17
|
+
text: "loading…"
|
|
18
|
+
});
|
|
19
|
+
const session = await withAgent(agentNode, (agent) => agent.runtimeSessions.getForAttempt({
|
|
20
|
+
taskId,
|
|
21
|
+
attemptN
|
|
22
|
+
}, { teamId }));
|
|
23
|
+
const out = RED.util.cloneMessage(msg);
|
|
24
|
+
out.payload = session;
|
|
25
|
+
out.taskId = taskId;
|
|
26
|
+
out.runtimeSession = {
|
|
27
|
+
taskId,
|
|
28
|
+
teamId,
|
|
29
|
+
attemptN,
|
|
30
|
+
session
|
|
31
|
+
};
|
|
32
|
+
this.status({
|
|
33
|
+
fill: session ? "green" : "yellow",
|
|
34
|
+
shape: session ? "dot" : "ring",
|
|
35
|
+
text: session ? "found" : "not found"
|
|
36
|
+
});
|
|
37
|
+
send(out);
|
|
38
|
+
done();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
this.status({
|
|
41
|
+
fill: "red",
|
|
42
|
+
shape: "ring",
|
|
43
|
+
text: "error"
|
|
44
|
+
});
|
|
45
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
run();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
RED.nodes.registerType("moltnet-runtime-session-get", RuntimeSessionGetNode);
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { init as default };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-runtime-session-upload', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'runtime session: upload',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
attemptN: { value: '', validate: RED.validators.number() },
|
|
13
|
+
maxBytes: { value: 26214400, validate: RED.validators.number() },
|
|
14
|
+
sessionKind: { value: 'root' },
|
|
15
|
+
parentSessionId: { value: '' },
|
|
16
|
+
sourceSlotId: { value: '' },
|
|
17
|
+
sourceRuntimeProfileId: { value: '' },
|
|
18
|
+
},
|
|
19
|
+
inputs: 1,
|
|
20
|
+
outputs: 1,
|
|
21
|
+
icon: 'font-awesome/fa-upload',
|
|
22
|
+
label: function () {
|
|
23
|
+
return this.name || 'runtime session: upload';
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script type="text/html" data-template-name="moltnet-runtime-session-upload">
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
31
|
+
<input type="text" id="node-input-name" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
35
|
+
<input type="text" id="node-input-agent" />
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
id="node-input-taskId"
|
|
42
|
+
placeholder="msg.payload.taskId"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
47
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
51
|
+
><i class="fa fa-random"></i> Team override</label
|
|
52
|
+
>
|
|
53
|
+
<input
|
|
54
|
+
type="checkbox"
|
|
55
|
+
id="node-input-allowMsgTeamOverride"
|
|
56
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
57
|
+
/>
|
|
58
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-attemptN"
|
|
62
|
+
><i class="fa fa-history"></i> Attempt</label
|
|
63
|
+
>
|
|
64
|
+
<input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-sessionKind"
|
|
68
|
+
><i class="fa fa-code-fork"></i> Kind</label
|
|
69
|
+
>
|
|
70
|
+
<select id="node-input-sessionKind">
|
|
71
|
+
<option value="root">Root</option>
|
|
72
|
+
<option value="extend">Extend</option>
|
|
73
|
+
<option value="fork">Fork</option>
|
|
74
|
+
</select>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-parentSessionId"
|
|
78
|
+
><i class="fa fa-link"></i> Parent</label
|
|
79
|
+
>
|
|
80
|
+
<input
|
|
81
|
+
type="text"
|
|
82
|
+
id="node-input-parentSessionId"
|
|
83
|
+
placeholder="parent runtime session UUID"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="form-row">
|
|
87
|
+
<label for="node-input-sourceSlotId"
|
|
88
|
+
><i class="fa fa-map-pin"></i> Slot</label
|
|
89
|
+
>
|
|
90
|
+
<input type="text" id="node-input-sourceSlotId" placeholder="slot UUID" />
|
|
91
|
+
</div>
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-sourceRuntimeProfileId"
|
|
94
|
+
><i class="fa fa-cog"></i> Profile</label
|
|
95
|
+
>
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
id="node-input-sourceRuntimeProfileId"
|
|
99
|
+
placeholder="runtime profile UUID"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="form-row">
|
|
103
|
+
<label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
|
|
104
|
+
<input type="number" id="node-input-maxBytes" />
|
|
105
|
+
</div>
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<script type="text/html" data-help-name="moltnet-runtime-session-upload">
|
|
109
|
+
<p>
|
|
110
|
+
Uploads durable runtime session bytes for a task attempt. The body is read
|
|
111
|
+
from <code>msg.payload</code> when it is a string, Buffer, Uint8Array, or
|
|
112
|
+
ArrayBuffer. Object payloads may provide <code>content</code>,
|
|
113
|
+
<code>body</code>, or base64 <code>contentBase64</code>.
|
|
114
|
+
</p>
|
|
115
|
+
<p>
|
|
116
|
+
Session kind and optional parent/source ids can be supplied by node fields
|
|
117
|
+
or matching fields on <code>msg.payload</code>. Emits session metadata on
|
|
118
|
+
<code>msg.payload</code> and <code>msg.runtimeSession</code>.
|
|
119
|
+
</p>
|
|
120
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
2
|
+
import { n as compact, t as bool } from "./query-utils.js";
|
|
3
|
+
import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
|
|
4
|
+
//#region src/nodes/runtime-session-upload.ts
|
|
5
|
+
var init = (RED) => {
|
|
6
|
+
function RuntimeSessionUploadNode(def) {
|
|
7
|
+
RED.nodes.createNode(this, def);
|
|
8
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
9
|
+
this.on("input", (msg, send, done) => {
|
|
10
|
+
const run = async () => {
|
|
11
|
+
try {
|
|
12
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("runtime-session-upload: no moltnet-agent configured");
|
|
13
|
+
const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-upload", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
14
|
+
const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes), "runtime-session-upload");
|
|
15
|
+
const query = buildUploadQuery(def, msg);
|
|
16
|
+
this.status({
|
|
17
|
+
fill: "blue",
|
|
18
|
+
shape: "dot",
|
|
19
|
+
text: "uploading…"
|
|
20
|
+
});
|
|
21
|
+
const session = await withAgent(agentNode, (agent) => agent.runtimeSessions.upload({
|
|
22
|
+
taskId,
|
|
23
|
+
attemptN
|
|
24
|
+
}, body, query, { teamId }));
|
|
25
|
+
const out = RED.util.cloneMessage({
|
|
26
|
+
...msg,
|
|
27
|
+
payload: void 0
|
|
28
|
+
});
|
|
29
|
+
out.payload = session;
|
|
30
|
+
out.taskId = taskId;
|
|
31
|
+
out.runtimeSession = session;
|
|
32
|
+
this.status({
|
|
33
|
+
fill: "green",
|
|
34
|
+
shape: "dot",
|
|
35
|
+
text: session.sha256 ?? "uploaded"
|
|
36
|
+
});
|
|
37
|
+
send(out);
|
|
38
|
+
done();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
this.status({
|
|
41
|
+
fill: "red",
|
|
42
|
+
shape: "ring",
|
|
43
|
+
text: "error"
|
|
44
|
+
});
|
|
45
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
run();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
RED.nodes.registerType("moltnet-runtime-session-upload", RuntimeSessionUploadNode);
|
|
52
|
+
};
|
|
53
|
+
function buildUploadQuery(def, msg) {
|
|
54
|
+
return compact({
|
|
55
|
+
sessionKind: resolveField(msg, "sessionKind", def.sessionKind) ?? resolveField(msg, "session_kind", void 0) ?? "root",
|
|
56
|
+
parentSessionId: resolveField(msg, "parentSessionId", def.parentSessionId) ?? resolveField(msg, "parent_session_id", void 0),
|
|
57
|
+
sourceSlotId: resolveField(msg, "sourceSlotId", def.sourceSlotId) ?? resolveField(msg, "source_slot_id", void 0),
|
|
58
|
+
sourceRuntimeProfileId: resolveField(msg, "sourceRuntimeProfileId", def.sourceRuntimeProfileId) ?? resolveField(msg, "source_runtime_profile_id", void 0)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { init as default };
|
|
@@ -45,75 +45,75 @@ function resolveField(msg, name, configured) {
|
|
|
45
45
|
function resolveMaxBytes(configured) {
|
|
46
46
|
return positiveInt(configured) ?? 26214400;
|
|
47
47
|
}
|
|
48
|
-
function resolveUploadBody(msg, maxBytes) {
|
|
48
|
+
function resolveUploadBody(msg, maxBytes, nodeName = "task-artifact-upload") {
|
|
49
49
|
const payload = msg.payload;
|
|
50
|
-
if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes);
|
|
51
|
-
if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes);
|
|
50
|
+
if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes, nodeName);
|
|
51
|
+
if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes, nodeName);
|
|
52
52
|
if (payload instanceof ArrayBuffer) {
|
|
53
|
-
if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
53
|
+
if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
54
54
|
return new Uint8Array(payload);
|
|
55
55
|
}
|
|
56
56
|
if (typeof payload === "string") {
|
|
57
|
-
if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
57
|
+
if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
58
58
|
return new TextEncoder().encode(payload);
|
|
59
59
|
}
|
|
60
60
|
const record = payloadRecord(msg);
|
|
61
61
|
if (typeof record.contentBase64 === "string") {
|
|
62
62
|
const normalized = record.contentBase64.replace(/\s/g, "");
|
|
63
|
-
if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
63
|
+
if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
64
64
|
return Buffer.from(normalized, "base64");
|
|
65
65
|
}
|
|
66
66
|
const content = record.content ?? record.body;
|
|
67
|
-
if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes);
|
|
68
|
-
if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes);
|
|
67
|
+
if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes, nodeName);
|
|
68
|
+
if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes, nodeName);
|
|
69
69
|
if (content instanceof ArrayBuffer) {
|
|
70
|
-
if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
70
|
+
if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
71
71
|
return new Uint8Array(content);
|
|
72
72
|
}
|
|
73
73
|
if (typeof content === "string") {
|
|
74
|
-
if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
74
|
+
if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
75
75
|
return new TextEncoder().encode(content);
|
|
76
76
|
}
|
|
77
|
-
throw new Error(
|
|
77
|
+
throw new Error(`${nodeName}: payload content is required`);
|
|
78
78
|
}
|
|
79
|
-
async function collectArtifactBody(value, maxBytes) {
|
|
79
|
+
async function collectArtifactBody(value, maxBytes, nodeName = "task-artifact-download") {
|
|
80
80
|
const source = value && typeof value === "object" && "stream" in value ? value.stream : value;
|
|
81
81
|
if (Buffer.isBuffer(source)) {
|
|
82
|
-
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
82
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
83
83
|
return source;
|
|
84
84
|
}
|
|
85
85
|
if (source instanceof Uint8Array) {
|
|
86
|
-
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
86
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
87
87
|
return Buffer.from(source);
|
|
88
88
|
}
|
|
89
89
|
if (source instanceof ArrayBuffer) {
|
|
90
|
-
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
90
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
91
91
|
return Buffer.from(source);
|
|
92
92
|
}
|
|
93
93
|
if (typeof source === "string") {
|
|
94
|
-
if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes);
|
|
94
|
+
if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
95
95
|
return Buffer.from(source);
|
|
96
96
|
}
|
|
97
97
|
if (source instanceof Readable) {
|
|
98
98
|
const chunks = [];
|
|
99
99
|
let bytes = 0;
|
|
100
|
-
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
|
|
100
|
+
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes, nodeName);
|
|
101
101
|
return Buffer.concat(chunks);
|
|
102
102
|
}
|
|
103
103
|
if (source && typeof source === "object" && Symbol.asyncIterator in source) {
|
|
104
104
|
const chunks = [];
|
|
105
105
|
let bytes = 0;
|
|
106
|
-
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
|
|
106
|
+
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes, nodeName);
|
|
107
107
|
return Buffer.concat(chunks);
|
|
108
108
|
}
|
|
109
109
|
if (source && typeof source === "object" && "arrayBuffer" in source) {
|
|
110
110
|
const size = source.size;
|
|
111
|
-
if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes);
|
|
111
|
+
if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
112
112
|
const arrayBuffer = await source.arrayBuffer();
|
|
113
|
-
if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
113
|
+
if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
114
114
|
return Buffer.from(arrayBuffer);
|
|
115
115
|
}
|
|
116
|
-
throw new Error(
|
|
116
|
+
throw new Error(`${nodeName}: unsupported downloaded body`);
|
|
117
117
|
}
|
|
118
118
|
function toBuffer(value) {
|
|
119
119
|
if (Buffer.isBuffer(value)) return value;
|
|
@@ -126,14 +126,14 @@ function recordField(value, key) {
|
|
|
126
126
|
if (!value || typeof value !== "object") return void 0;
|
|
127
127
|
return value[key];
|
|
128
128
|
}
|
|
129
|
-
function enforceMaxBytes(value, maxBytes) {
|
|
130
|
-
if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
129
|
+
function enforceMaxBytes(value, maxBytes, nodeName) {
|
|
130
|
+
if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
|
|
131
131
|
return value;
|
|
132
132
|
}
|
|
133
|
-
function pushChunk(chunks, chunk, bytes, maxBytes) {
|
|
133
|
+
function pushChunk(chunks, chunk, bytes, maxBytes, nodeName) {
|
|
134
134
|
const next = toBuffer(chunk);
|
|
135
135
|
const total = bytes + next.byteLength;
|
|
136
|
-
if (total > maxBytes) throw tooLarge("download", maxBytes);
|
|
136
|
+
if (total > maxBytes) throw tooLarge("download", maxBytes, nodeName);
|
|
137
137
|
chunks.push(next);
|
|
138
138
|
return total;
|
|
139
139
|
}
|
|
@@ -142,8 +142,8 @@ function decodedBase64Length(value) {
|
|
|
142
142
|
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
143
143
|
return Math.floor(value.length * 3 / 4) - padding;
|
|
144
144
|
}
|
|
145
|
-
function tooLarge(operation, maxBytes) {
|
|
146
|
-
return /* @__PURE__ */ new Error(
|
|
145
|
+
function tooLarge(operation, maxBytes, nodeName) {
|
|
146
|
+
return /* @__PURE__ */ new Error(`${nodeName}: ${operation} body exceeds ${maxBytes} bytes`);
|
|
147
147
|
}
|
|
148
148
|
//#endregion
|
|
149
149
|
export { requireAttemptContext as a, resolveUploadBody as c, requireArtifactContext as i, payloadRecord as n, resolveField as o, recordField as r, resolveMaxBytes as s, collectArtifactBody as t };
|