@themoltnet/node-red-contrib-core 0.11.0 → 0.12.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,12 @@ 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 eleven
19
- **action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
20
- `moltnet-task-wait`, `moltnet-task-artifacts-list`,
21
- `moltnet-task-artifact-upload`, `moltnet-task-artifact-download`,
18
+ - Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and the
19
+ MoltNet **action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
20
+ `moltnet-task-cancel`, `moltnet-task-wait`, `moltnet-task-artifacts-list`,
21
+ `moltnet-task-artifact-stage`, `moltnet-task-artifact-upload`,
22
+ `moltnet-task-artifact-download`, `moltnet-runtime-session-get`,
23
+ `moltnet-runtime-session-upload`, `moltnet-runtime-session-download`,
22
24
  `moltnet-workflow-status`, `moltnet-task-builder`, `moltnet-task-reader`,
23
25
  `moltnet-tasks-list`, `moltnet-entries-search`)
24
26
  register and appear in the palette.
@@ -80,10 +82,17 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
80
82
  `content`, `body`, or base64 `contentBase64`. Team context is the configured
81
83
  node/agent by default; message team overrides require an explicit checkbox.
82
84
  Emits artifact metadata on `msg.payload` and `msg.artifact`.
85
+ - **`moltnet-task-artifact-stage`** (palette: _task artifact: stage_) — stages
86
+ bytes for later binding as a task input artifact. It accepts the same payload
87
+ shapes and team controls as upload, but needs no task or attempt. The returned
88
+ metadata is emitted on `msg.payload` and `msg.artifact`; pass it to a task
89
+ builder's **References from** field.
83
90
  - **`moltnet-task-artifact-download`** (palette: _task artifact: download_) —
84
- downloads an artifact by task id, attempt number, and CID. Emits the artifact
85
- bytes as a Buffer on `msg.payload` and metadata on `msg.artifact`. Upload and
86
- download nodes enforce a local 25 MiB byte limit by default.
91
+ downloads an artifact by task id and CID. Set an attempt number for an exact
92
+ attempt artifact, or omit it to resolve the CID across the task, including a
93
+ bound input artifact. Emits the bytes as a Buffer on `msg.payload` and
94
+ metadata on `msg.artifact`. Stage, upload, and download nodes enforce a local
95
+ 25 MiB byte limit by default.
87
96
  - **`moltnet-workflow-status`** (palette: _workflow: status_) — reads the tasks of
88
97
  one workflow run (by `correlationId`) and emits a table-shaped `msg.payload`
89
98
  (array of `{ taskId, type, title, status, queuedAt, completedAt }`) plus
@@ -92,9 +101,10 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
92
101
  `tasks.create` body from the SDK fluent builder (`buildFreeform`). Pure offline
93
102
  transform: reads `teamId`/`diaryId` from the referenced `agent` (with optional
94
103
  typedInput overrides), maps **context rows** (slug ← msg/flow/global/str/json),
