@themoltnet/node-red-contrib-core 0.7.1 → 0.9.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
@@ -213,12 +213,32 @@ model-specialization option.
213
213
 
214
214
  [`examples/ab-eval-with-judge.flow.json`](./examples/ab-eval-with-judge.flow.json)
215
215
  imports a reusable **A/B eval with judge** subflow plus a small demo tab. The
216
- subflow runs `run_eval`, locally scores required fields/findings, then creates a
217
- `judge_eval_attempt` task and stores per-variant scores/deltas in flow context.
216
+ subflow runs one `run_eval` producer lane, records lightweight producer
217
+ metadata, then creates one `judge_eval_attempt` task and stores the variant
218
+ score/delta in flow context. The judge owns the rubric; the producer-side flow
219
+ does not duplicate hidden criteria. The demo tab owns the reusable workflow
220
+ runner pattern: initialize one correlation id, fan out configured variants in
221
+ parallel, record successful or failed lanes, and emit a group result only once
222
+ all expected variants have settled.
223
+
224
+ The bundled seed uses `evals/moltnet-practices/dbos-after-commit` and compares
225
+ `baseline-no-context` against `rendered-pack-dbos-rule`, a rendered-pack-style
226
+ context excerpt that teaches the DBOS/Drizzle transaction boundary. Replace
227
+ that inline context with a rendered MoltNet pack or skill content to evaluate a
228
+ real candidate context source against the same hidden judge rubric.
218
229
 
219
230
  Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
220
- included but blank: leave them blank for any eligible daemon to claim both
221
- tasks, or set producer/judge profile IDs and run one daemon per profile.
231
+ included and default to the shared Gemma eval profile; clear them to let any
232
+ eligible daemon claim both tasks, or set producer/judge profile IDs and run one
233
+ daemon per profile. The example uses `maxAttempts=2` for producer and judge
234
+ tasks so transient tool/model failures retry at the task layer before the lane
235
+ is marked failed.
236
+ Callers can also route per scenario or variant without editing the subflow:
237
+ set `msg.evalRuntimeProfiles.producer`, `msg.evalRuntimeProfiles.judge`,
238
+ `msg.evalScenario.runtimeProfiles.producer`, `msg.evalScenario.runtimeProfiles.judge`,
239
+ or the variant-level `runtimeProfile` / `producerRuntimeProfile` /
240
+ `judgeRuntimeProfile`. Each value may be a profile id string, `{ profileId }`,
241
+ or a full `allowedProfiles` array.
222
242
 
223
243
  ## Freeform deep review workflow
224
244
 
@@ -309,6 +329,75 @@ in the Node-RED instance) to get a live workflow cockpit without any custom Vue
309
329
  widget. A starter flow is in [`examples/cockpit.flow.json`](./examples/cockpit.flow.json)
310
330
  (inject → `moltnet-workflow-status` → debug; add a `ui-table` to visualize).
311
331
 
