@themoltnet/node-red-contrib-core 0.6.0 → 0.7.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 +97 -8
- package/dist/nodes/agent-call.js +19 -0
- package/dist/nodes/agent.js +3 -0
- package/dist/nodes/entries-search.js +2 -1
- package/dist/nodes/runtime-profile.js +2 -1
- package/dist/nodes/src.js +18 -0
- package/dist/nodes/task-artifact-download.js +3 -2
- package/dist/nodes/task-artifact-upload.js +3 -2
- package/dist/nodes/task-artifacts-list.js +2 -2
- package/dist/nodes/task-get.js +2 -2
- package/dist/nodes/task-wait.js +65 -11
- package/dist/nodes/tasks-create.js +46 -5
- package/dist/nodes/tasks-list.js +2 -2
- package/dist/nodes/workflow-status.js +2 -2
- package/examples/deep-review-freeform.flow.json +780 -0
- package/package.json +26 -3
package/README.md
CHANGED
|
@@ -220,6 +220,74 @@ Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
|
|
|
220
220
|
included but blank: leave them blank for any eligible daemon to claim both
|
|
221
221
|
tasks, or set producer/judge profile IDs and run one daemon per profile.
|
|
222
222
|
|
|
223
|
+
## Freeform deep review workflow
|
|
224
|
+
|
|
225
|
+
[`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
|
|
226
|
+
is a freeform-only code review workflow inspired by the `deep-review` agent
|
|
227
|
+
skill. It keeps MoltNet generic: the workflow is opinionated, but every agent
|
|
228
|
+
step is a `freeform` task.
|
|
229
|
+
|
|
230
|
+
The flow shape is:
|
|
231
|
+
|
|
232
|
+
`inject` -> `entries: search` -> **FREEZE** -> **PREFLIGHT** -> stock `switch`
|
|
233
|
+
on `PROCEED | PIVOT | ASK`. `PIVOT` becomes a design-review publish task.
|
|
234
|
+
`PROCEED` fans out one specialist task per review dimension, so multiple
|
|
235
|
+
daemons serving the specialist profile can claim work in parallel. Each
|
|
236
|
+
completed task reads its `specialist-findings` artifact, appends it to the
|
|
237
|
+
correlation-scoped `flow` state at `deepReview:<correlationId>`, and a small
|
|
238
|
+
completion gate runs **AGGREGATE** exactly once when all expected specialist
|
|
239
|
+
results have arrived. Tail events from wait nodes use link nodes into one
|
|
240
|
+
observability/debug lane so the main review path stays readable.
|
|
241
|
+
|
|
242
|
+
The **FREEZE** task asks the agent to create a review bundle artifact containing
|
|
243
|
+
target metadata, changed files, stats, and patch/file-list CIDs. The flow
|
|
244
|
+
keeps control flow deterministic by parsing the compact inline JSON
|
|
245
|
+
(`artifact kind=json`, `title=review-bundle`) with `task: read`, then uploading
|
|
246
|
+
that parsed bundle through `task artifact: upload` before using
|
|
247
|
+
`task artifacts: list` and `task artifact: download` to inspect the persistent
|
|
248
|
+
CID-backed artifact path.
|
|
249
|
+
|
|
250
|
+
The example also includes runtime-profile config nodes, so each stage can be
|
|
251
|
+
routed to a daemon serving the right Ollama Cloud model. These profiles were
|
|
252
|
+
created through the Runtime Profiles API for team
|
|
253
|
+
`6743b4b1-6b93-46e2-a048-19490f04f91a`; recreate them or replace the IDs when
|
|
254
|
+
running the flow in another team.
|
|
255
|
+
|
|
256
|
+
| Stage | Profile ID | Provider / model | Settings |
|
|
257
|
+
| ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
258
|
+
| 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` |
|
|
259
|
+
| 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` |
|
|
260
|
+
| 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` |
|
|
261
|
+
| 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]` |
|
|
262
|
+
|
|
263
|
+
All four profiles set `sandbox.env.NODE_OPTIONS=--dns-result-order=ipv4first`
|
|
264
|
+
to match the live Ollama daemon smoke pattern and require `OLLAMA_API_KEY`.
|
|
265
|
+
Repo-aware profiles deny `.env`, `.env.local`, and `.moltnet` through VFS
|
|
266
|
+
shadowing with `shadowMode=deny` and `hostExec.autoApprove=false`. FREEZE uses a
|
|
267
|
+
dedicated worktree and GitHub snapshot hosts `api.github.com`, `github.com`,
|
|
268
|
+
`objects.githubusercontent.com`, `codeload.github.com`, and
|
|
269
|
+
`raw.githubusercontent.com`; PREFLIGHT and SPECIALIST use `shared_mount` so they
|
|
270
|
+
can inspect code around the frozen bundle without mutating it. AGGREGATE stays
|
|
271
|
+
repo-free with workspace `none`.
|
|
272
|
+
|
|
273
|
+
Run one daemon per profile, or one daemon process configured with all four
|
|
274
|
+
profiles. Repeated `--profile` flags declare the daemon's priority order:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
export OLLAMA_API_KEY=...
|
|
278
|
+
|
|
279
|
+
npx @themoltnet/agent-daemon@latest poll \
|
|
280
|
+
--agent <agent-name> \
|
|
281
|
+
--team 6743b4b1-6b93-46e2-a048-19490f04f91a \
|
|
282
|
+
--profile deep-review-freeze-ollama-qwen-coder-v1 \
|
|
283
|
+
--profile deep-review-preflight-ollama-qwen-coder-v1 \
|
|
284
|
+
--profile deep-review-specialist-ollama-qwen-coder-v1 \
|
|
285
|
+
--profile deep-review-aggregate-ollama-kimi-v1
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The daemon host must have matching Pi model registry entries. This repo's
|
|
289
|
+
`.pi/models.json` already declares the Ollama Cloud provider and these models.
|
|
290
|
+
|
|
223
291
|
## Reproducing the issue-lifecycle shape
|
|
224
292
|
|
|
225
293
|
[`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json)
|
|
@@ -276,20 +344,41 @@ Three layers, increasing fidelity:
|
|
|
276
344
|
## Run Node-RED with these nodes (one command)
|
|
277
345
|
|
|
278
346
|
```bash
|
|
279
|
-
pnpm
|
|
280
|
-
PORT=1881 pnpm
|
|
347
|
+
pnpm exec nx run @themoltnet/node-red-contrib-core:dev # → http://localhost:1880
|
|
348
|
+
PORT=1881 pnpm exec nx run @themoltnet/node-red-contrib-core:dev
|
|
281
349
|
```
|
|
282
350
|
|
|
283
|
-
`
|
|
284
|
-
|
|
351
|
+
The Nx `dev` target builds this package and `@themoltnet/node-red-theme` through
|
|
352
|
+
target `dependsOn`, then `scripts/dev.mjs` links this package into a local
|
|
353
|
+
`.node-red-dev/` userDir (gitignored) and starts Node-RED 5 (fetched via `npx`
|
|
285
354
|
on first run). The MoltNet nodes appear under the **moltnet** palette category.
|
|
286
|
-
After editing a node, stop (Ctrl-C) and re-run — Node-RED does not
|
|
287
|
-
custom nodes.
|
|
355
|
+
After editing a node or the theme, stop (Ctrl-C) and re-run — Node-RED does not
|
|
356
|
+
hot-reload custom nodes.
|
|
357
|
+
|
|
358
|
+
To skin a local or hosted Node-RED instance with the MoltNet editor theme,
|
|
359
|
+
install `@themoltnet/node-red-theme` next to Node-RED and set:
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
import { moltnetEditorTheme } from '@themoltnet/node-red-theme';
|
|
363
|
+
|
|
364
|
+
export default {
|
|
365
|
+
editorTheme: moltnetEditorTheme({ title: 'MoltNet Flow Studio' }),
|
|
366
|
+
};
|
|
367
|
+
```
|
|
288
368
|
|
|
289
369
|
Open the editor, drag in `agent` + the task nodes, or import
|
|
290
370
|
[`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json) or
|
|
291
|
-
[`examples/cockpit.flow.json`](./examples/cockpit.flow.json)
|
|
292
|
-
|
|
371
|
+
[`examples/cockpit.flow.json`](./examples/cockpit.flow.json) or
|
|
372
|
+
[`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json),
|
|
373
|
+
then fill the agent's `clientId`/`clientSecret`.
|
|
374
|
+
|
|
375
|
+
If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
|
|
376
|
+
`Cannot read properties of undefined (reading 'getSessions')`, the browser is
|
|
377
|
+
usually sending a stale `auth-tokens*` localStorage value from an older
|
|
378
|
+
authenticated editor on the same origin. Clear site data for
|
|
379
|
+
`http://localhost:1880`, remove localStorage keys beginning with `auth-tokens`,
|
|
380
|
+
or restart this dev harness on a fresh port such as
|
|
381
|
+
`PORT=1881 pnpm exec nx run @themoltnet/node-red-contrib-core:dev`.
|
|
293
382
|
|
|
294
383
|
<details>
|
|
295
384
|
<summary>Manual harness (if you prefer to drive it yourself)</summary>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/nodes/agent-call.ts
|
|
2
|
+
async function withAgent(agentNode, operation) {
|
|
3
|
+
try {
|
|
4
|
+
return await operation(await agentNode.getAgent());
|
|
5
|
+
} catch (err) {
|
|
6
|
+
if (!isUnauthorizedError(err)) throw err;
|
|
7
|
+
agentNode.resetAgent();
|
|
8
|
+
return operation(await agentNode.getAgent());
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function isUnauthorizedError(err) {
|
|
12
|
+
if (!err || typeof err !== "object") return false;
|
|
13
|
+
const record = err;
|
|
14
|
+
if (record.statusCode === 401 || record.status === 401) return true;
|
|
15
|
+
if (record.code === "https://themolt.net/problems/unauthorized") return true;
|
|
16
|
+
return typeof record.message === "string" && /\bunauthorized\b/i.test(record.message) && /\btoken\b/i.test(record.message);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { withAgent as t };
|
package/dist/nodes/agent.js
CHANGED
|
@@ -8,6 +8,9 @@ var init = (RED) => {
|
|
|
8
8
|
this.teamId = def.teamId?.trim() || void 0;
|
|
9
9
|
this.diaryId = def.diaryId?.trim() || void 0;
|
|
10
10
|
let agentPromise = null;
|
|
11
|
+
this.resetAgent = function resetAgent() {
|
|
12
|
+
agentPromise = null;
|
|
13
|
+
};
|
|
11
14
|
this.getAgent = function getAgent() {
|
|
12
15
|
const clientSecret = this.credentials?.clientSecret;
|
|
13
16
|
if (!this.clientId || !clientSecret) return Promise.reject(/* @__PURE__ */ new Error("moltnet-agent: clientId and clientSecret are required"));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { a as nonEmpty, c as positiveInt, i as finiteNumber, n as compact, o as nonNegativeInt, r as csv, s as normalizeAliases, t as bool } from "./query-utils.js";
|
|
2
3
|
//#region src/nodes/entries-search.ts
|
|
3
4
|
var init = (RED) => {
|
|
@@ -15,7 +16,7 @@ var init = (RED) => {
|
|
|
15
16
|
shape: "dot",
|
|
16
17
|
text: "searching…"
|
|
17
18
|
});
|
|
18
|
-
const result = await (
|
|
19
|
+
const result = await withAgent(agentNode, (agent) => agent.entries.search(body));
|
|
19
20
|
const out = RED.util.cloneMessage(msg);
|
|
20
21
|
out.payload = result.results;
|
|
21
22
|
out.entries = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
//#region src/nodes/runtime-profile.ts
|
|
2
3
|
var init = (RED) => {
|
|
3
4
|
function MoltnetRuntimeProfileNode(def) {
|
|
@@ -19,7 +20,7 @@ var init = (RED) => {
|
|
|
19
20
|
});
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
|
-
const { items } = await (
|
|
23
|
+
const { items } = await withAgent(agentNode, (agent) => agent.runtimeProfiles.list(agentNode.teamId ? { teamId: agentNode.teamId } : void 0));
|
|
23
24
|
res.json({ profiles: items.map((p) => ({
|
|
24
25
|
id: p.id,
|
|
25
26
|
name: p.name,
|
package/dist/nodes/src.js
CHANGED
|
@@ -2836,6 +2836,15 @@ function unwrapResult(result) {
|
|
|
2836
2836
|
networkError.stack = error.stack;
|
|
2837
2837
|
throw networkError;
|
|
2838
2838
|
}
|
|
2839
|
+
const responseSummary = summarizeResponse(result.response);
|
|
2840
|
+
if (responseSummary) {
|
|
2841
|
+
const detail = stringifyUnknown(error);
|
|
2842
|
+
throw new MoltNetError(`MoltNet API request failed with HTTP ${responseSummary.status} ${responseSummary.statusText}: ${detail}`, {
|
|
2843
|
+
code: `HTTP_${responseSummary.status}`,
|
|
2844
|
+
detail,
|
|
2845
|
+
statusCode: responseSummary.status
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2839
2848
|
throw new MoltNetError(`Unexpected error from MoltNet API: ${stringifyUnknown(error)}`, { code: "UNKNOWN" });
|
|
2840
2849
|
}
|
|
2841
2850
|
if (result.data === void 0) throw new MoltNetError("Unexpected empty response from MoltNet API", { code: "EMPTY_RESPONSE" });
|
|
@@ -2853,6 +2862,15 @@ function stringifyUnknown(value) {
|
|
|
2853
2862
|
return String(value);
|
|
2854
2863
|
}
|
|
2855
2864
|
}
|
|
2865
|
+
function summarizeResponse(response) {
|
|
2866
|
+
if (!response || typeof response !== "object") return null;
|
|
2867
|
+
const candidate = response;
|
|
2868
|
+
if (typeof candidate.status !== "number") return null;
|
|
2869
|
+
return {
|
|
2870
|
+
status: candidate.status,
|
|
2871
|
+
statusText: typeof candidate.statusText === "string" && candidate.statusText ? candidate.statusText : "Error"
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2856
2874
|
function unwrapRequired(result, message, code) {
|
|
2857
2875
|
if (result.error || !result.data) throw new MoltNetError(message, { code });
|
|
2858
2876
|
return result.data;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { a as nonEmpty, t as bool } from "./query-utils.js";
|
|
2
3
|
import { a as requireAttemptContext, n as payloadRecord, r as recordField, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
|
|
3
4
|
//#region src/nodes/task-artifact-download.ts
|
|
@@ -17,11 +18,11 @@ var init = (RED) => {
|
|
|
17
18
|
shape: "dot",
|
|
18
19
|
text: "downloading…"
|
|
19
20
|
});
|
|
20
|
-
const result = await (
|
|
21
|
+
const result = await withAgent(agentNode, (agent) => agent.tasks.artifacts.download({
|
|
21
22
|
taskId,
|
|
22
23
|
attemptN,
|
|
23
24
|
cid
|
|
24
|
-
}, { teamId });
|
|
25
|
+
}, { teamId }));
|
|
25
26
|
const body = await collectArtifactBody(result, resolveMaxBytes(def.maxBytes));
|
|
26
27
|
const out = RED.util.cloneMessage({
|
|
27
28
|
...msg,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { t as bool } from "./query-utils.js";
|
|
2
3
|
import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
|
|
3
4
|
//#region src/nodes/task-artifact-upload.ts
|
|
@@ -17,10 +18,10 @@ var init = (RED) => {
|
|
|
17
18
|
shape: "dot",
|
|
18
19
|
text: "uploading…"
|
|
19
20
|
});
|
|
20
|
-
const artifact = await (
|
|
21
|
+
const artifact = await withAgent(agentNode, (agent) => agent.tasks.artifacts.upload({
|
|
21
22
|
taskId,
|
|
22
23
|
attemptN
|
|
23
|
-
}, body, query, { teamId });
|
|
24
|
+
}, body, query, { teamId }));
|
|
24
25
|
const out = RED.util.cloneMessage({
|
|
25
26
|
...msg,
|
|
26
27
|
payload: void 0
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { a as nonEmpty, c as positiveInt, n as compact, t as bool } from "./query-utils.js";
|
|
2
3
|
import { i as requireArtifactContext, n as payloadRecord } from "./task-artifact-utils.js";
|
|
3
4
|
//#region src/nodes/task-artifacts-list.ts
|
|
@@ -15,9 +16,8 @@ var init = (RED) => {
|
|
|
15
16
|
shape: "dot",
|
|
16
17
|
text: "loading…"
|
|
17
18
|
});
|
|
18
|
-
const agent = await agentNode.getAgent();
|
|
19
19
|
const query = buildQuery(def, msg);
|
|
20
|
-
const page = await agent.tasks.artifacts.listPage(taskId, query, { teamId });
|
|
20
|
+
const page = await withAgent(agentNode, (agent) => agent.tasks.artifacts.listPage(taskId, query, { teamId }));
|
|
21
21
|
const out = RED.util.cloneMessage(msg);
|
|
22
22
|
out.payload = page.artifacts;
|
|
23
23
|
out.taskId = taskId;
|
package/dist/nodes/task-get.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { t as buildTaskSnapshot } from "./task-snapshot.js";
|
|
2
3
|
//#region src/nodes/task-get.ts
|
|
3
4
|
var init = (RED) => {
|
|
@@ -15,8 +16,7 @@ var init = (RED) => {
|
|
|
15
16
|
shape: "dot",
|
|
16
17
|
text: "loading…"
|
|
17
18
|
});
|
|
18
|
-
const
|
|
19
|
-
const [task, attempts] = await Promise.all([agent.tasks.get(taskId), agent.tasks.listAttempts(taskId)]);
|
|
19
|
+
const [task, attempts] = await withAgent(agentNode, (agent) => Promise.all([agent.tasks.get(taskId), agent.tasks.listAttempts(taskId)]));
|
|
20
20
|
const snapshot = buildTaskSnapshot(task, attempts);
|
|
21
21
|
const out = RED.util.cloneMessage(msg);
|
|
22
22
|
out.payload = snapshot;
|
package/dist/nodes/task-wait.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { n as isTerminalTaskStatus, t as buildTaskSnapshot } from "./task-snapshot.js";
|
|
2
3
|
//#region src/nodes/task-wait.ts
|
|
3
4
|
var DEFAULT_POLL_SEC = 5;
|
|
@@ -9,48 +10,63 @@ var init = (RED) => {
|
|
|
9
10
|
const pollMs = Math.max(1, def.pollIntervalSec || DEFAULT_POLL_SEC) * 1e3;
|
|
10
11
|
const timeoutMs = (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) > 0 ? (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3 : 0;
|
|
11
12
|
const kindAllow = parseKinds(def.kinds);
|
|
13
|
+
const active = /* @__PURE__ */ new Map();
|
|
12
14
|
const pending = /* @__PURE__ */ new Set();
|
|
13
15
|
this.on("close", () => {
|
|
14
16
|
for (const t of pending) clearTimeout(t);
|
|
15
17
|
pending.clear();
|
|
16
18
|
});
|
|
17
19
|
this.on("input", (msg, send, done) => {
|
|
20
|
+
let taskIdForStatus;
|
|
21
|
+
let label = describeMessage(msg);
|
|
18
22
|
const run = async () => {
|
|
19
23
|
try {
|
|
20
24
|
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-wait: no moltnet-agent configured");
|
|
21
25
|
const taskId = resolveTaskId(msg, def.taskId);
|
|
22
26
|
if (!taskId) throw new Error("task-wait: taskId is required");
|
|
23
|
-
|
|
27
|
+
taskIdForStatus = taskId;
|
|
28
|
+
label = describeWait(taskId, msg, label);
|
|
29
|
+
active.set(taskId, label);
|
|
30
|
+
const correlationId = resolveCorrelationId(msg);
|
|
24
31
|
const startedAt = Date.now();
|
|
25
32
|
let afterSeq;
|
|
26
33
|
let polls = 0;
|
|
27
34
|
this.status({
|
|
28
35
|
fill: "blue",
|
|
29
36
|
shape: "dot",
|
|
30
|
-
text: "waiting
|
|
37
|
+
text: statusText("waiting", label, active.size)
|
|
31
38
|
});
|
|
32
39
|
for (;;) {
|
|
33
|
-
if (def.tail) afterSeq = await drainMessages({
|
|
40
|
+
if (def.tail) afterSeq = await withAgent(agentNode, (agent) => drainMessages({
|
|
34
41
|
agent,
|
|
35
42
|
taskId,
|
|
36
43
|
afterSeq,
|
|
37
44
|
kindAllow,
|
|
38
45
|
emit: (m) => {
|
|
39
46
|
const tailMsg = RED.util.cloneMessage(msg);
|
|
40
|
-
tailMsg.payload =
|
|
47
|
+
tailMsg.payload = correlationId ? {
|
|
48
|
+
...m,
|
|
49
|
+
correlationId
|
|
50
|
+
} : m;
|
|
41
51
|
tailMsg.taskId = taskId;
|
|
52
|
+
if (correlationId) tailMsg.correlationId = correlationId;
|
|
42
53
|
send([tailMsg, null]);
|
|
43
54
|
}
|
|
44
|
-
});
|
|
45
|
-
const task = await agent.tasks.get(taskId);
|
|
55
|
+
}));
|
|
56
|
+
const task = await withAgent(agentNode, (agent) => agent.tasks.get(taskId));
|
|
46
57
|
if (isTerminalTaskStatus(task.status)) {
|
|
47
|
-
const snapshot = buildTaskSnapshot(task, await agent.tasks.listAttempts(taskId));
|
|
58
|
+
const snapshot = buildTaskSnapshot(task, await withAgent(agentNode, (agent) => agent.tasks.listAttempts(taskId)));
|
|
48
59
|
const resultMsg = RED.util.cloneMessage(msg);
|
|
49
|
-
resultMsg.payload =
|
|
60
|
+
resultMsg.payload = correlationId ? {
|
|
61
|
+
...snapshot,
|
|
62
|
+
correlationId
|
|
63
|
+
} : snapshot;
|
|
64
|
+
if (correlationId) resultMsg.correlationId = correlationId;
|
|
65
|
+
active.delete(taskId);
|
|
50
66
|
this.status({
|
|
51
67
|
fill: snapshot.accepted ? "green" : "red",
|
|
52
68
|
shape: "dot",
|
|
53
|
-
text: `${snapshot.status}${snapshot.accepted ? "
|
|
69
|
+
text: statusText(`${snapshot.status}${snapshot.accepted ? " ok" : ""}`, label, active.size)
|
|
54
70
|
});
|
|
55
71
|
send([null, resultMsg]);
|
|
56
72
|
done();
|
|
@@ -61,15 +77,16 @@ var init = (RED) => {
|
|
|
61
77
|
this.status({
|
|
62
78
|
fill: "blue",
|
|
63
79
|
shape: "ring",
|
|
64
|
-
text: `${task.status}
|
|
80
|
+
text: statusText(`${task.status} ${polls}x`, label, active.size)
|
|
65
81
|
});
|
|
66
82
|
await sleep(pollMs, pending);
|
|
67
83
|
}
|
|
68
84
|
} catch (err) {
|
|
85
|
+
if (taskIdForStatus) active.delete(taskIdForStatus);
|
|
69
86
|
this.status({
|
|
70
87
|
fill: "red",
|
|
71
88
|
shape: "ring",
|
|
72
|
-
text: "error"
|
|
89
|
+
text: statusText("error", label, active.size)
|
|
73
90
|
});
|
|
74
91
|
done(err instanceof Error ? err : new Error(String(err)));
|
|
75
92
|
}
|
|
@@ -120,6 +137,43 @@ function resolveTaskId(msg, configured) {
|
|
|
120
137
|
}
|
|
121
138
|
return configured && configured.length > 0 ? configured : void 0;
|
|
122
139
|
}
|
|
140
|
+
function resolveCorrelationId(msg) {
|
|
141
|
+
if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
|
|
142
|
+
const payload = msg.payload;
|
|
143
|
+
if (payload && typeof payload === "object") {
|
|
144
|
+
const p = payload;
|
|
145
|
+
if (typeof p.correlationId === "string" && p.correlationId) return p.correlationId;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function describeMessage(msg) {
|
|
149
|
+
if (typeof msg.reviewDimension === "string" && msg.reviewDimension) return msg.reviewDimension;
|
|
150
|
+
const payload = msg.payload;
|
|
151
|
+
if (payload && typeof payload === "object") {
|
|
152
|
+
const p = payload;
|
|
153
|
+
if (typeof p.dimension === "string" && p.dimension) return p.dimension;
|
|
154
|
+
if (typeof p.title === "string" && p.title) return p.title;
|
|
155
|
+
}
|
|
156
|
+
return "task";
|
|
157
|
+
}
|
|
158
|
+
function describeWait(taskId, msg, fallback) {
|
|
159
|
+
if (fallback !== "task") return fallback;
|
|
160
|
+
const payload = msg.payload;
|
|
161
|
+
if (payload && typeof payload === "object") {
|
|
162
|
+
const p = payload;
|
|
163
|
+
if (typeof p.title === "string" && p.title) return p.title;
|
|
164
|
+
}
|
|
165
|
+
return shortId(taskId);
|
|
166
|
+
}
|
|
167
|
+
function statusText(action, label, activeCount) {
|
|
168
|
+
const suffix = activeCount > 0 ? ` · ${activeCount} active` : "";
|
|
169
|
+
return `${action} · ${truncate(label, 34)}${suffix}`;
|
|
170
|
+
}
|
|
171
|
+
function shortId(id) {
|
|
172
|
+
return id.slice(0, 8);
|
|
173
|
+
}
|
|
174
|
+
function truncate(value, max) {
|
|
175
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
176
|
+
}
|
|
123
177
|
function sleep(ms, pending) {
|
|
124
178
|
return new Promise((resolve) => {
|
|
125
179
|
const t = setTimeout(() => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
//#region src/nodes/tasks-create.ts
|
|
3
4
|
var init = (RED) => {
|
|
@@ -5,18 +6,24 @@ var init = (RED) => {
|
|
|
5
6
|
RED.nodes.createNode(this, def);
|
|
6
7
|
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
7
8
|
const profileNode = def.runtimeProfile ? RED.nodes.getNode(def.runtimeProfile) : null;
|
|
9
|
+
const active = /* @__PURE__ */ new Map();
|
|
10
|
+
let nextInvocationId = 0;
|
|
8
11
|
this.on("input", (msg, send, done) => {
|
|
12
|
+
const invocationId = ++nextInvocationId;
|
|
13
|
+
let label = describeMessage(msg);
|
|
9
14
|
const run = async () => {
|
|
10
15
|
try {
|
|
11
16
|
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("tasks-create: no moltnet-agent configured");
|
|
17
|
+
active.set(invocationId, label);
|
|
12
18
|
this.status({
|
|
13
19
|
fill: "blue",
|
|
14
20
|
shape: "dot",
|
|
15
|
-
text: "creating
|
|
21
|
+
text: statusText("creating", label, active.size)
|
|
16
22
|
});
|
|
17
|
-
const agent = await agentNode.getAgent();
|
|
18
23
|
const base = msg.payload && typeof msg.payload === "object" ? { ...msg.payload } : {};
|
|
19
24
|
if (!base.taskType) base.taskType = "freeform";
|
|
25
|
+
label = describeTaskBody(base, label);
|
|
26
|
+
active.set(invocationId, label);
|
|
20
27
|
if (!base.allowedProfiles && profileNode?.profileId) base.allowedProfiles = [{ profileId: profileNode.profileId }];
|
|
21
28
|
if (base.maxAttempts === void 0 && typeof def.maxAttempts === "number") base.maxAttempts = def.maxAttempts;
|
|
22
29
|
if (!base.teamId && agentNode.teamId) base.teamId = agentNode.teamId;
|
|
@@ -24,22 +31,24 @@ var init = (RED) => {
|
|
|
24
31
|
const correlationId = resolveCorrelationId(msg, base.correlationId, def.generateCorrelationId === true);
|
|
25
32
|
if (correlationId) base.correlationId = correlationId;
|
|
26
33
|
const { teamId, ...createBody } = base;
|
|
27
|
-
const task = await agent.tasks.create(createBody, { teamId });
|
|
34
|
+
const task = await withAgent(agentNode, (agent) => agent.tasks.create(createBody, { teamId }));
|
|
28
35
|
const out = RED.util.cloneMessage(msg);
|
|
29
36
|
if (correlationId) out.correlationId = correlationId;
|
|
30
37
|
out.payload = task;
|
|
38
|
+
active.delete(invocationId);
|
|
31
39
|
this.status({
|
|
32
40
|
fill: "green",
|
|
33
41
|
shape: "dot",
|
|
34
|
-
text: `
|
|
42
|
+
text: statusText(`created ${shortId(task.id)}`, label, active.size)
|
|
35
43
|
});
|
|
36
44
|
send(out);
|
|
37
45
|
done();
|
|
38
46
|
} catch (err) {
|
|
47
|
+
active.delete(invocationId);
|
|
39
48
|
this.status({
|
|
40
49
|
fill: "red",
|
|
41
50
|
shape: "ring",
|
|
42
|
-
text: "error"
|
|
51
|
+
text: statusText("error", label, active.size)
|
|
43
52
|
});
|
|
44
53
|
done(err instanceof Error ? err : new Error(String(err)));
|
|
45
54
|
}
|
|
@@ -60,5 +69,37 @@ function resolveCorrelationId(msg, fromPayload, generate) {
|
|
|
60
69
|
if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
|
|
61
70
|
return generate ? randomUUID() : void 0;
|
|
62
71
|
}
|
|
72
|
+
function describeMessage(msg) {
|
|
73
|
+
if (typeof msg.reviewDimension === "string" && msg.reviewDimension) return msg.reviewDimension;
|
|
74
|
+
const payload = msg.payload;
|
|
75
|
+
if (payload && typeof payload === "object") {
|
|
76
|
+
const p = payload;
|
|
77
|
+
if (typeof p.dimension === "string" && p.dimension) return p.dimension;
|
|
78
|
+
if (typeof p.title === "string" && p.title) return p.title;
|
|
79
|
+
}
|
|
80
|
+
return "task";
|
|
81
|
+
}
|
|
82
|
+
function describeTaskBody(body, fallback) {
|
|
83
|
+
if (fallback !== "task") return fallback;
|
|
84
|
+
const input = body.input;
|
|
85
|
+
if (input && typeof input === "object") {
|
|
86
|
+
const execution = input.execution;
|
|
87
|
+
if (execution && typeof execution === "object") {
|
|
88
|
+
const dimension = execution.dimension;
|
|
89
|
+
if (typeof dimension === "string" && dimension) return dimension;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return typeof body.title === "string" && body.title ? body.title : fallback;
|
|
93
|
+
}
|
|
94
|
+
function statusText(action, label, activeCount) {
|
|
95
|
+
const suffix = activeCount > 0 ? ` · ${activeCount} active` : "";
|
|
96
|
+
return `${action} · ${truncate(label, 34)}${suffix}`;
|
|
97
|
+
}
|
|
98
|
+
function shortId(id) {
|
|
99
|
+
return typeof id === "string" && id ? id.slice(0, 8) : "task";
|
|
100
|
+
}
|
|
101
|
+
function truncate(value, max) {
|
|
102
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
103
|
+
}
|
|
63
104
|
//#endregion
|
|
64
105
|
export { init as default };
|
package/dist/nodes/tasks-list.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
import { a as nonEmpty, c as positiveInt, n as compact, r as csv, t as bool } from "./query-utils.js";
|
|
2
3
|
//#region src/nodes/tasks-list.ts
|
|
3
4
|
var init = (RED) => {
|
|
@@ -15,9 +16,8 @@ var init = (RED) => {
|
|
|
15
16
|
shape: "dot",
|
|
16
17
|
text: "loading…"
|
|
17
18
|
});
|
|
18
|
-
const agent = await agentNode.getAgent();
|
|
19
19
|
const query = buildTasksQuery(def, msg);
|
|
20
|
-
const result = await agent.tasks.list(query, { teamId });
|
|
20
|
+
const result = await withAgent(agentNode, (agent) => agent.tasks.list(query, { teamId }));
|
|
21
21
|
const out = RED.util.cloneMessage(msg);
|
|
22
22
|
out.payload = result.items;
|
|
23
23
|
out.tasks = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as withAgent } from "./agent-call.js";
|
|
1
2
|
//#region src/nodes/workflow-status.ts
|
|
2
3
|
var init = (RED) => {
|
|
3
4
|
function WorkflowStatusNode(def) {
|
|
@@ -17,12 +18,11 @@ var init = (RED) => {
|
|
|
17
18
|
});
|
|
18
19
|
const teamId = agentNode.teamId;
|
|
19
20
|
if (!teamId) throw new Error("workflow-status: agent teamId is required");
|
|
20
|
-
const agent = await agentNode.getAgent();
|
|
21
21
|
const query = {
|
|
22
22
|
correlationId,
|
|
23
23
|
limit: def.limit && def.limit > 0 ? def.limit : 50
|
|
24
24
|
};
|
|
25
|
-
const res = await agent.tasks.list(query, { teamId });
|
|
25
|
+
const res = await withAgent(agentNode, (agent) => agent.tasks.list(query, { teamId }));
|
|
26
26
|
const rows = res.items.map((t) => ({
|
|
27
27
|
taskId: t.id,
|
|
28
28
|
type: t.taskType,
|