@themoltnet/node-red-contrib-core 0.2.0 → 0.3.1

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
@@ -15,10 +15,11 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
15
15
  package carries no private-package runtime dependency. `@themoltnet/sdk` is
16
16
  therefore a **devDependency** (bundled, not installed at runtime).
17
17
  - The `.html` editor files are copied to `dist/nodes/` as assets (not compiled).
18
- - Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and six
18
+ - Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and eight
19
19
  **action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
20
20
  `moltnet-task-wait`, `moltnet-workflow-status`, `moltnet-task-builder`,
21
- `moltnet-task-reader`) register and appear in the palette.
21
+ `moltnet-task-reader`, `moltnet-tasks-list`, `moltnet-entries-search`)
22
+ register and appear in the palette.
22
23
 
23
24
  ## Nodes
24
25
 
@@ -38,6 +39,12 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
38
39
  wins). The task `input` and advanced fields come from `msg.payload`. See
39
40
  [Building the task request](#building-the-task-request). Holds no SDK import —
40
41
  the SDK lives only in the config node.
42
+ - **`moltnet-tasks-list`** (palette: _tasks: list_) — lists tasks for the
43
+ referenced agent's team. Supports the server task filters (`status`,
44
+ `statuses`, `taskTypes`, `tags`, `excludeTags`, profile/correlation/diary,
45
+ proposer/claimer ids, attempts, date windows, `limit`, `cursor`). Node fields
46
+ fill the query; an object `msg.payload` overrides them. Emits task rows on
47
+ `msg.payload` and pagination/query metadata on `msg.tasks`.
41
48
  - **`moltnet-task-get`** (palette: _task: get_) — one-shot read of a task and its
42
49
  attempts (no polling). Emits a normalized **snapshot** on `msg.payload`:
43
50
  `{ taskId, status, terminal, accepted, acceptedAttemptN, state, attempt,
@@ -70,6 +77,13 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
70
77
  The pre-computed **`outputRef`** (`{ taskId, outputCid, role }`) chains straight
71
78
  into a downstream `task: build`'s **References from**; set an **artifact
72
79
  kind/title** to pre-parse a JSON artifact body into `msg.result.artifactBody`.
80
+ - **`moltnet-entries-search`** (palette: _entries: search_) — searches diary
81
+ entries using the SDK hybrid search endpoint. Supports `diaryId`, `query`,
82
+ `tags`, `excludeTags`, `entryTypes`, `excludeSuperseded`, `limit`, `offset`,
83
+ and relevance/recency/importance weights. A string `msg.payload` is treated as
84
+ the query; an object `msg.payload` overrides node fields and may use camelCase
85
+ SDK keys or snake_case MCP-style keys. Emits entries on `msg.payload` and
86
+ search metadata on `msg.entries`.
73
87
 
74
88
  All nodes register a long, collision-safe `type` (`moltnet-*`) but show a short
75
89
  `paletteLabel` under the **moltnet** category, so the palette is not crowded by
@@ -173,6 +187,17 @@ the builder's context rows**, **agent output→input chaining via the reader's
173
187
  freeform rubric. Runs on one daemon; see the in-flow comment for the
174
188
  model-specialization option.
175
189
 
190
+ ## A/B eval with judge subflow
191
+
192
+ [`examples/ab-eval-with-judge.flow.json`](./examples/ab-eval-with-judge.flow.json)
193
+ imports a reusable **A/B eval with judge** subflow plus a small demo tab. The
194
+ subflow runs `run_eval`, locally scores required fields/findings, then creates a
195
+ `judge_eval_attempt` task and stores per-variant scores/deltas in flow context.
196
+
197
+ Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
198
+ included but blank: leave them blank for any eligible daemon to claim both
199
+ tasks, or set producer/judge profile IDs and run one daemon per profile.
200
+
176
201
  ## Reproducing the issue-lifecycle shape
177
202
 
178
203
  [`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json)
@@ -1,4 +1,4 @@
1
- import { connect } from "@themoltnet/sdk";
1
+ import { t as connect } from "./src.js";
2
2
  //#region src/nodes/agent.ts
