@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.
@@ -1,4 +1,4 @@
1
- import { TaskBuildError, buildTask } from "@themoltnet/sdk";
1
+ import { i as TaskBuildError, r as buildTask } from "./src.js";
2
2
  //#region src/nodes/task-builder.ts
3
3
  /** Split a comma-separated config string into trimmed, non-empty values. */
4
4
  function parseCsv(raw) {
@@ -1,4 +1,4 @@
1
- import { TaskResultError, createResultReader } from "@themoltnet/sdk";
1
+ import { a as TaskResultError, n as createResultReader } from "./src.js";
2
2
  //#region src/nodes/task-reader.ts
3
3
  var init = (RED) => {
4
4
  function TaskReaderNode(def) {
@@ -0,0 +1,174 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-tasks-list', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'tasks: list',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ status: { value: '' },
10
+ statusList: { value: '' },
11
+ taskTypes: { value: '' },
12
+ tags: { value: '' },
13
+ excludeTags: { value: '' },
14
+ profileId: { value: '' },
15
+ correlationId: { value: '' },
16
+ diaryId: { value: '' },
17
+ proposedByAgentId: { value: '' },
18
+ proposedByHumanId: { value: '' },
19
+ claimedByAgentId: { value: '' },
20
+ hasAttempts: { value: '' },
21
+ queuedAfter: { value: '' },
22
+ queuedBefore: { value: '' },
23
+ completedAfter: { value: '' },
24
+ completedBefore: { value: '' },
25
+ limit: { value: 20, validate: RED.validators.number() },
26
+ cursor: { value: '' },
27
+ },
28
+ inputs: 1,
29
+ outputs: 1,
30
+ icon: 'font-awesome/fa-list',
31
+ label: function () {
32
+ return this.name || 'tasks: list';
33
+ },
34
+ });
35
+ </script>
36
+
37
+ <script type="text/html" data-template-name="moltnet-tasks-list">
38
+ <div class="form-row">
39
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
40
+ <input type="text" id="node-input-name" />
41
+ </div>
42
+ <div class="form-row">
43
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
44
+ <input type="text" id="node-input-agent" />
45
+ </div>
46
+ <div class="form-row">
47
+ <label for="node-input-status"><i class="fa fa-check"></i> Status</label>
48
+ <input type="text" id="node-input-status" placeholder="queued" />
49
+ </div>
50
+ <div class="form-row">
51
+ <label for="node-input-statusList"
52
+ ><i class="fa fa-check-square-o"></i> Statuses</label
53
+ >
54
+ <input
55
+ type="text"
56
+ id="node-input-statusList"
57
+ placeholder="waiting,queued"
58
+ />
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-taskTypes"><i class="fa fa-cubes"></i> Types</label>
62
+ <input
63
+ type="text"
64
+ id="node-input-taskTypes"
65
+ placeholder="fulfill_brief,render_pack"
66
+ />
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
70
+ <input type="text" id="node-input-tags" placeholder="issue,triage" />
71
+ </div>
72
+ <div class="form-row">
73
+ <label for="node-input-excludeTags"
74
+ ><i class="fa fa-ban"></i> Exclude</label
75
+ >
76
+ <input type="text" id="node-input-excludeTags" placeholder="archived" />
77
+ </div>
78
+ <div class="form-row">
79
+ <label for="node-input-profileId"
80
+ ><i class="fa fa-id-card"></i> Profile</label
81
+ >
82
+ <input type="text" id="node-input-profileId" />
83
+ </div>
84
+ <div class="form-row">
85
+ <label for="node-input-correlationId"
86
+ ><i class="fa fa-link"></i> Correlation</label
87
+ >
88
+ <input
89
+ type="text"
90
+ id="node-input-correlationId"
91
+ placeholder="msg.payload overrides"
92
+ />
93
+ </div>
94
+ <div class="form-row">
95
+ <label for="node-input-diaryId"><i class="fa fa-book"></i> Diary</label>
96
+ <input type="text" id="node-input-diaryId" />
97
+ </div>
98
+ <div class="form-row">
99
+ <label for="node-input-proposedByAgentId"
100
+ ><i class="fa fa-user-circle"></i> Proposed agent</label
101
+ >
102
+ <input type="text" id="node-input-proposedByAgentId" />
103
+ </div>
104
+ <div class="form-row">
105
+ <label for="node-input-proposedByHumanId"
106
+ ><i class="fa fa-user"></i> Proposed human</label
107
+ >
108
+ <input type="text" id="node-input-proposedByHumanId" />
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-claimedByAgentId"
112
+ ><i class="fa fa-hand-paper-o"></i> Claimed agent</label
113
+ >
114
+ <input type="text" id="node-input-claimedByAgentId" />
115
+ </div>
116
+ <div class="form-row">
117
+ <label for="node-input-hasAttempts"
118
+ ><i class="fa fa-history"></i> Attempts</label
119
+ >
120
+ <select id="node-input-hasAttempts">
121
+ <option value="">Any</option>
122
+ <option value="true">Has attempts</option>
123
+ <option value="false">No attempts</option>
124
+ </select>
125
+ </div>
126
+ <div class="form-row">
127
+ <label for="node-input-queuedAfter"
128
+ ><i class="fa fa-clock-o"></i> Queued after</label
129
+ >
130
+ <input
131
+ type="text"
132
+ id="node-input-queuedAfter"
133
+ placeholder="2026-06-25T00:00:00Z"
134
+ />
135
+ </div>
136
+ <div class="form-row">
137
+ <label for="node-input-queuedBefore"
138
+ ><i class="fa fa-clock-o"></i> Queued before</label
139
+ >
140
+ <input type="text" id="node-input-queuedBefore" />
141
+ </div>
142
+ <div class="form-row">
143
+ <label for="node-input-completedAfter"
144
+ ><i class="fa fa-calendar-check-o"></i> Done after</label
145
+ >
146
+ <input type="text" id="node-input-completedAfter" />
147
+ </div>
148
+ <div class="form-row">
149
+ <label for="node-input-completedBefore"
150
+ ><i class="fa fa-calendar-check-o"></i> Done before</label
151
+ >
152
+ <input type="text" id="node-input-completedBefore" />
153
+ </div>
154
+ <div class="form-row">
155
+ <label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
156
+ <input type="number" id="node-input-limit" />
157
+ </div>
158
+ <div class="form-row">
159
+ <label for="node-input-cursor"><i class="fa fa-forward"></i> Cursor</label>
160
+ <input type="text" id="node-input-cursor" />
161
+ </div>
162
+ </script>
163
+
164
+ <script type="text/html" data-help-name="moltnet-tasks-list">
165
+ <p>
166
+ Lists MoltNet tasks for the configured agent's team. Node fields build the
167
+ query, and any object fields on <code>msg.payload</code> override them.
168
+ </p>
169
+ <p>
170
+ Emits the task array on <code>msg.payload</code>. Pagination metadata and
171
+ the final query are on <code>msg.tasks</code>:
172
+ <code>{ total, nextCursor, query }</code>.
173
+ </p>
174
+ </script>
@@ -0,0 +1,77 @@
1
+ import { a as nonEmpty, c as positiveInt, n as compact, r as csv, t as bool } from "./query-utils.js";
2
+ //#region src/nodes/tasks-list.ts
3
+ var init = (RED) => {
4
+ function TasksListNode(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("tasks-list: no moltnet-agent configured");
11
+ const teamId = agentNode.teamId;
12
+ if (!teamId) throw new Error("tasks-list: agent teamId is required");
13
+ this.status({
14
+ fill: "blue",
15
+ shape: "dot",
16
+ text: "loading…"
17
+ });
18
+ const agent = await agentNode.getAgent();
19
+ const query = buildTasksQuery(def, msg);
20
+ const result = await agent.tasks.list(query, { teamId });
21
+ const out = RED.util.cloneMessage(msg);
22
+ out.payload = result.items;
23
+ out.tasks = {
24
+ total: result.total,
25
+ nextCursor: result.nextCursor,
26
+ query
27
+ };
28
+ this.status({
29
+ fill: "green",
30
+ shape: "dot",
31
+ text: `${result.items.length} task(s)`
32
+ });
33
+ send(out);
34
+ done();
35
+ } catch (err) {
36
+ this.status({
37
+ fill: "red",
38
+ shape: "ring",
39
+ text: "error"
40
+ });
41
+ done(err instanceof Error ? err : new Error(String(err)));
42
+ }
43
+ };
44
+ run();
45
+ });
46
+ }
47
+ RED.nodes.registerType("moltnet-tasks-list", TasksListNode);
48
+ };
49
+ function buildTasksQuery(def, msg) {
50
+ const configured = {
51
+ status: nonEmpty(def.status),
52
+ statuses: csv(def.statusList),
53
+ taskTypes: csv(def.taskTypes),
54
+ tags: csv(def.tags),
55
+ excludeTags: csv(def.excludeTags),
56
+ profileId: nonEmpty(def.profileId),
57
+ correlationId: nonEmpty(def.correlationId),
58
+ diaryId: nonEmpty(def.diaryId),
59
+ proposedByAgentId: nonEmpty(def.proposedByAgentId),
60
+ proposedByHumanId: nonEmpty(def.proposedByHumanId),
61
+ claimedByAgentId: nonEmpty(def.claimedByAgentId),
62
+ hasAttempts: bool(def.hasAttempts),
63
+ queuedAfter: nonEmpty(def.queuedAfter),
64
+ queuedBefore: nonEmpty(def.queuedBefore),
65
+ completedAfter: nonEmpty(def.completedAfter),
66
+ completedBefore: nonEmpty(def.completedBefore),
67
+ limit: positiveInt(def.limit),
68
+ cursor: nonEmpty(def.cursor)
69
+ };
70
+ const payload = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
71
+ return compact({
72
+ ...configured,
73
+ ...payload
74
+ });
75
+ }
76
+ //#endregion
77
+ export { init as default };
@@ -0,0 +1,292 @@
1
+ [
2
+ {
3
+ "category": "moltnet",
4
+ "color": "#D7F7C2",
5
+ "env": [],
6
+ "icon": "font-awesome/fa-balance-scale",
7
+ "id": "subflow_ab_eval_with_judge",
8
+ "in": [
9
+ {
10
+ "wires": [
11
+ {
12
+ "id": "sf_ab_build_run_eval"
13
+ }
14
+ ],
15
+ "x": 40,
16
+ "y": 120
17
+ }
18
+ ],
19
+ "info": "Generic run_eval -> local score -> judge_eval_attempt pipeline. Parent flow supplies msg.evalScenario, msg.evalVariantLabel, msg.evalSkillContext, msg.evalJudgeCriteria, and msg.correlationId. Configure the agent/runtime-profile nodes after import, then run matching agent daemons.",
20
+ "meta": {},
21
+ "name": "A/B eval with judge",
22
+ "out": [
23
+ {
24
+ "wires": [
25
+ {
26
+ "id": "sf_ab_store_delta",
27
+ "port": 0
28
+ }
29
+ ],
30
+ "x": 1280,
31
+ "y": 280
32
+ }
33
+ ],
34
+ "type": "subflow"
35
+ },
36
+ {
37
+ "func": "const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst scenario = msg.evalScenario;\nif (!scenario || typeof scenario !== 'object') {\n node.error('Missing msg.evalScenario', msg);\n return null;\n}\nconst variantLabel = msg.evalVariantLabel || msg.payload?.variantLabel || 'skill-rubric-v1';\nconst correlationId = msg.correlationId || msg.evalGroupCorrelationId || msg.payload?.correlationId;\nif (!uuidRe.test(String(correlationId || ''))) {\n node.error('Missing valid correlationId for eval group', msg);\n return null;\n}\nconst evidence = scenario.evidence || {};\nconst expected = scenario.expected || {};\nconst basePrompt = scenario.prompt || [\n 'Run this eval scenario.',\n '',\n 'Return ONLY one valid JSON object. Do not use Markdown, headings, prose, tables, or code fences.',\n 'Use only the supplied evidence. Do not browse. Do not invent facts. Mark unknowns explicitly.',\n '',\n 'Scenario: ' + (scenario.title || scenario.id || 'unknown'),\n 'Variant: ' + variantLabel,\n '',\n 'Evidence JSON:',\n JSON.stringify(evidence, null, 2)\n].join('\\n');\nconst taskContexts = [\n { slug: 'eval-evidence', binding: 'context_inline', content: JSON.stringify(evidence, null, 2) },\n { slug: 'eval-expectations', binding: 'context_inline', content: JSON.stringify(expected, null, 2) }\n];\nif (variantLabel !== 'baseline-no-skill' && msg.evalSkillContext?.content) {\n taskContexts.unshift({\n slug: msg.evalSkillContext.slug || 'eval-skill-context',\n binding: msg.evalSkillContext.binding || 'skill',\n content: msg.evalSkillContext.content\n });\n}\nmsg.correlationId = correlationId;\nmsg.evalGroupCorrelationId = correlationId;\nmsg.evalScenario = scenario;\nmsg.evalVariantLabel = variantLabel;\nmsg.payload = {\n taskType: 'run_eval',\n title: 'Eval: ' + (scenario.id || 'scenario') + ' / ' + variantLabel,\n tags: ['eval', scenario.id || 'scenario', variantLabel],\n correlationId,\n input: {\n scenario: { prompt: basePrompt },\n variantLabel,\n execution: { mode: 'vitro', workspace: 'none' },\n context: taskContexts\n }\n};\nreturn msg;",
38
+ "id": "sf_ab_build_run_eval",
39
+ "name": "build run_eval",
40
+ "outputs": 1,
41
+ "type": "function",
42
+ "wires": [["sf_ab_create_run_eval"]],
43
+ "x": 170,
44
+ "y": 120,
45
+ "z": "subflow_ab_eval_with_judge"
46
+ },
47
+ {
48
+ "agent": "eval_agent_cfg",
49
+ "generateCorrelationId": false,
50
+ "id": "sf_ab_create_run_eval",
51
+ "maxAttempts": 1,
52
+ "name": "RUN EVAL producer",
53
+ "runtimeProfile": "eval_profile_producer",
54
+ "type": "moltnet-tasks-create",
55
+ "wires": [["sf_ab_wait_run_eval"]],
56
+ "x": 390,
57
+ "y": 120,
58
+ "z": "subflow_ab_eval_with_judge"
59
+ },
60
+ {
61
+ "agent": "eval_agent_cfg",
62
+ "id": "sf_ab_wait_run_eval",
63
+ "kinds": "",
64
+ "name": "wait RUN EVAL",
65
+ "pollIntervalSec": 5,
66
+ "tail": true,
67
+ "taskId": "",
68
+ "timeoutSec": 1800,
69
+ "type": "moltnet-task-wait",
70
+ "wires": [[], ["sf_ab_stash_run_eval"]],
71
+ "x": 600,
72
+ "y": 120,
73
+ "z": "subflow_ab_eval_with_judge"
74
+ },
75
+ {
76
+ "func": "msg.evalTaskId = msg.payload?.taskId || msg.payload?.task?.id || msg.payload?.id || msg.taskId || null;\nmsg.evalAcceptedAttemptN = msg.payload?.acceptedAttemptN || msg.payload?.task?.acceptedAttemptN || 1;\nreturn msg;",
77
+ "id": "sf_ab_stash_run_eval",
78
+ "name": "stash producer task id",
79
+ "outputs": 1,
80
+ "type": "function",
81
+ "wires": [["sf_ab_read_run_eval"]],
82
+ "x": 820,
83
+ "y": 120,
84
+ "z": "subflow_ab_eval_with_judge"
85
+ },
86
+ {
87
+ "artifactKind": "",
88
+ "artifactTitle": "",
89
+ "id": "sf_ab_read_run_eval",
90
+ "name": "read RUN EVAL",
91
+ "role": "context",
92
+ "source": "payload",
93
+ "type": "moltnet-task-reader",
94
+ "wires": [["sf_ab_score_run_eval"]],
95
+ "x": 1040,
96
+ "y": 120,
97
+ "z": "subflow_ab_eval_with_judge"
98
+ },
99
+ {
100
+ "func": "function extractJsonObject(text) {\n if (!text || typeof text !== 'string') return null;\n const fenced = text.match(/```json\\s*([\\s\\S]*?)```/i) || text.match(/```\\s*([\\s\\S]*?)```/);\n const candidates = [];\n if (fenced) candidates.push(fenced[1]);\n const first = text.indexOf('{');\n const last = text.lastIndexOf('}');\n if (first >= 0 && last > first) candidates.push(text.slice(first, last + 1));\n for (const candidate of candidates) {\n try { return JSON.parse(candidate); } catch (_) {}\n }\n return null;\n}\nconst output = msg.payload || {};\nconst response = output.response || msg.result?.summary || '';\nconst analysis = extractJsonObject(response);\nconst expected = msg.evalScenario?.expected || {};\nconst requiredFields = msg.evalRequiredFields || ['decision', 'readinessScore0to100', 'decisionRationale', 'topActions', 'unknowns'];\nconst missingFields = analysis ? requiredFields.filter((field) => analysis[field] === undefined) : requiredFields;\nconst responseLower = response.toLowerCase();\nconst requiredFindings = expected.requiredFindings || [];\nconst missingFindings = requiredFindings.filter((item) => !responseLower.includes(String(item).toLowerCase()));\nconst forbiddenHits = (expected.forbidden || []).filter((item) => responseLower.includes(String(item).toLowerCase()));\nconst decision = analysis?.decision || null;\nconst decisionOk = decision ? (expected.acceptableDecisions || []).includes(decision) : false;\nconst checks = {\n parsedAnalysis: Boolean(analysis),\n requiredFields: missingFields.length === 0,\n decisionOk,\n noForbiddenHits: forbiddenHits.length === 0,\n requiredFindingsPresent: missingFindings.length === 0\n};\nconst score0to100 = Math.round((Object.values(checks).filter(Boolean).length / Object.keys(checks).length) * 100);\nmsg.evalProducerScore = {\n scenarioId: msg.evalScenario?.id,\n variantLabel: msg.evalVariantLabel,\n correlationId: msg.correlationId,\n taskId: msg.evalTaskId || null,\n score0to100,\n checks,\n decision,\n missingFields,\n missingFindings,\n forbiddenHits,\n analysis,\n responseRef: msg.result?.outputRef || null\n};\nreturn msg;",
101
+ "id": "sf_ab_score_run_eval",
102
+ "name": "local score",
103
+ "outputs": 1,
104
+ "type": "function",
105
+ "wires": [["sf_ab_build_judge_eval"]],
106
+ "x": 200,
107
+ "y": 210,
108
+ "z": "subflow_ab_eval_with_judge"
109
+ },
110
+ {
111
+ "func": "const targetTaskId = msg.evalTaskId || msg.payload?.taskId;\nconst targetAttemptN = msg.evalAcceptedAttemptN || msg.payload?.acceptedAttemptN || 1;\nif (!targetTaskId) {\n node.error('Missing target run_eval task id for judge_eval_attempt', msg);\n return null;\n}\nconst criteria = msg.evalJudgeCriteria || [];\nif (!Array.isArray(criteria) || criteria.length === 0) {\n node.error('Missing msg.evalJudgeCriteria', msg);\n return null;\n}\nmsg.evalJudgeTargetTaskId = targetTaskId;\nmsg.evalJudgeTargetAttemptN = targetAttemptN;\nmsg.payload = {\n taskType: 'judge_eval_attempt',\n title: 'Judge eval: ' + (msg.evalScenario?.id || 'unknown') + ' / ' + (msg.evalVariantLabel || 'unknown'),\n tags: ['eval-judge', msg.evalScenario?.id || 'unknown', msg.evalVariantLabel || 'unknown'],\n correlationId: msg.correlationId,\n input: {\n targetTaskId,\n targetAttemptN,\n successCriteria: {\n version: 1,\n rubric: {\n rubricId: msg.evalJudgeRubricId || 'ab-eval-rubric',\n version: msg.evalJudgeRubricVersion || '1',\n contentHash: msg.evalJudgeRubricHash || 'node-red-rubric',\n criteria\n }\n }\n }\n};\nreturn msg;",
112
+ "id": "sf_ab_build_judge_eval",
113
+ "name": "build judge_eval_attempt",
114
+ "outputs": 1,
115
+ "type": "function",
116
+ "wires": [["sf_ab_create_judge_eval"]],
117
+ "x": 460,
118
+ "y": 210,
119
+ "z": "subflow_ab_eval_with_judge"
120
+ },
121
+ {
122
+ "agent": "eval_agent_cfg",
123
+ "generateCorrelationId": false,
124
+ "id": "sf_ab_create_judge_eval",
125
+ "maxAttempts": 1,
126
+ "name": "JUDGE EVAL",
127
+ "runtimeProfile": "eval_profile_judge",
128
+ "type": "moltnet-tasks-create",
129
+ "wires": [["sf_ab_wait_judge_eval"]],
130
+ "x": 220,
131
+ "y": 290,
132
+ "z": "subflow_ab_eval_with_judge"
133
+ },
134
+ {
135
+ "agent": "eval_agent_cfg",
136
+ "id": "sf_ab_wait_judge_eval",
137
+ "kinds": "",
138
+ "name": "wait JUDGE EVAL",
139
+ "pollIntervalSec": 5,
140
+ "tail": true,
141
+ "taskId": "",
142
+ "timeoutSec": 1800,
143
+ "type": "moltnet-task-wait",
144
+ "wires": [[], ["sf_ab_stash_judge_eval"]],
145
+ "x": 430,
146
+ "y": 290,
147
+ "z": "subflow_ab_eval_with_judge"
148
+ },
149
+ {
150
+ "func": "msg.evalJudgeTaskId = msg.payload?.taskId || msg.payload?.task?.id || msg.payload?.id || msg.taskId || null;\nreturn msg;",
151
+ "id": "sf_ab_stash_judge_eval",
152
+ "name": "stash judge task id",
153
+ "outputs": 1,
154
+ "type": "function",
155
+ "wires": [["sf_ab_read_judge_eval"]],
156
+ "x": 660,
157
+ "y": 290,
158
+ "z": "subflow_ab_eval_with_judge"
159
+ },
160
+ {
161
+ "artifactKind": "",
162
+ "artifactTitle": "",
163
+ "id": "sf_ab_read_judge_eval",
164
+ "name": "read JUDGE EVAL",
165
+ "role": "context",
166
+ "source": "payload",
167
+ "type": "moltnet-task-reader",
168
+ "wires": [["sf_ab_store_delta"]],
169
+ "x": 880,
170
+ "y": 290,
171
+ "z": "subflow_ab_eval_with_judge"
172
+ },
173
+ {
174
+ "func": "function scoreFromComposite(judgment) {\n return typeof judgment.composite === 'number' ? Math.round(judgment.composite * 100) : null;\n}\n\nfunction buildVariantRecord(msg, judgment, judgeScore0to100) {\n return {\n correlationId: msg.correlationId || msg.evalGroupCorrelationId || null,\n scenarioId: msg.evalScenario?.id || 'unknown',\n variantLabel: msg.evalVariantLabel || judgment.variantLabel || 'unknown',\n runEvalTaskId: msg.evalJudgeTargetTaskId || msg.evalTaskId || null,\n runEvalAttemptN: msg.evalJudgeTargetAttemptN || msg.evalAcceptedAttemptN || null,\n judgeTaskId: msg.evalJudgeTaskId || null,\n producerScore0to100: msg.evalProducerScore?.score0to100 ?? null,\n judgeScore0to100,\n judgeComposite: typeof judgment.composite === 'number' ? judgment.composite : null,\n verdict: judgment.verdict || null,\n scores: judgment.scores || [],\n updatedAt: new Date().toISOString()\n };\n}\n\nfunction upsertScenarioResult(record) {\n const groups = flow.get('abEvalResults') || {};\n const group = groups[record.correlationId] || {\n correlationId: record.correlationId,\n scenarios: {},\n createdAt: new Date().toISOString()\n };\n const scenario = group.scenarios[record.scenarioId] || {};\n scenario[record.variantLabel] = record;\n group.scenarios[record.scenarioId] = scenario;\n group.updatedAt = new Date().toISOString();\n groups[record.correlationId] = group;\n flow.set('abEvalResults', groups);\n return scenario;\n}\n\nfunction rankedVariants(scenario) {\n return Object.values(scenario)\n .filter((item) => typeof item.judgeScore0to100 === 'number')\n .sort((a, b) => b.judgeScore0to100 - a.judgeScore0to100);\n}\n\nfunction buildDelta(correlationId, scenarioId, scenario) {\n const baseline = scenario['baseline-no-skill'];\n const skill = scenario['skill-rubric-v1'];\n if (!baseline || !skill) return null;\n return {\n correlationId,\n scenarioId,\n baselineScore0to100: baseline.judgeScore0to100,\n skillScore0to100: skill.judgeScore0to100,\n judgeDelta0to100: baseline.judgeScore0to100 === null || skill.judgeScore0to100 === null ? null : skill.judgeScore0to100 - baseline.judgeScore0to100,\n baselineProducerScore0to100: baseline.producerScore0to100,\n skillProducerScore0to100: skill.producerScore0to100,\n producerDelta0to100: baseline.producerScore0to100 === null || skill.producerScore0to100 === null ? null : skill.producerScore0to100 - baseline.producerScore0to100\n };\n}\n\nconst judgment = msg.payload || {};\nconst judgeScore0to100 = scoreFromComposite(judgment);\nconst record = buildVariantRecord(msg, judgment, judgeScore0to100);\nconst scenario = upsertScenarioResult(record);\nconst winner = rankedVariants(scenario)[0] || null;\n\nmsg.payload = {\n correlationId: record.correlationId,\n scenarioId: record.scenarioId,\n variantLabel: record.variantLabel,\n producerScore0to100: record.producerScore0to100,\n judgeScore0to100,\n verdict: judgment.verdict || null,\n scores: judgment.scores || [],\n winner,\n delta: buildDelta(record.correlationId, record.scenarioId, scenario),\n variants: scenario\n};\nreturn msg;",
175
+ "id": "sf_ab_store_delta",
176
+ "name": "store judgment + delta",
177
+ "outputs": 1,
178
+ "type": "function",
179
+ "wires": [[]],
180
+ "x": 1110,
181
+ "y": 290,
182
+ "z": "subflow_ab_eval_with_judge"
183
+ },
184
+ {
185
+ "disabled": false,
186
+ "id": "ab_eval_demo_tab",
187
+ "info": "Imports the A/B eval with judge subflow. Fill the moltnet-agent config, optionally set producer/judge runtime profile IDs, then run matching agent daemons before injecting the sample scenario.",
188
+ "label": "MoltNet A/B Eval (example)",
189
+ "type": "tab"
190
+ },
191
+ {
192
+ "apiUrl": "https://api.themolt.net",
193
+ "clientId": "",
194
+ "diaryId": "",
195
+ "id": "eval_agent_cfg",
196
+ "name": "eval-agent",
197
+ "teamId": "",
198
+ "type": "moltnet-agent"
199
+ },
200
+ {
201
+ "agent": "eval_agent_cfg",
202
+ "id": "eval_profile_producer",
203
+ "name": "eval-producer",
204
+ "profileId": "",
205
+ "profileName": "",
206
+ "type": "moltnet-runtime-profile"
207
+ },
208
+ {
209
+ "agent": "eval_agent_cfg",
210
+ "id": "eval_profile_judge",
211
+ "name": "eval-judge",
212
+ "profileId": "",
213
+ "profileName": "",
214
+ "type": "moltnet-runtime-profile"
215
+ },
216
+ {
217
+ "id": "ab_eval_note",
218
+ "info": "Run at least one agent daemon that can claim run_eval and judge_eval_attempt tasks. If you set runtime profile IDs above, run one daemon per profile. Leave profile IDs blank to let any eligible daemon claim both tasks.",
219
+ "name": "Daemon setup",
220
+ "type": "comment",
221
+ "wires": [],
222
+ "x": 160,
223
+ "y": 40,
224
+ "z": "ab_eval_demo_tab"
225
+ },
226
+ {
227
+ "func": "function uuid() {\n return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>\n (Number(c) ^ Math.floor(Math.random() * 16) >> Number(c) / 4).toString(16)\n );\n}\nmsg.correlationId = uuid();\nmsg.evalVariantLabel = 'skill-rubric-v1';\nmsg.evalSkillContext = {\n slug: 'example-skill-context',\n binding: 'skill',\n content: 'Prefer concise structured analysis. Ground every recommendation in supplied evidence.'\n};\nmsg.evalScenario = {\n id: 'demo-readiness',\n title: 'Demo traffic-fit readiness scenario',\n evidence: {\n offer: 'Trial subscription for a niche productivity tool',\n landingPage: 'Mentions benefits but lacks pricing and proof',\n tracking: ['page_view'],\n constraints: ['No browsing', 'Use evidence only']\n },\n expected: {\n acceptableDecisions: ['needs_work', 'not_ready'],\n requiredFindings: ['pricing', 'proof', 'tracking'],\n forbidden: ['ready to scale']\n }\n};\nmsg.evalJudgeCriteria = [\n { id: 'json-validity', title: 'Valid structured output', weight: 0.2, description: 'Response is parseable and includes the required fields.' },\n { id: 'evidence-grounding', title: 'Evidence grounding', weight: 0.4, description: 'Findings use only supplied evidence and mark unknowns.' },\n { id: 'decision-quality', title: 'Decision quality', weight: 0.4, description: 'Decision and actions match the scenario expectations.' }\n];\nreturn msg;",
228
+ "id": "ab_eval_seed",
229
+ "name": "seed sample eval",
230
+ "outputs": 1,
231
+ "type": "function",
232
+ "wires": [["ab_eval_subflow"]],
233
+ "x": 170,
234
+ "y": 120,
235
+ "z": "ab_eval_demo_tab"
236
+ },
237
+ {
238
+ "crontab": "",
239
+ "id": "ab_eval_inject",
240
+ "name": "run sample",
241
+ "once": false,
242
+ "onceDelay": 0.1,
243
+ "payload": "",
244
+ "payloadType": "date",
245
+ "props": [
246
+ {
247
+ "p": "payload"
248
+ }
249
+ ],
250
+ "repeat": "",
251
+ "topic": "",
252
+ "type": "inject",
253
+ "wires": [["ab_eval_seed"]],
254
+ "x": 110,
255
+ "y": 180,
256
+ "z": "ab_eval_demo_tab"
257
+ },
258
+ {
259
+ "id": "ab_eval_subflow",
260
+ "name": "A/B eval with judge",
261
+ "type": "subflow:subflow_ab_eval_with_judge",
262
+ "wires": [["ab_eval_debug"]],
263
+ "x": 410,
264
+ "y": 120,
265
+ "z": "ab_eval_demo_tab"
266
+ },
267
+ {
268
+ "active": true,
269
+ "complete": "payload",
270
+ "console": false,
271
+ "id": "ab_eval_debug",
272
+ "name": "eval result",
273
+ "statusType": "auto",
274
+ "statusVal": "",
275
+ "targetType": "msg",
276
+ "tosidebar": true,
277
+ "tostatus": false,
278
+ "type": "debug",
279
+ "wires": [],
280
+ "x": 650,
281
+ "y": 120,
282
+ "z": "ab_eval_demo_tab"
283
+ },
284
+ {
285
+ "env": [],
286
+ "id": "ab_eval_modules",
287
+ "modules": {
288
+ "@themoltnet/node-red-contrib-core": "0.2.0"
289
+ },
290
+ "type": "global-config"
291
+ }
292
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/node-red-contrib-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Node-RED nodes for the MoltNet API",
6
6
  "keywords": [
@@ -24,11 +24,13 @@
24
24
  "moltnet-agent": "dist/nodes/agent.js",
25
25
  "moltnet-runtime-profile": "dist/nodes/runtime-profile.js",
26
26
  "moltnet-tasks-create": "dist/nodes/tasks-create.js",
27
+ "moltnet-tasks-list": "dist/nodes/tasks-list.js",
27
28
  "moltnet-task-get": "dist/nodes/task-get.js",
28
29
  "moltnet-task-wait": "dist/nodes/task-wait.js",
29
30
  "moltnet-workflow-status": "dist/nodes/workflow-status.js",
30
31
  "moltnet-task-builder": "dist/nodes/task-builder.js",
31
- "moltnet-task-reader": "dist/nodes/task-reader.js"
32
+ "moltnet-task-reader": "dist/nodes/task-reader.js",
33
+ "moltnet-entries-search": "dist/nodes/entries-search.js"
32
34
  }
33
35
  },
34
36
  "engines": {
@@ -36,7 +38,7 @@
36
38
  },
37
39
  "main": "dist/nodes/agent.js",
38
40
  "dependencies": {
39
- "@themoltnet/sdk": "^0.112.0"
41
+ "@themoltnet/sdk": "0.113.0"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@types/node": "^22.19.0",