332
+ ## Axiom alert triage workflow
333
+
334
+ [`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json)
335
+ is an observability triage experiment with two trigger modes:
336
+
337
+ `Axiom monitor history polling` → **parser/enricher task** →
338
+ **codebase triage task** → **judge task**.
339
+
340
+ When Axiom custom webhook notifiers are available, the same triage chain can also
341
+ start from `POST /axiom/monitor`.
342
+
343
+ The polling path calls Axiom monitor history with `AXIOM_MONITORING_TOKEN`
344
+ (`AXIOM_API_TOKEN` is accepted as a fallback for local experiments), dedupes
345
+ events by monitor/state/timestamp in flow context, starts work only for
346
+ trigger-like events, builds task bodies through `task: build`, and chains tasks
347
+ through `task: read` `artifactRef` values so large telemetry artifacts stay in
348
+ MoltNet. Resolution/no-op events go to a debug lane instead of starting codebase
349
+ triage.
350
+
351
+ Polling environment:
352
+
353
+ - `AXIOM_MONITORING_TOKEN`: Axiom token with monitor history/read access and,
354
+ for the current single-token experiment, telemetry query read access.
355
+ - `AXIOM_MONITOR_IDS`: optional comma-separated monitor ids. Defaults to the
356
+ MoltNet monitors created by `infra/axiom/monitors/apply.mjs`.
357
+ - `AXIOM_POLL_WINDOW_MINUTES`: optional lookback window, default `15`.
358
+
359
+ To test the flow without a real Axiom trigger, use the **inject synthetic
360
+ alert** node in the example. It builds the same normalized `msg.axiom` shape as
361
+ the polling/webhook paths, marks it with `synthetic: true`, and routes it through
362
+ the common monitor-events link so the parser, triage, artifact chaining, and
363
+ judge stages all run.
364
+
365
+ Optional Axiom custom webhook setup:
366
+
367
+ - URL: `https://<node-red-host>/axiom/monitor`
368
+ - Method: `POST`
369
+ - Optional secret header: `x-axiom-webhook-secret: <secret>`
370
+ - Node-RED env: `AXIOM_WEBHOOK_SECRET=<secret>` when that header is used
371
+
372
+ Suggested custom webhook body:
373
+
374
+ ```json
375
+ {
376
+ "action": "{{.Action}}",
377
+ "body": "{{.Body}}",
378
+ "endTime": "now",
379
+ "monitorId": "{{.MonitorID}}",
380
+ "monitorName": "{{.Title}}",
381
+ "service": "moltnet-rest-api",
382
+ "severity": "warning",
383
+ "startTime": "now-30m",
384
+ "value": "{{.Value}}"
385
+ }
386
+ ```
387
+
388
+ The parser/enricher task prompt asks the agent to use the repo-local
389
+ `axiom-alert-triage` skill and produce a JSON artifact titled
390
+ `telemetry-evidence`. The next task consumes that artifact, compares findings
391
+ against the `getlarge/themoltnet` codebase, GitHub issues, and MoltNet entries,
392
+ then writes `triage-report`. The final judge task writes `triage-judgment`.
393
+
394
+ Before enabling live webhooks, refine Axiom monitors so broad auth/client noise
395
+ (401/404) is separated from actionable alerts such as 5xx errors, latency
396
+ regression, event loop pressure, and memory pressure. The companion repo-local
397
+ `axiom-observability` skill and [`infra/axiom`](../../infra/axiom) document the
398
+ monitor/dashboard maintenance conventions and provide idempotent apply scripts
399
+ for the committed Axiom config.
400
+
312
401
  ## Build
313
402
 
314
403
  ```bash
@@ -369,7 +458,9 @@ export default {
369
458
  Open the editor, drag in `agent` + the task nodes, or import
370
459
  [`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json) or
371
460
  [`examples/cockpit.flow.json`](./examples/cockpit.flow.json) or
