@themoltnet/node-red-contrib-core 0.10.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 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 | Settings |
277
- | ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
278
- | FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=12000`, `maxTurns=60`, `maxBashTimeouts=5`, `defaultWorkspaceMode=dedicated_worktree`, `allowedWorkspaceModes=[dedicated_worktree]`, requires `git`, `gh`, `rg` |
279
- | PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=10000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
280
- | SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.15`, `maxOutputTokens=18000`, `maxTurns=24`, `maxBashTimeouts=3`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
281
- | AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `thinkingLevel=high`, `temperature=0.2`, `maxOutputTokens=24000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=none`, `allowedWorkspaceModes=[none]` |
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-ollama-qwen-coder-v1 \
303
- --profile deep-review-preflight-ollama-qwen-coder-v1 \
304
- --profile deep-review-specialist-ollama-qwen-coder-v1 \
305
- --profile deep-review-aggregate-ollama-kimi-v1
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,77 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-cancel', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task: cancel',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ reason: { value: '' },
11
+ skipMissing: { value: false },
12
+ ignoreErrors: { value: false },
13
+ },
14
+ inputs: 1,
15
+ outputs: 1,
16
+ icon: 'font-awesome/fa-ban',
17
+ label: function () {
18
+ return this.name || 'task: cancel';
19
+ },
20
+ });
21
+ </script>
22
+
23
+ <script type="text/html" data-template-name="moltnet-task-cancel">
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.taskId / msg.payload rows"
38
+ />
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-reason"><i class="fa fa-comment"></i> Reason</label>
42
+ <input type="text" id="node-input-reason" placeholder="workflow failed" />
43
+ </div>
44
+ <div class="form-row">
45
+ <label>&nbsp;</label>
46
+ <input
47
+ type="checkbox"
48
+ id="node-input-skipMissing"
49
+ style="display:inline-block; width:auto; vertical-align:top;"
50
+ />
51
+ <span>Pass through when no task id is available</span>
52
+ </div>
53
+ <div class="form-row">
54
+ <label>&nbsp;</label>
55
+ <input
56
+ type="checkbox"
57
+ id="node-input-ignoreErrors"
58
+ style="display:inline-block; width:auto; vertical-align:top;"
59
+ />
60
+ <span>Keep the original message if cancel fails</span>
61
+ </div>
62
+ </script>
63
+
64
+ <script type="text/html" data-help-name="moltnet-task-cancel">
65
+ <p>Cancels one or more MoltNet tasks.</p>
66
+ <p>
67
+ Task ids are resolved from <code>msg.taskId</code>,
68
+ <code>msg.taskIds</code>, <code>msg.payload.taskId</code>,
69
+ <code>msg.payload.id</code>, <code>msg.payload.failure.taskId</code>, an
70
+ array on <code>msg.payload</code>, or the configured Task ID.
71
+ </p>
72
+ <p>
73
+ The original <code>msg.payload</code> is preserved. Cancelled task rows are
74
+ written to <code>msg.cancelledTasks</code>; ignored cancel failures are
75
+ written to <code>msg.cancelErrors</code>.
76
+ </p>
77
+ </script>
@@ -0,0 +1,135 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ //#region src/nodes/task-cancel.ts
3
+ var TERMINAL_STATUSES = new Set([
4
+ "completed",
5
+ "failed",
6
+ "cancelled",
7
+ "expired"
8
+ ]);
9
+ var init = (RED) => {
10
+ function TaskCancelNode(def) {
11
+ RED.nodes.createNode(this, def);
12
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
13
+ this.on("input", (msg, send, done) => {
14
+ const run = async () => {
15
+ try {
16
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-cancel: no moltnet-agent configured");
17
+ const taskIds = resolveTaskIds(msg, def.taskId);
18
+ if (taskIds.length === 0) {
19
+ if (def.skipMissing) {
20
+ const out = RED.util.cloneMessage(msg);
21
+ out.cancelledTasks = [];
22
+ out.cancelErrors = [];
23
+ this.status({
24
+ fill: "yellow",
25
+ shape: "ring",
26
+ text: "no task id"
27
+ });
28
+ send(out);
29
+ done();
30
+ return;
31
+ }
32
+ throw new Error("task-cancel: taskId is required");
33
+ }
34
+ this.status({
35
+ fill: "blue",
36
+ shape: "dot",
37
+ text: `cancelling ${taskIds.length}`
38
+ });
39
+ const reason = resolveReason(msg, def.reason);
40
+ const cancelled = [];
41
+ const errors = [];
42
+ await withAgent(agentNode, async (agent) => {
43
+ for (const taskId of taskIds) try {
44
+ const task = await agent.tasks.cancel(taskId, { reason });
45
+ cancelled.push(task);
46
+ } catch (error) {
47
+ const message = errorMessage(error);
48
+ if (!def.ignoreErrors) throw error;
49
+ errors.push({
50
+ taskId,
51
+ message
52
+ });
53
+ }
54
+ });
55
+ const out = RED.util.cloneMessage(msg);
56
+ out.cancelledTasks = cancelled;
57
+ out.cancelErrors = errors;
58
+ if (cancelled.length === 1) out.cancelledTask = cancelled[0];
59
+ if (taskIds.length === 1) out.taskId = taskIds[0];
60
+ this.status({
61
+ fill: errors.length > 0 ? "yellow" : "green",
62
+ shape: errors.length > 0 ? "ring" : "dot",
63
+ text: errors.length > 0 ? `cancelled ${cancelled.length}, ${errors.length} error(s)` : `cancelled ${cancelled.length}`
64
+ });
65
+ send(out);
66
+ done();
67
+ } catch (err) {
68
+ this.status({
69
+ fill: "red",
70
+ shape: "ring",
71
+ text: "error"
72
+ });
73
+ done(err instanceof Error ? err : new Error(String(err)));
74
+ }
75
+ };
76
+ run();
77
+ });
78
+ }
79
+ RED.nodes.registerType("moltnet-task-cancel", TaskCancelNode);
80
+ };
81
+ function resolveTaskIds(msg, configured) {
82
+ const ids = /* @__PURE__ */ new Set();
83
+ addId(ids, msg.taskId);
84
+ addIds(ids, msg.taskIds);
85
+ collectFromPayload(ids, msg.payload);
86
+ addId(ids, configured);
87
+ return [...ids];
88
+ }
89
+ function collectFromPayload(ids, value) {
90
+ if (!value) return;
91
+ if (typeof value === "string") {
92
+ addId(ids, value);
93
+ return;
94
+ }
95
+ if (Array.isArray(value)) {
96
+ for (const item of value) collectFromPayload(ids, item);
97
+ return;
98
+ }
99
+ if (typeof value !== "object") return;
100
+ const record = value;
101
+ addId(ids, record.taskId);
102
+ addId(ids, record.id);
103
+ addIds(ids, record.taskIds);
104
+ addIds(ids, record.ids);
105
+ if (Array.isArray(record.tasks)) addIds(ids, record.tasks);
106
+ if (record.failure && typeof record.failure === "object") addId(ids, record.failure.taskId);
107
+ if (typeof record.status === "string" && TERMINAL_STATUSES.has(record.status)) return;
108
+ }
109
+ function addIds(ids, value) {
110
+ if (!Array.isArray(value)) return;
111
+ for (const item of value) collectFromPayload(ids, item);
112
+ }
113
+ function addId(ids, value) {
114
+ if (typeof value === "string" && value.trim()) ids.add(value.trim());
115
+ }
116
+ function resolveReason(msg, configured) {
117
+ if (configured && configured.trim()) return configured.trim();
118
+ const payload = msg.payload;
119
+ if (payload && typeof payload === "object") {
120
+ const p = payload;
121
+ if (p.failure && typeof p.failure === "object") {
122
+ const error = p.failure.error;
123
+ if (error && typeof error === "object") {
124
+ const message = error.message;
125
+ if (typeof message === "string" && message.trim()) return `workflow failed: ${message.trim()}`;
126
+ }
127
+ }
128
+ }
129
+ return "workflow failed";
130
+ }
131
+ function errorMessage(error) {
132
+ return error instanceof Error ? error.message : String(error);
133
+ }
134
+ //#endregion
135
+ export { init as default };
@@ -16,6 +16,9 @@
16
16
  agent: { value: '', type: 'moltnet-agent', required: true },