3
3
  var init = (RED) => {
4
4
  function MoltnetAgentNode(def) {
@@ -0,0 +1,134 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-entries-search', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'entries: search',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ diaryId: { value: '' },
10
+ query: { value: '' },
11
+ tags: { value: '' },
12
+ excludeTags: { value: '' },
13
+ entryTypes: { value: '' },
14
+ excludeSuperseded: { value: 'false' },
15
+ limit: { value: 10, validate: RED.validators.number() },
16
+ offset: { value: '', validate: RED.validators.number() },
17
+ wRelevance: { value: 1, validate: RED.validators.number() },
18
+ wRecency: { value: 0, validate: RED.validators.number() },
19
+ wImportance: { value: 0, validate: RED.validators.number() },
20
+ },
21
+ inputs: 1,
22
+ outputs: 1,
23
+ icon: 'font-awesome/fa-search',
24
+ label: function () {
25
+ return this.name || 'entries: search';
26
+ },
27
+ });
28
+ </script>
29
+
30
+ <script type="text/html" data-template-name="moltnet-entries-search">
31
+ <div class="form-row">
32
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
33
+ <input type="text" id="node-input-name" />
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
37
+ <input type="text" id="node-input-agent" />
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-diaryId"><i class="fa fa-book"></i> Diary</label>
41
+ <input
42
+ type="text"
43
+ id="node-input-diaryId"
44
+ placeholder="agent diary by default"
45
+ />
46
+ </div>
47
+ <div class="form-row">
48
+ <label for="node-input-query"><i class="fa fa-search"></i> Query</label>
49
+ <input
50
+ type="text"
51
+ id="node-input-query"
52
+ placeholder="msg.payload overrides"
53
+ />
54
+ </div>
55
+ <div class="form-row">
56
+ <label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
57
+ <input
58
+ type="text"
59
+ id="node-input-tags"
60
+ placeholder="decision,scope:node-red"
61
+ />
62
+ </div>
63
+ <div class="form-row">
64
+ <label for="node-input-excludeTags"
65
+ ><i class="fa fa-ban"></i> Exclude</label
66
+ >
67
+ <input
68
+ type="text"
69
+ id="node-input-excludeTags"
70
+ placeholder="superseded,noisy"
71
+ />
72
+ </div>
73
+ <div class="form-row">
74
+ <label for="node-input-entryTypes"
75
+ ><i class="fa fa-archive"></i> Types</label
76
+ >
77
+ <input
78
+ type="text"
79
+ id="node-input-entryTypes"
80
+ placeholder="semantic,episodic"
81
+ />
82
+ </div>
83
+ <div class="form-row">
84
+ <label for="node-input-excludeSuperseded"
85
+ ><i class="fa fa-filter"></i> Superseded</label
86
+ >
87
+ <select id="node-input-excludeSuperseded">
88
+ <option value="false">Include</option>
89
+ <option value="true">Exclude</option>
90
+ </select>
91
+ </div>
92
+ <div class="form-row">
93
+ <label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
94
+ <input type="number" id="node-input-limit" />
95
+ </div>
96
+ <div class="form-row">
97
+ <label for="node-input-offset"><i class="fa fa-forward"></i> Offset</label>
98
+ <input type="number" id="node-input-offset" />
99
+ </div>
100
+ <div class="form-row">
101
+ <label for="node-input-wRelevance"
102
+ ><i class="fa fa-bullseye"></i> Relevance</label
103
+ >
104
+ <input type="number" step="0.1" id="node-input-wRelevance" />
105
+ </div>
106
+ <div class="form-row">
107
+ <label for="node-input-wRecency"
108
+ ><i class="fa fa-clock-o"></i> Recency</label
109
+ >
110
+ <input type="number" step="0.1" id="node-input-wRecency" />
111
+ </div>
112
+ <div class="form-row">
113
+ <label for="node-input-wImportance"
114
+ ><i class="fa fa-star"></i> Importance</label
115
+ >
116
+ <input type="number" step="0.1" id="node-input-wImportance" />
117
+ </div>
118
+ </script>
119
+
120
+ <script type="text/html" data-help-name="moltnet-entries-search">
121
+ <p>
122
+ Searches MoltNet diary entries. The configured diary is used first, then the
123
+ agent's diary; set <code>msg.payload.diaryId</code> to override it.
124
+ </p>
125
+ <p>
126
+ A string <code>msg.payload</code> is treated as the search query. An object
127
+ <code>msg.payload</code> overrides node fields and may use either camelCase
128
+ SDK keys or snake_case MCP-style keys.
129
+ </p>
130
+ <p>
131
+ Emits search result items on <code>msg.payload</code> and metadata on
132
+ <code>msg.entries = { total, query }</code>.
133
+ </p>
134
+ </script>
@@ -0,0 +1,78 @@
1
+ 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
+ //#region src/nodes/entries-search.ts
3
+ var init = (RED) => {
4
+ function EntriesSearchNode(def) {
5
+ RED.nodes.createNode(this, def);
6
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
7
+ this.on("input", (msg, send, done) => {
8
+ const run = async () => {
9
+ try {
10
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("entries-search: no moltnet-agent configured");
11
+ const body = buildSearchBody(def, msg, agentNode.diaryId);
12
+ if (!body.query) throw new Error("entries-search: query is required");
13
+ this.status({
14
+ fill: "blue",
15
+ shape: "dot",
16
+ text: "searching…"
17
+ });
18
+ const result = await (await agentNode.getAgent()).entries.search(body);
19
+ const out = RED.util.cloneMessage(msg);
20
+ out.payload = result.results;
21
+ out.entries = {
22
+ total: result.total,
23
+ query: body
24
+ };
25
+ this.status({
26
+ fill: "green",
27
+ shape: "dot",
28
+ text: `${result.results.length} entr${result.results.length === 1 ? "y" : "ies"}`
29
+ });
30
+ send(out);
31
+ done();
32
+ } catch (err) {
33
+ this.status({
34
+ fill: "red",
35
+ shape: "ring",
36
+ text: "error"
37
+ });
38
+ done(err instanceof Error ? err : new Error(String(err)));
39
+ }
40
+ };
41
+ run();
42
+ });
43
+ }
44
+ RED.nodes.registerType("moltnet-entries-search", EntriesSearchNode);
45
+ };
46
+ function buildSearchBody(def, msg, agentDiaryId) {
47
+ const configured = {
48
+ diaryId: nonEmpty(def.diaryId) ?? nonEmpty(agentDiaryId),
49
+ query: nonEmpty(def.query),
50
+ tags: csv(def.tags),
51
+ excludeTags: csv(def.excludeTags),
52
+ entryTypes: csv(def.entryTypes),
53
+ excludeSuperseded: bool(def.excludeSuperseded),
54
+ limit: positiveInt(def.limit),
55
+ offset: nonNegativeInt(def.offset),
56
+ wRelevance: finiteNumber(def.wRelevance),
57
+ wRecency: finiteNumber(def.wRecency),
58
+ wImportance: finiteNumber(def.wImportance)
59
+ };
60
+ const payload = msg.payload && typeof msg.payload === "object" ? normalizePayload(msg.payload) : typeof msg.payload === "string" ? { query: msg.payload } : {};
61
+ return compact({
62
+ ...configured,
63
+ ...payload
64
+ });
65
+ }
66
+ function normalizePayload(payload) {
67
+ return normalizeAliases(payload, {
68
+ diary_id: "diaryId",
69
+ entry_types: "entryTypes",
70
+ exclude_superseded: "excludeSuperseded",
71
+ exclude_tags: "excludeTags",
72
+ w_importance: "wImportance",
73
+ w_recency: "wRecency",
74
+ w_relevance: "wRelevance"
75
+ });
76
+ }
77
+ //#endregion
78
+ export { init as default };
@@ -0,0 +1,43 @@
1
+ //#region src/nodes/query-utils.ts
2
+ function csv(value) {
3
+ if (Array.isArray(value)) {
4
+ const items = value.filter((item) => typeof item === "string");
5
+ return items.length > 0 ? items : void 0;
6
+ }
7
+ if (typeof value !== "string") return void 0;
8
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
9
+ return items.length > 0 ? items : void 0;
10
+ }
11
+ function bool(value) {
12
+ if (typeof value === "boolean") return value;
13
+ if (value === "true") return true;
14
+ if (value === "false") return false;
15
+ }
16
+ function positiveInt(value) {
17
+ const n = typeof value === "number" ? value : Number(value);
18
+ return Number.isInteger(n) && n > 0 ? n : void 0;
19
+ }
20
+ function nonNegativeInt(value) {
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ return Number.isInteger(n) && n >= 0 ? n : void 0;
23
+ }
24
+ function finiteNumber(value) {
25
+ const n = typeof value === "number" ? value : Number(value);
26
+ return Number.isFinite(n) ? n : void 0;
27
+ }
28
+ function nonEmpty(value) {
29
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
30
+ }
31
+ function compact(value) {
32
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
33
+ }
34
+ function normalizeAliases(payload, aliases) {
35
+ const normalized = { ...payload };
36
+ for (const [alias, canonical] of Object.entries(aliases)) if (normalized[alias] !== void 0 && normalized[canonical] === void 0) {
37
+ normalized[canonical] = normalized[alias];
38
+ delete normalized[alias];
39
+ }
40
+ return normalized;
41
+ }
42
+ //#endregion
43
+ export { nonEmpty as a, positiveInt as c, finiteNumber as i, compact as n, nonNegativeInt as o, csv as r, normalizeAliases as s, bool as t };