95
- binds a prior task's output via **References from** (a `msg`-path to an
96
- `outputRef` from `task: read`), and toggles the **submit-output** / schema
97
- gates. Emits the flat body on `msg.payload` for a downstream `tasks: create`
104
+ binds a prior task's output or staged input artifact via **References from**
105
+ (a `msg`-path to an `outputRef` from `task: read` or metadata from
106
+ `task artifact: stage`), and toggles the **submit-output** / schema gates.
107
+ Emits the flat body on `msg.payload` for a downstream `tasks: create`
98
108
  (the SDK's `{ body, teamId }` envelope is flattened to `{ ...body, teamId }`).
99
109
  Validation errors surface on the node (red ring).
100
110
  - **`moltnet-task-reader`** (palette: _task: read_) — parses a completed snapshot
@@ -1,6 +1,6 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
2
  import { t as bool } from "./query-utils.js";
3
- import { a as requireAttemptContext, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
3
+ import { a as requireAttemptContext, c as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
4
4
  //#region src/nodes/runtime-session-download.ts
5
5
  var init = (RED) => {
6
6
  function RuntimeSessionDownloadNode(def) {
@@ -1,6 +1,6 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
2
  import { n as compact, t as bool } from "./query-utils.js";
3
- import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
3
+ import { a as requireAttemptContext, c as resolveMaxBytes, s as resolveField, u as resolveUploadBody } from "./task-artifact-utils.js";
4
4
  //#region src/nodes/runtime-session-upload.ts
5
5
  var init = (RED) => {
6
6
  function RuntimeSessionUploadNode(def) {
package/dist/nodes/src.js CHANGED
@@ -1927,7 +1927,34 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1927
1927
  ...options
1928
1928
  });
1929
1929
  /**
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.
1930
+ * Stage immutable content-addressed artifact bytes for later binding as task input artifacts via task creation references. Creates no metadata row; staged bytes are not downloadable until bound to a task, and unbound objects are garbage-collected after a grace window.
1931
+ */
1932
+ var stageTaskArtifact = (options) => (options.client ?? client).put({
1933
+ bodySerializer: null,
1934
+ security: [
1935
+ {
1936
+ scheme: "bearer",
1937
+ type: "http"
1938
+ },
1939
+ {
1940
+ name: "X-Moltnet-Session-Token",
1941
+ type: "apiKey"
1942
+ },
1943
+ {
1944
+ in: "cookie",
1945
+ name: "ory_kratos_session",
1946
+ type: "apiKey"
1947
+ }
1948
+ ],
1949
+ url: "/task-artifacts/staged",
1950
+ ...options,
1951
+ headers: {
1952
+ "Content-Type": "application/octet-stream",
1953
+ ...options.headers
1954
+ }
1955
+ });
1956
+ /**
1957
+ * Queue asynchronous deletion of waiting, queued, and terminal tasks in bulk. By default, dispatched, running, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
1931
1958
  */
1932
1959
  var batchDeleteTasks = (options) => (options.client ?? client).delete({
1933
1960
  security: [
@@ -9379,12 +9406,15 @@ function Evaluate(type, options = {}) {
9379
9406
  * V1 bindings only; Tier-2 (reference_file, mcp_resource, imported_file,
9380
9407
  * tool_response_seed, additional_context_hook) ship in a later slice.
9381
9408
  */
9382
- var ContextBinding = Union([
9383
- Literal("skill"),
9384
- Literal("context_inline"),
9385
- Literal("prompt_prefix"),
9386
- Literal("user_inline")
9387
- ], { $id: "ContextBinding" });
9409
+ var CONTEXT_BINDINGS = [
9410
+ "skill",
9411
+ "context_inline",
9412
+ "prompt_prefix",
9413
+ "user_inline"
9414
+ ];
9415
+ /** Maximum UTF-16 code units accepted in one ContextRef content field. */
9416
+ var CONTEXT_REF_MAX_CONTENT_LENGTH = 65536;
9417
+ var ContextBinding = Unsafe(Union(CONTEXT_BINDINGS.map((binding) => Literal(binding)), { $id: "ContextBinding" }));
9388
9418
  /** Reusable input fragment for any task type. Soft cap at 5 items. */
9389
9419
  var TaskContext = _Array_(_Object_({
9390
9420
  slug: String$1({
@@ -9395,7 +9425,7 @@ var TaskContext = _Array_(_Object_({
9395
9425
  binding: ContextBinding,
9396
9426
  content: String$1({
9397
9427
  minLength: 1,
9398
- maxLength: 65536
9428
+ maxLength: CONTEXT_REF_MAX_CONTENT_LENGTH
9399
9429
  })
9400
9430
  }, {
9401
9431
  $id: "ContextRef",
@@ -9633,6 +9663,11 @@ var RuntimeProfileSandboxResumeCommand = Union([String$1({
9633
9663
  maximum: 6e4
9634
9664
  }))
9635
9665
  }, { additionalProperties: false })]);
9666
+ var RuntimeProfileAllowedHost = String$1({
9667
+ minLength: 1,
9668
+ maxLength: 255,
9669
+ pattern: "^(?:\\*\\.)?(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$"
9670
+ });
9636
9671
  var RuntimeProfileSandbox = _Object_({
9637
9672
  snapshot: Optional(_Object_({
9638
9673
  setupCommands: Optional(_Array_(String$1({
@@ -9649,6 +9684,10 @@ var RuntimeProfileSandbox = _Object_({
9649
9684
  pattern: "^[0-9]+[KMGTP]?$"
9650
9685
  }))
9651
9686
  }, { additionalProperties: false })),
9687
+ network: Optional(_Object_({
9688
+ allowedHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 })),
9689
+ allowedInternalHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 }))
9690
+ }, { additionalProperties: false })),
9652
9691
  resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
9653
9692
  vfs: Optional(_Object_({
9654
9693
  shadow: Optional(_Array_(String$1({
@@ -9992,7 +10031,7 @@ _Object_({
9992
10031
  * (server-side schema check). Self-assessment is a truthful self-rating,
9993
10032
  * NOT enforcement — `verification.passed=false` does not block /complete
9994
10033
  * and does not affect `acceptedAttemptN`. See
9995
- * `docs/understand/agent-runtime.md` for the full producer/judge flow.
10034
+ * `docs/use/tasks-and-runtime.md` for the full producer/judge flow.
9996
10035
  *
9997
10036
  * **Binding evaluation** (judgment tasks: `assess_brief`, `judge_pack`).
9998
10037
  * A separate task whose IS the application of `successCriteria` to
@@ -10108,7 +10147,7 @@ _Object_({
10108
10147
  id: String$1({ format: "uuid" }),
10109
10148
  teamId: String$1({ format: "uuid" }),
10110
10149
  taskId: String$1({ format: "uuid" }),
10111
- attemptN: Integer({ minimum: 1 }),
10150
+ attemptN: Union([Integer({ minimum: 1 }), Null()]),
10112
10151
  kind: String$1({
10113
10152
  minLength: 1,
10114
10153
  maxLength: 100
@@ -10130,7 +10169,7 @@ _Object_({
10130
10169
  minLength: 1,
10131
10170
  maxLength: 100
10132
10171
  }),
10133
- createdByAgentId: String$1({ format: "uuid" }),
10172
+ createdByAgentId: Union([String$1({ format: "uuid" }), Null()]),
10134
10173
  expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10135
10174
  createdAt: String$1({ format: "date-time" })
10136
10175
  }, { $id: "TaskArtifact" })),
@@ -10146,6 +10185,16 @@ _Object_({
10146
10185
  $id: "ListTaskArtifactsQuery",
10147
10186
  additionalProperties: false
10148
10187
  });
10188
+ var HeaderSafeContentType = String$1({
10189
+ minLength: 1,
10190
+ maxLength: 200,
10191
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10192
+ });
10193
+ var HeaderSafeContentEncoding = String$1({
10194
+ minLength: 1,
10195
+ maxLength: 100,
10196
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10197
+ });
10149
10198
  _Object_({
10150
10199
  kind: String$1({
10151
10200
  minLength: 1,
@@ -10155,14 +10204,8 @@ _Object_({
10155
10204
  minLength: 1,
10156
10205
  maxLength: 255
10157
10206
  }),
10158
- contentType: Optional(String$1({
10159
- minLength: 1,
10160
- maxLength: 200
10161
- })),
10162
- contentEncoding: Optional(String$1({
10163
- minLength: 1,
10164
- maxLength: 100
10165
- }))
10207
+ contentType: Optional(HeaderSafeContentType),
10208
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10166
10209
  }, {
10167
10210
  $id: "UploadTaskArtifactQuery",
10168
10211
  additionalProperties: false
@@ -10194,6 +10237,34 @@ _Object_({
10194
10237
  $id: "TaskArtifactContentParams",
10195
10238
  additionalProperties: false
10196
10239
  });
10240
+ _Object_({
10241
+ contentType: Optional(HeaderSafeContentType),
10242
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10243
+ }, {
10244
+ $id: "StageTaskArtifactQuery",
10245
+ additionalProperties: false
10246
+ });
10247
+ _Object_({
10248
+ cid: String$1({
10249
+ minLength: 1,
10250
+ maxLength: 100
10251
+ }),
10252
+ sizeBytes: Integer({ minimum: 0 }),
10253
+ contentType: String$1({
10254
+ minLength: 1,
10255
+ maxLength: 200
10256
+ })
10257
+ }, { $id: "StagedTaskArtifact" });
10258
+ _Object_({
10259
+ taskId: String$1({ format: "uuid" }),
10260
+ cid: String$1({
10261
+ minLength: 1,
10262
+ maxLength: 100
10263
+ })
10264
+ }, {
10265
+ $id: "TaskArtifactTaskContentParams",
10266
+ additionalProperties: false
10267
+ });
10197
10268
  new TextEncoder();
10198
10269
  new TextDecoder();
10199
10270
  //#endregion
@@ -10707,6 +10778,27 @@ var JudgeEvalAttemptInput = _Object_({
10707
10778
  $id: "JudgeEvalAttemptInput",
10708
10779
  additionalProperties: false
10709
10780
  });
10781
+ /** Agent-authored part of a judge attempt's output. */
10782
+ var JudgeEvalAttemptSubmission = _Object_({
10783
+ targetTaskId: String$1({ format: "uuid" }),
10784
+ targetAttemptN: Integer({ minimum: 1 }),
10785
+ variantLabel: String$1({
10786
+ minLength: 1,
10787
+ maxLength: 64,
10788
+ pattern: "^(?!.* - ).*$"
10789
+ }),
10790
+ scores: _Array_(JudgePackScore, { minItems: 1 }),
10791
+ composite: Number$1({
10792
+ minimum: 0,
10793
+ maximum: 1
10794
+ }),
10795
+ verdict: String$1({ minLength: 1 }),
10796
+ judgeModel: Optional(String$1({ minLength: 1 }))
10797
+ }, {
10798
+ $id: "JudgeEvalAttemptSubmission",
10799
+ additionalProperties: false
10800
+ });
10801
+ /** Durable output after the executor stamps the claim trace context. */
10710
10802
  var JudgeEvalAttemptOutput = _Object_({
10711
10803
  targetTaskId: String$1({ format: "uuid" }),
10712
10804
  targetAttemptN: Integer({ minimum: 1 }),
@@ -10722,7 +10814,7 @@ var JudgeEvalAttemptOutput = _Object_({
10722
10814
  }),
10723
10815
  verdict: String$1({ minLength: 1 }),
10724
10816
  judgeModel: Optional(String$1({ minLength: 1 })),
10725
- traceparent: String$1({ minLength: 1 })
10817
+ traceparent: Optional(String$1({ minLength: 1 }))
10726
10818
  }, {
10727
10819
  $id: "JudgeEvalAttemptOutput",
10728
10820
  additionalProperties: false
@@ -11014,15 +11106,33 @@ var RunEvalInput = _Object_({
11014
11106
  $id: "RunEvalInput",
11015
11107
  additionalProperties: false
11016
11108
  });
11109
+ var RunEvalArtifact = _Object_({
11110
+ path: String$1({ minLength: 1 }),
11111
+ cid: String$1({ minLength: 1 })
11112
+ }, { additionalProperties: false });
11113
+ /**
11114
+ * Fields the eval agent authors through its submit-output tool. Runtime
11115
+ * telemetry deliberately does not live here: an agent cannot truthfully
11116
+ * measure provider token usage, wall-clock duration, or the claim trace.
11117
+ */
11118
+ var RunEvalSubmission = _Object_({
11119
+ response: String$1({ minLength: 1 }),
11120
+ artifacts: Optional(_Array_(RunEvalArtifact)),
11121
+ verification: Optional(VerificationRecord)
11122
+ }, {
11123
+ $id: "RunEvalSubmission",
11124
+ additionalProperties: false
11125
+ });
11126
+ /**
11127
+ * Durable eval output. The daemon materializes this from RunEvalSubmission
11128
+ * and observed execution metadata before the task service accepts it.
11129
+ */
11017
11130
  var RunEvalOutput = _Object_({
11018
11131
  response: String$1({ minLength: 1 }),
11019
- artifacts: Optional(_Array_(_Object_({
11020
- path: String$1({ minLength: 1 }),
11021
- cid: String$1({ minLength: 1 })
11022
- }, { additionalProperties: false }))),
11132
+ artifacts: Optional(_Array_(RunEvalArtifact)),
11023
11133
  totalTokens: Integer({ minimum: 0 }),
11024
11134
  durationMs: Integer({ minimum: 0 }),
11025
- traceparent: String$1({ minLength: 1 }),
11135
+ traceparent: Optional(String$1({ minLength: 1 })),
11026
11136
  verification: Optional(VerificationRecord)
11027
11137
  }, {
11028
11138
  $id: "RunEvalOutput",
@@ -11167,6 +11277,7 @@ var BUILT_IN_TASK_TYPES = {
11167
11277
  name: RUN_EVAL_TYPE,
11168
11278
  inputSchema: RunEvalInput,
11169
11279
  outputSchema: RunEvalOutput,
11280
+ submissionSchema: RunEvalSubmission,
11170
11281
  outputKind: "artifact",
11171
11282
  resumable: true,
11172
11283
  workspaceScope: "session",
@@ -11179,6 +11290,7 @@ var BUILT_IN_TASK_TYPES = {
11179
11290
  name: JUDGE_EVAL_ATTEMPT_TYPE,
11180
11291
  inputSchema: JudgeEvalAttemptInput,
11181
11292
  outputSchema: JudgeEvalAttemptOutput,
11293
+ submissionSchema: JudgeEvalAttemptSubmission,
11182
11294
  outputKind: "judgment",
11183
11295
  workspaceScope: "attempt",
11184
11296
  sessionScope: "none",
@@ -13755,22 +13867,33 @@ function validateTaskInput(taskType, input) {
13755
13867
  }
13756
13868
  return [];
13757
13869
  }
13758
- function validateTaskOutput(taskType, output, input) {
13870
+ function checkVerificationInputCid(value, runtime) {
13871
+ const verification = value !== null && typeof value === "object" ? value.verification : void 0;
13872
+ if (runtime?.inputCid && verification !== void 0 && verification.inputCid !== runtime.inputCid) return [{
13873
+ field: "output/verification/inputCid",
13874
+ message: "must match the task input CID"
13875
+ }];
13876
+ return [];
13877
+ }
13878
+ function validateTaskResult(taskType, value, input, runtime, submission = false) {
13759
13879
  const entry = getTaskTypeEntry(taskType);
13760
13880
  if (!entry) return [{
13761
13881
  field: "taskType",
13762
13882
  message: `Unknown task type: ${taskType}`
13763
13883
  }];
13764
- const errors = schemaErrors("output", entry.outputSchema, output);
13884
+ const errors = schemaErrors("output", submission ? entry.submissionSchema ?? entry.outputSchema : entry.outputSchema, value);
13765
13885
  if (errors.length > 0) return errors;
13766
13886
  if (entry.validateOutput) {
13767
- const validationError = entry.validateOutput(output, input);
13887
+ const validationError = entry.validateOutput(value, input);
13768
13888
  if (validationError) return [{
13769
13889
  field: "output",
13770
13890
  message: validationError
13771
13891
  }];
13772
13892
  }
13773
- return [];
13893
+ return checkVerificationInputCid(value, runtime);
13894
+ }
13895
+ function validateTaskOutput(taskType, output, input, runtime) {
13896
+ return validateTaskResult(taskType, output, input, runtime);
13774
13897
  }
13775
13898
  /**
13776
13899
  * Resolve the TypeBox output schema registered for `taskType`. Returns
@@ -13793,6 +13916,42 @@ function validateTaskCreateRequest(args) {
13793
13916
  field: "references",
13794
13917
  message: `At least one reference is required for task type: ${args.taskType}`
13795
13918
  });
13919
+ errors.push(...validateTaskReferences(args.references));
13920
+ return errors;
13921
+ }
13922
+ /**
13923
+ * Cross-field rules for the reference shapes the schema alone cannot
13924
+ * express. Every reference must be exactly one of:
13925
+ *
13926
+ * - **task output ref**: taskId set, outputCid required; an optional
13927
+ * artifact must name its producing attempt (attemptN).
13928
+ * - **input artifact ref**: taskId null, artifact without attemptN;
13929
+ * outputCid omitted (or equal to artifact.cid when sent) -- the bytes
13930
+ * were staged before any task existed, so there is no output to name.
13931
+ * - **external ref**: taskId null, external present, no artifact.
13932
+ *
13933
+ * Anything else used to be silently persisted into taskRefs and never
13934
+ * materialized; now it fails fast with a field error.
13935
+ */
13936
+ function validateTaskReferences(references) {
13937
+ const errors = [];
13938
+ (references ?? []).forEach((ref, index) => {
13939
+ const invalid = (message) => errors.push({
13940
+ field: `references[${index}]`,
13941
+ message
13942
+ });
13943
+ if (ref.taskId !== null) {
13944
+ if (ref.outputCid === void 0) invalid("outputCid is required when referencing a task output");
13945
+ if (ref.artifact && ref.artifact.attemptN === void 0) invalid("artifact references to another task must include attemptN; input artifacts are referenced with taskId null");
13946
+ return;
13947
+ }
13948
+ if (ref.artifact) {
13949
+ if (ref.artifact.attemptN !== void 0) invalid("input artifact references (taskId null) must not include attemptN; reference the producing task by id instead");
13950
+ if (ref.outputCid !== void 0 && ref.outputCid !== ref.artifact.cid) invalid("outputCid on an input artifact reference must be omitted or equal artifact.cid");
13951
+ return;
13952
+ }
13953
+ if (!ref.external) invalid("references with taskId null must carry either an artifact (input artifact) or an external descriptor");
13954
+ });
13796
13955
  return errors;
13797
13956
  }
13798
13957
  //#endregion
@@ -13909,7 +14068,7 @@ Unsafe(Cyclic({ ClaimCondition: Unsafe(Union([
13909
14068
  */
13910
14069
  var TaskRef = _Object_({
13911
14070
  taskId: Union([Uuid, Null()]),
13912
- outputCid: Cid,
14071
+ outputCid: Optional(Cid),
13913
14072
  role: Union([
13914
14073
  Literal("judged_work"),
13915
14074
  Literal("reviewed_diff"),
@@ -13930,7 +14089,7 @@ var TaskRef = _Object_({
13930
14089
  })),
13931
14090
  artifact: Optional(_Object_({
13932
14091
  cid: Cid,
13933
- attemptN: Integer({ minimum: 1 }),
14092
+ attemptN: Optional(Integer({ minimum: 1 })),
13934
14093
  kind: Optional(String$1({
13935
14094
  minLength: 1,
13936
14095
  maxLength: 100
@@ -14400,30 +14559,53 @@ var TaskBuilder = class {
14400
14559
  return this;
14401
14560
  }
14402
14561
  /**
14403
- * Add a reference to a persistent task artifact while retaining the accepted
14404
- * output CID as the provenance anchor.
14562
+ * Add either a staged input artifact or a persistent attempt artifact.
14563
+ * Staged metadata returned by `tasks.artifacts.stage()` carries
14564
+ * `artifactSource: 'staged'` and produces a reference with `taskId: null`, no
14565
+ * `outputCid`, and no `attemptN`. Persistent artifacts require the producing
14566
+ * task, accepted output CID, artifact CID, and positive attempt number so
14567
+ * their provenance remains explicit.
14405
14568
  *
14406
- * @param source - A result reader, raw artifact reference, or `TaskRef`.
14569
+ * @param source - Staged SDK metadata, a result reader, raw artifact reference, or `TaskRef`.
14407
14570
  * @param role - The role the referenced artifact plays.
14408
14571
  * @returns This builder, for chaining.
14409
- * @throws {TaskBuildError} when output or artifact CID is missing.
14572
+ * @throws {TaskBuildError} when the source is ambiguous or required provenance is missing.
14410
14573
  */
14411
14574
  artifactReference(source, role) {
14412
14575
  let ref;
14413
14576
  if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
14414
- else if ("artifact" in source && source.artifact?.cid) {
14415
- if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14416
- field: "references/artifact/attemptN",
14417
- message: "artifact reference is missing required attemptN"
14418
- }]);
14419
- ref = {
14420
- ...source,
14421
- role
14422
- };
14423
- } else {
14577
+ else if ("artifactSource" in source && source.artifactSource === "staged" && source.cid) ref = {
14578
+ taskId: null,
14579
+ role,
14580
+ artifact: {
14581
+ cid: source.cid,
14582
+ ...source.kind ? { kind: source.kind } : {},
14583
+ ...source.title ? { title: source.title } : {},
14584
+ ...source.contentType ? { contentType: source.contentType } : {}
14585
+ }
14586
+ };
14587
+ else if ("cid" in source) throw new TaskBuildError([{
14588
+ field: "references/artifactSource",
14589
+ message: "top-level artifact CID is ambiguous; use metadata returned by tasks.artifacts.stage()"
14590
+ }]);
14591
+ else if ("artifact" in source && source.artifact?.cid) if (source.taskId === null && source.artifact.attemptN === void 0) ref = {
14592
+ taskId: null,
14593
+ role,
14594
+ artifact: { ...source.artifact }
14595
+ };
14596
+ else if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14597
+ field: "references/artifact/attemptN",
14598
+ message: "artifact reference is missing required attemptN"
14599
+ }]);
14600
+ else ref = {
14601
+ ...source,
14602
+ role
14603
+ };
14604
+ else {
14424
14605
  const s = source;
14425
14606
  const errors = [];
14426
- if (!s.outputCid) errors.push({
14607
+ const inputArtifact = s.taskId === null && s.attemptN === void 0;
14608
+ if (!inputArtifact && !s.outputCid) errors.push({
14427
14609
  field: "references/outputCid",
14428
14610
  message: "reference is missing required outputCid"
14429
14611
  });
@@ -14431,19 +14613,18 @@ var TaskBuilder = class {
14431
14613
  field: "references/artifact/cid",
14432
14614
  message: "artifact reference is missing required cid"
14433
14615
  });
14434
- if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14616
+ if (!inputArtifact && (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1)) errors.push({
14435
14617
  field: "references/artifact/attemptN",
14436
14618
  message: "artifact reference is missing required attemptN"
14437
14619
  });
14438
14620
  if (errors.length > 0) throw new TaskBuildError(errors);
14439
- const attemptN = s.attemptN;
14440
14621
  ref = {
14441
14622
  taskId: s.taskId ?? null,
14442
- outputCid: s.outputCid,
14623
+ ...!inputArtifact && s.outputCid ? { outputCid: s.outputCid } : {},
14443
14624
  role,
14444
14625
  artifact: {
14445
14626
  cid: s.artifactCid,
14446
- attemptN,
14627
+ ...s.attemptN !== void 0 ? { attemptN: s.attemptN } : {},
14447
14628
  ...s.kind ? { kind: s.kind } : {},
14448
14629
  ...s.title ? { title: s.title } : {},
14449
14630
  ...s.contentType ? { contentType: s.contentType } : {}
@@ -14913,6 +15094,22 @@ function createTasksNamespace(context) {
14913
15094
  }));
14914
15095
  },
14915
15096
  artifacts: {
15097
+ async stage(body, query, options) {
15098
+ return {
15099
+ ...unwrapResult(await stageTaskArtifact({
15100
+ auth,
15101
+ body,
15102
+ client,
15103
+ duplex: "half",
15104
+ headers: {
15105
+ ...requiredTeamHeaders(options),
15106
+ "content-type": "application/octet-stream"
15107
+ },
15108
+ query
15109
+ })),
15110
+ artifactSource: "staged"
15111
+ };
15112
+ },
14916
15113
  async upload(path, body, query, options) {
14917
15114
  return unwrapResult(await uploadTaskArtifact({
14918
15115
  auth,
@@ -14946,17 +15143,24 @@ function createTasksNamespace(context) {
14946
15143
  }));
14947
15144
  },
14948
15145
  async download(path, options) {
14949
- const result = await client.request({
15146
+ const request = {
14950
15147
  auth,
14951
15148
  headers: requiredTeamHeaders(options),
14952
15149
  method: "GET",
14953
15150
  parseAs: "stream",
14954
- path,
14955
15151
  security: [{
14956
15152
  scheme: "bearer",
14957
15153
  type: "http"
14958
- }],
15154
+ }]
15155
+ };
15156
+ const result = "attemptN" in path ? await client.request({
15157
+ ...request,
15158
+ path,
14959
15159
  url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
15160
+ }) : await client.request({
15161
+ ...request,
15162
+ path,
15163
+ url: "/tasks/{taskId}/artifacts/{cid}/content"
14960
15164
  });
14961
15165
  const normalizedStream = normalizeDownloadStream(unwrapResult(result));
14962
15166
  if (normalizedStream) return {
@@ -72,12 +72,17 @@
72
72
 
73
73
  <script type="text/html" data-help-name="moltnet-task-artifact-download">
74
74
  <p>
75
- Downloads a task artifact by task id, attempt number, and CID. CID is taken
76
- from <code>msg.cid</code>, <code>msg.payload.cid</code>,
75
+ Downloads a task artifact by task id and CID. CID is taken from
76
+ <code>msg.cid</code>, <code>msg.payload.cid</code>,
77
77
  <code>msg.payload.artifact.cid</code>, or the configured CID. Team ID is
78
78
  taken from this node or the configured agent unless Team override is
79
79
  enabled.
80
80
  </p>
81
+ <p>
82
+ Attempt is optional. Omit it to resolve the CID across the task, including
83
+ input artifacts bound when the task was created. Set it to require an exact
84
+ attempt artifact.
85
+ </p>
81
86
  <p>
82
87
  Emits the artifact bytes as a Buffer on <code>msg.payload</code>. Download
83
88
  metadata is placed on <code>msg.artifact</code>. The local byte limit
@@ -1,6 +1,6 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
2
  import { a as nonEmpty, t as bool } from "./query-utils.js";
3
- import { a as requireAttemptContext, n as payloadRecord, r as recordField, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
3
+ import { c as resolveMaxBytes, i as requireArtifactContext, n as payloadRecord, o as resolveAttemptSelection, r as recordField, t as collectArtifactBody } from "./task-artifact-utils.js";
4
4
  //#region src/nodes/task-artifact-download.ts
5
5
  var init = (RED) => {
6
6
  function TaskArtifactDownloadNode(def) {
@@ -10,7 +10,10 @@ var init = (RED) => {
10
10
  const run = async () => {
11
11
  try {
12
12
  if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-download: no moltnet-agent configured");
13
- const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
13
+ const { taskId, teamId } = requireArtifactContext("task-artifact-download", msg, def.taskId, def.teamId, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ const attempt = resolveAttemptSelection(msg, def.attemptN);
15
+ if (attempt.supplied && attempt.attemptN === void 0) throw new Error("task-artifact-download: attemptN must be a positive integer");
16
+ const attemptN = attempt.attemptN;
14
17
  const cid = resolveCid(msg, def.cid);
15
18
  if (!cid) throw new Error("task-artifact-download: cid is required");
16
19
  this.status({
@@ -18,7 +21,10 @@ var init = (RED) => {
18
21
  shape: "dot",
19
22
  text: "downloading…"
20
23
  });
21
- const result = await withAgent(agentNode, (agent) => agent.tasks.artifacts.download({
24
+ const result = await withAgent(agentNode, (agent) => agent.tasks.artifacts.download(attemptN === void 0 ? {
25
+ taskId,
26
+ cid
27
+ } : {
22
28
  taskId,
23
29
  attemptN,
24
30
  cid
@@ -33,7 +39,7 @@ var init = (RED) => {
33
39
  out.artifact = {
34
40
  taskId,
35
41
  teamId,
36
- attemptN,
42
+ ...attemptN === void 0 ? {} : { attemptN },
37
43
  cid,
38
44
  ...metadataFromResult(result)
39
45
  };
@@ -0,0 +1,93 @@
1
+ <script type="text/javascript">
2
+ var moltnetTaskArtifactStageMaxBytes = 25 * 1024 * 1024;
3
+ RED.nodes.registerType('moltnet-task-artifact-stage', {
4
+ category: 'moltnet',
5
+ color: '#00d4c8',
6
+ paletteLabel: 'task artifact: stage',
7
+ defaults: {
8
+ name: { value: '' },
9
+ agent: { value: '', type: 'moltnet-agent', required: true },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ maxBytes: {
13
+ value: moltnetTaskArtifactStageMaxBytes,
14
+ validate: RED.validators.number(),
15
+ },
16
+ contentType: { value: '' },
17
+ contentEncoding: { value: '' },
18
+ },
19
+ inputs: 1,
20
+ outputs: 1,
21
+ icon: 'font-awesome/fa-upload',
22
+ label: function () {
23
+ return this.name || 'task artifact: stage';
24
+ },
25
+ });
26
+ </script>
27
+
28
+ <script type="text/html" data-template-name="moltnet-task-artifact-stage">
29
+ <div class="form-row">
30
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-input-name" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
35
+ <input type="text" id="node-input-agent" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
39
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-allowMsgTeamOverride"
43
+ ><i class="fa fa-random"></i> Team override</label
44
+ >
45
+ <input
46
+ type="checkbox"
47
+ id="node-input-allowMsgTeamOverride"
48
+ style="display:inline-block;width:auto;vertical-align:top"
49
+ />
50
+ <span>Allow <code>msg.teamId</code></span>
51
+ </div>
52
+ <div class="form-row">
53
+ <label for="node-input-maxBytes"
54
+ ><i class="fa fa-database"></i> Max bytes</label
55
+ >
56
+ <input type="number" id="node-input-maxBytes" />
57
+ </div>
58
+ <div class="form-row">
59
+ <label for="node-input-contentType"
60
+ ><i class="fa fa-file-o"></i> Type</label
61
+ >
62
+ <input
63
+ type="text"
64
+ id="node-input-contentType"
65
+ placeholder="application/pdf"
66
+ />
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-contentEncoding"
70
+ ><i class="fa fa-compress"></i> Encoding</label
71
+ >
72
+ <input type="text" id="node-input-contentEncoding" placeholder="gzip" />
73
+ </div>
74
+ </script>
75
+
76
+ <script type="text/html" data-help-name="moltnet-task-artifact-stage">
77
+ <p>
78
+ Stages content-addressed bytes for later binding to a task through an input
79
+ artifact reference. Staging creates no visible artifact row; the returned
80
+ CID becomes readable only after a task binds it.
81
+ </p>
82
+ <p>
83
+ Reads bytes from the same payload shapes as task artifact upload and emits
84
+ <code>{ artifactSource: "staged", cid, sizeBytes, contentType }</code> on
85
+ <code>msg.payload</code> and <code>msg.artifact</code>. Wire
86
+ <code>msg.artifact</code> into a task builder's References from field.
87
+ </p>
88
+ <p>
89
+ Buffer and Uint8Array payloads avoid encoding overhead. The
90
+ <code>{ contentBase64 }</code> form is available for JSON-only sources. The
91
+ local byte limit defaults to 25 MiB.
92
+ </p>
93
+ </script>
@@ -0,0 +1,54 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { t as bool } from "./query-utils.js";
3
+ import { c as resolveMaxBytes, l as resolveTeamId, s as resolveField, u as resolveUploadBody } from "./task-artifact-utils.js";
4
+ //#region src/nodes/task-artifact-stage.ts
5
+ var init = (RED) => {
6
+ function TaskArtifactStageNode(def) {
7
+ RED.nodes.createNode(this, def);
8
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
9
+ this.on("input", (msg, send, done) => {
10
+ const run = async () => {
11
+ try {
12
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-stage: no moltnet-agent configured");
13
+ const teamId = resolveTeamId(msg, def.teamId, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ if (!teamId) throw new Error("task-artifact-stage: teamId is required");
15
+ const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes), "task-artifact-stage");
16
+ const query = {
17
+ contentType: resolveField(msg, "contentType", def.contentType),
18
+ contentEncoding: resolveField(msg, "contentEncoding", def.contentEncoding)
19
+ };
20
+ this.status({
21
+ fill: "blue",
22
+ shape: "dot",
23
+ text: "staging..."
24
+ });
25
+ const artifact = await withAgent(agentNode, (agent) => agent.tasks.artifacts.stage(body, query, { teamId }));
26
+ const out = RED.util.cloneMessage({
27
+ ...msg,
28
+ payload: void 0
29
+ });
30
+ out.payload = artifact;
31
+ out.artifact = artifact;
32
+ this.status({
33
+ fill: "green",
34
+ shape: "dot",
35
+ text: artifact.cid
36
+ });
37
+ send(out);
38
+ done();
39
+ } catch (err) {
40
+ this.status({
41
+ fill: "red",
42
+ shape: "ring",
43
+ text: "error"
44
+ });
45
+ done(err instanceof Error ? err : new Error(String(err)));
46
+ }
47
+ };
48
+ run();
49
+ });
50
+ }
51
+ RED.nodes.registerType("moltnet-task-artifact-stage", TaskArtifactStageNode);
52
+ };
53
+ //#endregion
54
+ export { init as default };
@@ -1,6 +1,6 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
2
  import { t as bool } from "./query-utils.js";
3
- import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
3
+ import { a as requireAttemptContext, c as resolveMaxBytes, s as resolveField, u as resolveUploadBody } from "./task-artifact-utils.js";
4
4
  //#region src/nodes/task-artifact-upload.ts
5
5
  var init = (RED) => {
6
6
  function TaskArtifactUploadNode(def) {
@@ -17,8 +17,22 @@ function resolveTeamId(msg, configured, agentNode, allowMsgTeamOverride) {
17
17
  return nonEmpty(payloadRecord(msg).teamId) ?? nonEmpty(configured) ?? agentNode.teamId;
18
18
  }
19
19
  function resolveAttemptN(msg, configured) {
20
+ return resolveAttemptSelection(msg, configured).attemptN;
21
+ }
22
+ function resolveAttemptSelection(msg, configured) {
20
23
  const payload = payloadRecord(msg);
21
- return positiveInt(msg.attemptN) ?? positiveInt(payload.attemptN) ?? positiveInt(recordField(payload.attempt, "attemptN")) ?? positiveInt(recordField(payload.artifact, "attemptN")) ?? positiveInt(configured);
24
+ const candidate = [
25
+ msg.attemptN,
26
+ payload.attemptN,
27
+ recordField(payload.attempt, "attemptN"),
28
+ recordField(payload.artifact, "attemptN"),
29
+ configured
30
+ ].find(isSupplied);
31
+ if (candidate === void 0) return { supplied: false };
32
+ return {
33
+ supplied: true,
34
+ attemptN: positiveInt(candidate)
35
+ };
22
36
  }
23
37
  function requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride) {
24
38
  const taskId = resolveTaskId(msg, configuredTaskId);
@@ -59,9 +73,8 @@ function resolveUploadBody(msg, maxBytes, nodeName = "task-artifact-upload") {
59
73
  }
60
74
  const record = payloadRecord(msg);
61
75
  if (typeof record.contentBase64 === "string") {
62
- const normalized = record.contentBase64.replace(/\s/g, "");
63
- if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
64
- return Buffer.from(normalized, "base64");
76
+ if (decodedBase64Length(record.contentBase64) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
77
+ return Buffer.from(record.contentBase64, "base64");
65
78
  }
66
79
  const content = record.content ?? record.body;
67
80
  if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes, nodeName);
@@ -139,11 +152,26 @@ function pushChunk(chunks, chunk, bytes, maxBytes, nodeName) {
139
152
  }
140
153
  function decodedBase64Length(value) {
141
154
  if (!value) return 0;
142
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
143
- return Math.floor(value.length * 3 / 4) - padding;
155
+ let characters = 0;
156
+ let previous = "";
157
+ let last = "";
158
+ for (let index = 0; index < value.length; index += 1) {
159
+ if (isBase64Whitespace(value.charCodeAt(index))) continue;
160
+ characters += 1;
161
+ previous = last;
162
+ last = value[index];
163
+ }
164
+ const padding = last === "=" ? previous === "=" ? 2 : 1 : 0;
165
+ return Math.max(0, Math.floor(characters * 3 / 4) - padding);
166
+ }
167
+ function isSupplied(value) {
168
+ return value !== void 0 && value !== null && value !== "";
169
+ }
170
+ function isBase64Whitespace(code) {
171
+ return code === 32 || code >= 9 && code <= 13;
144
172
  }
145
173
  function tooLarge(operation, maxBytes, nodeName) {
146
174
  return /* @__PURE__ */ new Error(`${nodeName}: ${operation} body exceeds ${maxBytes} bytes`);
147
175
  }
148
176
  //#endregion
149
- export { requireAttemptContext as a, resolveUploadBody as c, requireArtifactContext as i, payloadRecord as n, resolveField as o, recordField as r, resolveMaxBytes as s, collectArtifactBody as t };
177
+ export { requireAttemptContext as a, resolveMaxBytes as c, requireArtifactContext as i, resolveTeamId as l, payloadRecord as n, resolveAttemptSelection as o, recordField as r, resolveField as s, collectArtifactBody as t, resolveUploadBody as u };
@@ -75,7 +75,8 @@ var init = (RED) => {
75
75
  }
76
76
  if (def.referencesFrom) {
77
77
  const ref = RED.util.getMessageProperty(msg, def.referencesFrom);
78
- if (ref && ref.outputCid) builder.references(ref, def.referencesRole ?? "context");
78
+ if (ref?.artifact || ref?.artifactSource === "staged" || ref?.cid) builder.artifactReference(ref, def.referencesRole ?? "context");
79
+ else if (ref?.outputCid) builder.references(ref, def.referencesRole ?? "context");
79
80
  }
80
81
  if (def.submitOutputGate) builder.requireSubmitOutput();
81
82
  if (def.schemaCid) builder.requireSchema(def.schemaCid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/node-red-contrib-core",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "Node-RED nodes for the MoltNet API",
6
6
  "keywords": [
@@ -30,6 +30,7 @@
30
30
  "moltnet-task-wait": "dist/nodes/task-wait.js",
31
31
  "moltnet-task-artifacts-list": "dist/nodes/task-artifacts-list.js",
32
32
  "moltnet-task-artifact-upload": "dist/nodes/task-artifact-upload.js",
33
+ "moltnet-task-artifact-stage": "dist/nodes/task-artifact-stage.js",
33
34
  "moltnet-task-artifact-download": "dist/nodes/task-artifact-download.js",
34
35
  "moltnet-runtime-session-get": "dist/nodes/runtime-session-get.js",
35
36
  "moltnet-runtime-session-upload": "dist/nodes/runtime-session-upload.js",
@@ -45,7 +46,7 @@
45
46
  },
46
47
  "main": "dist/nodes/agent.js",
47
48
  "dependencies": {
48
- "@themoltnet/sdk": "0.119.0"
49
+ "@themoltnet/sdk": "0.121.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^22.19.0",