17
17
  taskId: { value: '' },
18
18
  pollIntervalSec: { value: 5, validate: RED.validators.number() },
19
+ // Legacy field used by older example flows. Keeping it in defaults lets
20
+ // the editor migrate existing nodes instead of showing them as broken.
21
+ intervalMs: { value: undefined, required: false },
19
22
  timeoutSec: { value: 1800, validate: RED.validators.number() },
20
23
  tail: { value: false },
21
24
  kinds: { value: '' },
@@ -28,6 +31,14 @@
28
31
  return this.name || 'task: wait';
29
32
  },
30
33
  oneditprepare: function () {
34
+ if (
35
+ (this.pollIntervalSec === undefined ||
36
+ this.pollIntervalSec === null ||
37
+ this.pollIntervalSec === '') &&
38
+ typeof this.intervalMs === 'number'
39
+ ) {
40
+ $('#node-input-pollIntervalSec').val(this.intervalMs / 1000);
41
+ }
31
42
  // Render one checkbox per message kind. `kinds` is stored as a
32
43
  // comma-separated string (the runtime contract); empty = all kinds.
33
44
  const selected = (this.kinds || '')
@@ -56,6 +67,7 @@
56
67
  toggle();
57
68
  },
58
69
  oneditsave: function () {
70
+ this.intervalMs = undefined;
59
71
  const all = $('.mn-kind').length;
60
72
  const checked = $('.mn-kind:checked')
61
73
  .map(function () {
@@ -7,7 +7,8 @@ var init = (RED) => {
7
7
  function TaskWaitNode(def) {
8
8
  RED.nodes.createNode(this, def);
9
9
  const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
10
- const pollMs = Math.max(1, def.pollIntervalSec || DEFAULT_POLL_SEC) * 1e3;
10
+ const pollSec = def.pollIntervalSec ?? (typeof def.intervalMs === "number" ? def.intervalMs / 1e3 : void 0);
11
+ const pollMs = Math.max(1, pollSec || DEFAULT_POLL_SEC) * 1e3;
11
12
  const timeoutMs = (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) > 0 ? (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3 : 0;
12
13
  const kindAllow = parseKinds(def.kinds);
13
14
  const active = /* @__PURE__ */ new Map();
@@ -24,6 +25,7 @@ var init = (RED) => {
24
25
  if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-wait: no moltnet-agent configured");
25
26
  const taskId = resolveTaskId(msg, def.taskId);
26
27
  if (!taskId) throw new Error("task-wait: taskId is required");
28
+ msg.taskId = taskId;
27
29
  taskIdForStatus = taskId;
28
30
  label = describeWait(taskId, msg, label);
29
31
  active.set(taskId, label);
@@ -61,6 +63,7 @@ var init = (RED) => {
61
63
  ...snapshot,
62
64
  correlationId
63
65
  } : snapshot;
66
+ resultMsg.taskId = taskId;
64
67
  if (correlationId) resultMsg.correlationId = correlationId;
65
68
  active.delete(taskId);
66
69
  this.status({
@@ -34,6 +34,7 @@ var init = (RED) => {
34
34
  const task = await withAgent(agentNode, (agent) => agent.tasks.create(createBody, { teamId }));
35
35
  const out = RED.util.cloneMessage(msg);
36
36
  if (correlationId) out.correlationId = correlationId;
37
+ out.taskId = task.id;
37
38
  out.payload = task;
38
39
  active.delete(invocationId);
39
40
  this.status({