372
- [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json),
461
+ [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
462
+ or
463
+ [`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json),
373
464
  then fill the agent's `clientId`/`clientSecret`.
374
465
 
375
466
  If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
package/dist/nodes/src.js CHANGED
@@ -1927,7 +1927,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1927
1927
  ...options
1928
1928
  });
1929
1929
  /**
1930
- * Delete terminal tasks in bulk. Safe mode skips live, unauthorized, missing, and protected tasks.
1930
+ * Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
1931
1931
  */
1932
1932
  var batchDeleteTasks = (options) => (options.client ?? client).delete({
1933
1933
  security: [
@@ -14172,6 +14172,86 @@ var PRODUCER_TASK_TYPES = new Set([
14172
14172
  "render_pack",
14173
14173
  "run_eval"
14174
14174
  ]);
14175
+ function isNonEmptyString(value) {
14176
+ return typeof value === "string" && value.length > 0;
14177
+ }
14178
+ function criterionWeight(criterion, index) {
14179
+ if (typeof criterion.weight === "number") return criterion.weight;
14180
+ if (typeof criterion.max_score === "number") return criterion.max_score / 100;
14181
+ if (typeof criterion.maxScore === "number") return criterion.maxScore / 100;
14182
+ throw new TaskBuildError([{
14183
+ field: `successCriteria/rubric/criteria/${index}/weight`,
14184
+ message: "criterion is missing weight or max_score"
14185
+ }]);
14186
+ }
14187
+ /**
14188
+ * Normalize authoring-time rubric criteria to canonical MoltNet rubric
14189
+ * criteria. Accepts `{id,title,description,weight}` and
14190
+ * `{name,description,max_score}` style inputs, strips authoring-only fields,
14191
+ * and fills a default scoring mode.
14192
+ */
14193
+ function normalizeRubricCriteria(criteria, options) {
14194
+ const errors = [];
14195
+ const normalized = criteria.map((criterion, index) => {
14196
+ const id = criterion.id ?? criterion.name;
14197
+ const description = criterion.description ?? criterion.title;
14198
+ if (!isNonEmptyString(id)) errors.push({
14199
+ field: `successCriteria/rubric/criteria/${index}/id`,
14200
+ message: "criterion is missing id or name"
14201
+ });
14202
+ if (!isNonEmptyString(description)) errors.push({
14203
+ field: `successCriteria/rubric/criteria/${index}/description`,
14204
+ message: "criterion is missing description or title"
14205
+ });
14206
+ return {
14207
+ id: id ?? "",
14208
+ description: description ?? "",
14209
+ weight: criterionWeight(criterion, index),
14210
+ scoring: criterion.scoring ?? options?.scoring ?? "llm_score"
14211
+ };
14212
+ });
14213
+ if (errors.length > 0) throw new TaskBuildError(errors);
14214
+ return normalized;
14215
+ }
14216
+ /**
14217
+ * Build a canonical `SuccessCriteria` envelope from rubric/checklist-style
14218
+ * criteria. This keeps rubrics readable at the authoring boundary while
14219
+ * preserving the strict task schema on the wire.
14220
+ */
14221
+ function buildRubricSuccessCriteria(options) {
14222
+ const rubric = {
14223
+ rubricId: options.rubricId,
14224
+ version: options.version ?? "v1",
14225
+ criteria: normalizeRubricCriteria(options.criteria, { scoring: options.scoring }),
14226
+ ...options.contentHash ? { contentHash: options.contentHash } : {},
14227
+ ...options.preamble ? { preamble: options.preamble } : {},
14228
+ ...options.scope ? { scope: options.scope } : {}
14229
+ };
14230
+ const weightError = validateRubricWeights(rubric);
14231
+ if (weightError) throw new TaskBuildError([{
14232
+ field: "successCriteria/rubric/criteria",
14233
+ message: weightError
14234
+ }]);
14235
+ return {
14236
+ version: 1,
14237
+ rubric
14238
+ };
14239
+ }
14240
+ function resolveJudgeEvalAttemptTarget(target) {
14241
+ if ("judgeEvalTarget" in target && typeof target.judgeEvalTarget === "function") return target.judgeEvalTarget();
14242
+ if ("targetTaskId" in target) return {
14243
+ targetTaskId: target.targetTaskId,
14244
+ targetAttemptN: target.targetAttemptN
14245
+ };
14246
+ if ("taskId" in target) return {
14247
+ targetTaskId: target.taskId,
14248
+ targetAttemptN: target.accepted?.attemptN ?? target.attemptN ?? 1
14249
+ };
14250
+ throw new TaskBuildError([{
14251
+ field: "target",
14252
+ message: "judge_eval_attempt target is missing task id"
14253
+ }]);
14254
+ }
14175
14255
  /**
14176
14256
  * Fluent, network-free builder for a `tasks.create` body. Encodes the
14177
14257
  * non-obvious task schema (context arrays, success-criteria gates,
@@ -14622,6 +14702,20 @@ function buildJudgeEvalAttempt(input) {
14622
14702
  return buildTask("judge_eval_attempt", input);
14623
14703
  }
14624
14704
  /**
14705
+ * Build a `judge_eval_attempt` task from an accepted `run_eval` result (or a
14706
+ * small target tuple) plus human-friendly rubric criteria.
14707
+ *
14708
+ * @param target - A `TaskResultReader` or `{targetTaskId,targetAttemptN}` tuple.
14709
+ * @param options - Rubric metadata and eval/checklist-style criteria.
14710
+ * @returns A typed {@link TaskBuilder}.
14711
+ */
14712
+ function buildJudgeEvalAttemptForRunEval(target, options) {
14713
+ return buildJudgeEvalAttempt({
14714
+ ...resolveJudgeEvalAttemptTarget(target),
14715
+ successCriteria: buildRubricSuccessCriteria(options)
14716
+ });
14717
+ }
14718
+ /**
14625
14719
  * Build a `pr_review` task. Requires `subject` + `successCriteria`. Note the
14626
14720
  * rubric criteria must use `boolean` scoring for this task type.
14627
14721
  *
@@ -14659,7 +14753,9 @@ var TaskResultReader = class {
14659
14753
  accepted;
14660
14754
  /** Token / cost usage for the accepted attempt, if reported. */
14661
14755
  usage;
14756
+ /** Task id for the task whose accepted attempt is being read. */
14662
14757
  taskId;
14758
+ /** CID of the accepted attempt output. */
14663
14759
  outputCid;
14664
14760
  constructor(task, attempt) {
14665
14761
  const errors = [];
@@ -14752,6 +14848,19 @@ var TaskResultReader = class {
14752
14848
  };
14753
14849
  }
14754
14850
  /**
14851
+ * Return the target tuple required by `judge_eval_attempt`.
14852
+ *
14853
+ * This intentionally uses the accepted attempt number, not merely the
14854
+ * attempt object passed to the reader, so a downstream judge is pinned to
14855
+ * the producer output that the task accepted.
14856
+ */
14857
+ judgeEvalTarget() {
14858
+ return {
14859
+ targetTaskId: this.taskId,
14860
+ targetAttemptN: this.accepted.attemptN
14861
+ };
14862
+ }
14863
+ /**
14755
14864
  * Build a `TaskRef` that anchors a downstream task to this accepted output
14756
14865
  * and points at one persistent task artifact by CID.
14757
14866
  *
@@ -14889,6 +14998,7 @@ function createTasksNamespace(context) {
14889
14998
  buildAssessBrief,
14890
14999
  buildJudgePack,
14891
15000
  buildJudgeEvalAttempt,
15001
+ buildJudgeEvalAttemptForRunEval,
14892
15002
  buildPrReview,
14893
15003
  async readResult(taskOrId) {
14894
15004
  const task = typeof taskOrId === "string" ? unwrapResult(await getTask({
@@ -17223,4 +17333,4 @@ if (!etc.sha512Sync) etc.sha512Sync = (...m) => {
17223
17333
  return hash.digest();
17224
17334
  };
17225
17335
  //#endregion
17226
- export { TaskResultError as a, TaskBuildError as i, createResultReader as n, buildTask as r, connect as t };
17336
+ export { TaskBuildError as a, buildTask as i, createResultReader as n, TaskResultError as o, buildJudgeEvalAttemptForRunEval as r, connect as t };
@@ -61,6 +61,8 @@
61
61
  >
62
62
  <select id="node-input-taskType">
63
63
  <option value="freeform">freeform</option>
64
+ <option value="run_eval">run_eval</option>
65
+ <option value="judge_eval_attempt">judge_eval_attempt</option>
64
66
  </select>
65
67
  </div>
66
68
  <div class="form-row">
@@ -1,10 +1,13 @@
1
- import { i as TaskBuildError, r as buildTask } from "./src.js";
1
+ import { a as TaskBuildError, i as buildTask, r as buildJudgeEvalAttemptForRunEval } 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) {
5
5
  if (!raw) return [];
6
6
  return raw.split(",").map((s) => s.trim()).filter(Boolean);
7
7
  }
8
+ function isRuntimeProfileRef(value) {
9
+ return typeof value === "object" && value !== null && typeof value.profileId === "string";
10
+ }
8
11
  /** Resolve a context mapping's raw value from the message / context stores / literal. */
9
12
  function resolveValue(RED, node, msg, m) {
10
13
  switch (m.valueType) {
@@ -49,8 +52,12 @@ var init = (RED) => {
49
52
  this.on("input", (msg, send, done) => {
50
53
  try {
51
54
  const payloadInput = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
52
- const brief = typeof payloadInput.input?.brief === "string" ? payloadInput.input.brief : def.brief ?? "";
53
- const builder = buildTask(def.taskType?.trim() || "freeform", { brief });
55
+ const taskType = typeof payloadInput.taskType === "string" && payloadInput.taskType ? payloadInput.taskType : def.taskType?.trim() || "freeform";
56
+ const inputData = payloadInput.input && typeof payloadInput.input === "object" ? payloadInput.input : taskType === "freeform" || taskType === "fulfill_brief" ? { brief: def.brief ?? "" } : {};
57
+ const builder = taskType === "judge_eval_attempt" && payloadInput.judgeRubric && typeof payloadInput.judgeRubric === "object" ? buildJudgeEvalAttemptForRunEval({
58
+ targetTaskId: inputData.targetTaskId,
59
+ targetAttemptN: inputData.targetAttemptN
60
+ }, payloadInput.judgeRubric) : buildTask(taskType, inputData);
54
61
  const teamId = resolveOverride(RED, this, msg, def.teamId, def.teamIdType) ?? payloadInput.teamId ?? agentNode?.teamId;
55
62
  const diaryId = resolveOverride(RED, this, msg, def.diaryId, def.diaryIdType) ?? payloadInput.diaryId ?? agentNode?.diaryId;
56
63
  if (teamId) builder.team(teamId);
@@ -81,6 +88,12 @@ var init = (RED) => {
81
88
  if (title) builder.title(title);
82
89
  const tags = Array.isArray(payloadInput.tags) ? payloadInput.tags : parseCsv(def.tags);
83
90
  if (tags.length > 0) builder.tags(...tags);
91
+ if (typeof payloadInput.correlationId === "string" && payloadInput.correlationId) builder.correlationId(payloadInput.correlationId);
92
+ if (typeof payloadInput.maxAttempts === "number") builder.maxAttempts(payloadInput.maxAttempts);
93
+ if (Array.isArray(payloadInput.allowedProfiles)) {
94
+ const allowedProfiles = payloadInput.allowedProfiles.filter(isRuntimeProfileRef);
95
+ if (allowedProfiles.length > 0) builder.allowProfiles(...allowedProfiles);
96
+ }
84
97
  const built = builder.build();
85
98
  const out = RED.util.cloneMessage(msg);
86
99
  out.payload = {
@@ -1,4 +1,4 @@
1
- import { a as TaskResultError, n as createResultReader } from "./src.js";
1
+ import { n as createResultReader, o as TaskResultError } from "./src.js";
2
2
  //#region src/nodes/task-reader.ts
3
3
  var init = (RED) => {
4
4
  function TaskReaderNode(def) {