@themoltnet/node-red-contrib-core 0.10.0 → 0.12.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
@@ -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.
@@ -57,6 +59,11 @@ want the MoltNet editor skin.
57
59
  `{ taskId, status, terminal, accepted, acceptedAttemptN, state, attempt,
58
60
  attempts, error, task }`. `state` is the accepted attempt's output artifact
59
61
  (the lifecycle "phase" payload) or `null`. For switch/branch logic.
62
+ - **`moltnet-task-cancel`** (palette: _task: cancel_) — cancels one task or an
63
+ array of task rows/ids. Reads ids from `msg.taskId`, `msg.taskIds`,
64
+ `msg.payload.taskId`, `msg.payload.id`, `msg.payload.failure.taskId`, or an
65
+ array on `msg.payload`. Preserves the original payload and writes results to
66
+ `msg.cancelledTasks` / `msg.cancelErrors`.
60
67
  - **`moltnet-task-wait`** (palette: _task: wait_) — polls a task until it settles,
61
68
  in one loop doing double duty like the CLI's `task tail`. **Two outputs:**
62
69
  output 1 (_tail_) emits each new task message as it arrives (gated by a `tail`
@@ -75,10 +82,17 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
75
82
  `content`, `body`, or base64 `contentBase64`. Team context is the configured
76
83
  node/agent by default; message team overrides require an explicit checkbox.
77
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.
78
90
  - **`moltnet-task-artifact-download`** (palette: _task artifact: download_) —
79
- downloads an artifact by task id, attempt number, and CID. Emits the artifact
80
- bytes as a Buffer on `msg.payload` and metadata on `msg.artifact`. Upload and
81
- 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.
82
96
  - **`moltnet-workflow-status`** (palette: _workflow: status_) — reads the tasks of
83
97
  one workflow run (by `correlationId`) and emits a table-shaped `msg.payload`
84
98
  (array of `{ taskId, type, title, status, queuedAt, completedAt }`) plus
@@ -87,9 +101,10 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
87
101
  `tasks.create` body from the SDK fluent builder (`buildFreeform`). Pure offline
88
102
  transform: reads `teamId`/`diaryId` from the referenced `agent` (with optional
89
103
  typedInput overrides), maps **context rows** (slug ← msg/flow/global/str/json),
90
- binds a prior task's output via **References from** (a `msg`-path to an
91
- `outputRef` from `task: read`), and toggles the **submit-output** / schema
92
- 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`
93
108
  (the SDK's `{ body, teamId }` envelope is flattened to `{ ...body, teamId }`).
94
109
  Validation errors surface on the node (red ring).
95
110
  - **`moltnet-task-reader`** (palette: _task: read_) — parses a completed snapshot
@@ -273,12 +288,17 @@ created through the Runtime Profiles API for team
273
288
  `6743b4b1-6b93-46e2-a048-19490f04f91a`; recreate them or replace the IDs when
274
289
  running the flow in another team.
275
290
 
276
- | Stage | Profile ID | Provider / model | Settings |
277
- | ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
278
- | FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=12000`, `maxTurns=60`, `maxBashTimeouts=5`, `defaultWorkspaceMode=dedicated_worktree`, `allowedWorkspaceModes=[dedicated_worktree]`, requires `git`, `gh`, `rg` |
279
- | PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.1`, `maxOutputTokens=10000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
280
- | SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `thinkingLevel=high`, `temperature=0.15`, `maxOutputTokens=18000`, `maxTurns=24`, `maxBashTimeouts=3`, `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
281
- | AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `thinkingLevel=high`, `temperature=0.2`, `maxOutputTokens=24000`, `maxTurns=16`, `maxBashTimeouts=2`, `defaultWorkspaceMode=none`, `allowedWorkspaceModes=[none]` |
291
+ | Stage | Profile ID | Provider / model | Workspace and tool contract |
292
+ | ---------- | -------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
293
+ | FREEZE | `1a653eb9-7bfa-475f-b517-c070c9c25b5e` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=dedicated_worktree`, `allowedWorkspaceModes=[dedicated_worktree]`, requires `git`, `gh`, `rg` |
294
+ | PREFLIGHT | `f4bb1d9b-6281-4158-ad88-cbcb1198c3dc` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
295
+ | SPECIALIST | `f50e9c58-4180-4e07-b120-08b6097c13d5` | `ollama-cloud/qwen3-coder:480b-cloud` | `defaultWorkspaceMode=shared_mount`, `allowedWorkspaceModes=[shared_mount]`, requires `git`, `rg` |
296
+ | AGGREGATE | `29db793d-3ad9-420b-96e7-df5356b3d19b` | `ollama-cloud/kimi-k2.7-code:cloud` | `defaultWorkspaceMode=none`, `allowedWorkspaceModes=[none]` |
297
+
298
+ The example intentionally avoids prescribing `maxTurns`, output-token caps,
299
+ sampling temperature, or other model-behavior tuning. Keep those profile values
300
+ at the platform/runtime defaults until a repeated run gives enough evidence to
301
+ set a limit deliberately.
282
302
 
283
303
  All four profiles set `sandbox.env.NODE_OPTIONS=--dns-result-order=ipv4first`
284
304
  to match the live Ollama daemon smoke pattern and require `OLLAMA_API_KEY`.
@@ -299,10 +319,10 @@ export OLLAMA_API_KEY=...
299
319
  npx @themoltnet/agent-daemon@latest poll \
300
320
  --agent <agent-name> \
301
321
  --team 6743b4b1-6b93-46e2-a048-19490f04f91a \
302
- --profile deep-review-freeze-ollama-qwen-coder-v1 \
303
- --profile deep-review-preflight-ollama-qwen-coder-v1 \
304
- --profile deep-review-specialist-ollama-qwen-coder-v1 \
305
- --profile deep-review-aggregate-ollama-kimi-v1
322
+ --profile deep-review-freeze-v1 \
323
+ --profile deep-review-preflight-v1 \
324
+ --profile deep-review-specialist-v1 \
325
+ --profile deep-review-aggregate-v1
306
326
  ```
307
327
 
308
328
  The daemon host must have matching Pi model registry entries. This repo's
@@ -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: [
@@ -9633,6 +9660,11 @@ var RuntimeProfileSandboxResumeCommand = Union([String$1({
9633
9660
  maximum: 6e4
9634
9661
  }))
9635
9662
  }, { additionalProperties: false })]);
9663
+ var RuntimeProfileAllowedHost = String$1({
9664
+ minLength: 1,
9665
+ maxLength: 255,
9666
+ 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])?))*$"
9667
+ });
9636
9668
  var RuntimeProfileSandbox = _Object_({
9637
9669
  snapshot: Optional(_Object_({
9638
9670
  setupCommands: Optional(_Array_(String$1({
@@ -9649,6 +9681,10 @@ var RuntimeProfileSandbox = _Object_({
9649
9681
  pattern: "^[0-9]+[KMGTP]?$"
9650
9682
  }))
9651
9683
  }, { additionalProperties: false })),
9684
+ network: Optional(_Object_({
9685
+ allowedHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 })),
9686
+ allowedInternalHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 }))
9687
+ }, { additionalProperties: false })),
9652
9688
  resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
9653
9689
  vfs: Optional(_Object_({
9654
9690
  shadow: Optional(_Array_(String$1({
@@ -9992,7 +10028,7 @@ _Object_({
9992
10028
  * (server-side schema check). Self-assessment is a truthful self-rating,
9993
10029
  * NOT enforcement — `verification.passed=false` does not block /complete
9994
10030
  * and does not affect `acceptedAttemptN`. See
9995
- * `docs/understand/agent-runtime.md` for the full producer/judge flow.
10031
+ * `docs/use/tasks-and-runtime.md` for the full producer/judge flow.
9996
10032
  *
9997
10033
  * **Binding evaluation** (judgment tasks: `assess_brief`, `judge_pack`).
9998
10034
  * A separate task whose IS the application of `successCriteria` to
@@ -10108,7 +10144,7 @@ _Object_({
10108
10144
  id: String$1({ format: "uuid" }),
10109
10145
  teamId: String$1({ format: "uuid" }),
10110
10146
  taskId: String$1({ format: "uuid" }),
10111
- attemptN: Integer({ minimum: 1 }),
10147
+ attemptN: Union([Integer({ minimum: 1 }), Null()]),
10112
10148
  kind: String$1({
10113
10149
  minLength: 1,
10114
10150
  maxLength: 100
@@ -10130,7 +10166,7 @@ _Object_({
10130
10166
  minLength: 1,
10131
10167
  maxLength: 100
10132
10168
  }),
10133
- createdByAgentId: String$1({ format: "uuid" }),
10169
+ createdByAgentId: Union([String$1({ format: "uuid" }), Null()]),
10134
10170
  expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10135
10171
  createdAt: String$1({ format: "date-time" })
10136
10172
  }, { $id: "TaskArtifact" })),
@@ -10146,6 +10182,16 @@ _Object_({
10146
10182
  $id: "ListTaskArtifactsQuery",
10147
10183
  additionalProperties: false
10148
10184
  });
10185
+ var HeaderSafeContentType = String$1({
10186
+ minLength: 1,
10187
+ maxLength: 200,
10188
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10189
+ });
10190
+ var HeaderSafeContentEncoding = String$1({
10191
+ minLength: 1,
10192
+ maxLength: 100,
10193
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10194
+ });
10149
10195
  _Object_({
10150
10196
  kind: String$1({
10151
10197
  minLength: 1,
@@ -10155,14 +10201,8 @@ _Object_({
10155
10201
  minLength: 1,
10156
10202
  maxLength: 255
10157
10203
  }),
10158
- contentType: Optional(String$1({
10159
- minLength: 1,
10160
- maxLength: 200
10161
- })),
10162
- contentEncoding: Optional(String$1({
10163
- minLength: 1,
10164
- maxLength: 100
10165
- }))
10204
+ contentType: Optional(HeaderSafeContentType),
10205
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10166
10206
  }, {
10167
10207
  $id: "UploadTaskArtifactQuery",
10168
10208
  additionalProperties: false
@@ -10194,6 +10234,34 @@ _Object_({
10194
10234
  $id: "TaskArtifactContentParams",
10195
10235
  additionalProperties: false
10196
10236
  });
10237
+ _Object_({
10238
+ contentType: Optional(HeaderSafeContentType),
10239
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10240
+ }, {
10241
+ $id: "StageTaskArtifactQuery",
10242
+ additionalProperties: false
10243
+ });
10244
+ _Object_({
10245
+ cid: String$1({
10246
+ minLength: 1,
10247
+ maxLength: 100
10248
+ }),
10249
+ sizeBytes: Integer({ minimum: 0 }),
10250
+ contentType: String$1({
10251
+ minLength: 1,
10252
+ maxLength: 200
10253
+ })
10254
+ }, { $id: "StagedTaskArtifact" });
10255
+ _Object_({
10256
+ taskId: String$1({ format: "uuid" }),
10257
+ cid: String$1({
10258
+ minLength: 1,
10259
+ maxLength: 100
10260
+ })
10261
+ }, {
10262
+ $id: "TaskArtifactTaskContentParams",
10263
+ additionalProperties: false
10264
+ });
10197
10265
  new TextEncoder();
10198
10266
  new TextDecoder();
10199
10267
  //#endregion
@@ -13793,6 +13861,42 @@ function validateTaskCreateRequest(args) {
13793
13861
  field: "references",
13794
13862
  message: `At least one reference is required for task type: ${args.taskType}`
13795
13863
  });
13864
+ errors.push(...validateTaskReferences(args.references));
13865
+ return errors;
13866
+ }
13867
+ /**
13868
+ * Cross-field rules for the reference shapes the schema alone cannot
13869
+ * express. Every reference must be exactly one of:
13870
+ *
13871
+ * - **task output ref**: taskId set, outputCid required; an optional
13872
+ * artifact must name its producing attempt (attemptN).
13873
+ * - **input artifact ref**: taskId null, artifact without attemptN;
13874
+ * outputCid omitted (or equal to artifact.cid when sent) -- the bytes
13875
+ * were staged before any task existed, so there is no output to name.
13876
+ * - **external ref**: taskId null, external present, no artifact.
13877
+ *
13878
+ * Anything else used to be silently persisted into taskRefs and never
13879
+ * materialized; now it fails fast with a field error.
13880
+ */
13881
+ function validateTaskReferences(references) {
13882
+ const errors = [];
13883
+ (references ?? []).forEach((ref, index) => {
13884
+ const invalid = (message) => errors.push({
13885
+ field: `references[${index}]`,
13886
+ message
13887
+ });
13888
+ if (ref.taskId !== null) {
13889
+ if (ref.outputCid === void 0) invalid("outputCid is required when referencing a task output");
13890
+ if (ref.artifact && ref.artifact.attemptN === void 0) invalid("artifact references to another task must include attemptN; input artifacts are referenced with taskId null");
13891
+ return;
13892
+ }
13893
+ if (ref.artifact) {
13894
+ if (ref.artifact.attemptN !== void 0) invalid("input artifact references (taskId null) must not include attemptN; reference the producing task by id instead");
13895
+ if (ref.outputCid !== void 0 && ref.outputCid !== ref.artifact.cid) invalid("outputCid on an input artifact reference must be omitted or equal artifact.cid");
13896
+ return;
13897
+ }
13898
+ if (!ref.external) invalid("references with taskId null must carry either an artifact (input artifact) or an external descriptor");
13899
+ });
13796
13900
  return errors;
13797
13901
  }
13798
13902
  //#endregion
@@ -13909,7 +14013,7 @@ Unsafe(Cyclic({ ClaimCondition: Unsafe(Union([
13909
14013
  */
13910
14014
  var TaskRef = _Object_({
13911
14015
  taskId: Union([Uuid, Null()]),
13912
- outputCid: Cid,
14016
+ outputCid: Optional(Cid),
13913
14017
  role: Union([
13914
14018
  Literal("judged_work"),
13915
14019
  Literal("reviewed_diff"),
@@ -13930,7 +14034,7 @@ var TaskRef = _Object_({
13930
14034
  })),
13931
14035
  artifact: Optional(_Object_({
13932
14036
  cid: Cid,
13933
- attemptN: Integer({ minimum: 1 }),
14037
+ attemptN: Optional(Integer({ minimum: 1 })),
13934
14038
  kind: Optional(String$1({
13935
14039
  minLength: 1,
13936
14040
  maxLength: 100
@@ -14400,30 +14504,53 @@ var TaskBuilder = class {
14400
14504
  return this;
14401
14505
  }
14402
14506
  /**
14403
- * Add a reference to a persistent task artifact while retaining the accepted
14404
- * output CID as the provenance anchor.
14507
+ * Add either a staged input artifact or a persistent attempt artifact.
14508
+ * Staged metadata returned by `tasks.artifacts.stage()` carries
14509
+ * `artifactSource: 'staged'` and produces a reference with `taskId: null`, no
14510
+ * `outputCid`, and no `attemptN`. Persistent artifacts require the producing
14511
+ * task, accepted output CID, artifact CID, and positive attempt number so
14512
+ * their provenance remains explicit.
14405
14513
  *
14406
- * @param source - A result reader, raw artifact reference, or `TaskRef`.
14514
+ * @param source - Staged SDK metadata, a result reader, raw artifact reference, or `TaskRef`.
14407
14515
  * @param role - The role the referenced artifact plays.
14408
14516
  * @returns This builder, for chaining.
14409
- * @throws {TaskBuildError} when output or artifact CID is missing.
14517
+ * @throws {TaskBuildError} when the source is ambiguous or required provenance is missing.
14410
14518
  */
14411
14519
  artifactReference(source, role) {
14412
14520
  let ref;
14413
14521
  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 {
14522
+ else if ("artifactSource" in source && source.artifactSource === "staged" && source.cid) ref = {
14523
+ taskId: null,
14524
+ role,
14525
+ artifact: {
14526
+ cid: source.cid,
14527
+ ...source.kind ? { kind: source.kind } : {},
14528
+ ...source.title ? { title: source.title } : {},
14529
+ ...source.contentType ? { contentType: source.contentType } : {}
14530
+ }
14531
+ };
14532
+ else if ("cid" in source) throw new TaskBuildError([{
14533
+ field: "references/artifactSource",
14534
+ message: "top-level artifact CID is ambiguous; use metadata returned by tasks.artifacts.stage()"
14535
+ }]);
14536
+ else if ("artifact" in source && source.artifact?.cid) if (source.taskId === null && source.artifact.attemptN === void 0) ref = {
14537
+ taskId: null,
14538
+ role,
14539
+ artifact: { ...source.artifact }
14540
+ };
14541
+ else if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14542
+ field: "references/artifact/attemptN",
14543
+ message: "artifact reference is missing required attemptN"
14544
+ }]);
14545
+ else ref = {
14546
+ ...source,
14547
+ role
14548
+ };
14549
+ else {
14424
14550
  const s = source;
14425
14551
  const errors = [];
14426
- if (!s.outputCid) errors.push({
14552
+ const inputArtifact = s.taskId === null && s.attemptN === void 0;
14553
+ if (!inputArtifact && !s.outputCid) errors.push({
14427
14554
  field: "references/outputCid",
14428
14555
  message: "reference is missing required outputCid"
14429
14556
  });
@@ -14431,19 +14558,18 @@ var TaskBuilder = class {
14431
14558
  field: "references/artifact/cid",
14432
14559
  message: "artifact reference is missing required cid"
14433
14560
  });
14434
- if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14561
+ if (!inputArtifact && (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1)) errors.push({
14435
14562
  field: "references/artifact/attemptN",
14436
14563
  message: "artifact reference is missing required attemptN"
14437
14564
  });
14438
14565
  if (errors.length > 0) throw new TaskBuildError(errors);
14439
- const attemptN = s.attemptN;
14440
14566
  ref = {
14441
14567
  taskId: s.taskId ?? null,
14442
- outputCid: s.outputCid,
14568
+ ...!inputArtifact && s.outputCid ? { outputCid: s.outputCid } : {},
14443
14569
  role,
14444
14570
  artifact: {
14445
14571
  cid: s.artifactCid,
14446
- attemptN,
14572
+ ...s.attemptN !== void 0 ? { attemptN: s.attemptN } : {},
14447
14573
  ...s.kind ? { kind: s.kind } : {},
14448
14574
  ...s.title ? { title: s.title } : {},
14449
14575
  ...s.contentType ? { contentType: s.contentType } : {}
@@ -14913,6 +15039,22 @@ function createTasksNamespace(context) {
14913
15039
  }));
14914
15040
  },
14915
15041
  artifacts: {
15042
+ async stage(body, query, options) {
15043
+ return {
15044
+ ...unwrapResult(await stageTaskArtifact({
15045
+ auth,
15046
+ body,
15047
+ client,
15048
+ duplex: "half",
15049
+ headers: {
15050
+ ...requiredTeamHeaders(options),
15051
+ "content-type": "application/octet-stream"
15052
+ },
15053
+ query
15054
+ })),
15055
+ artifactSource: "staged"
15056
+ };
15057
+ },
14916
15058
  async upload(path, body, query, options) {
14917
15059
  return unwrapResult(await uploadTaskArtifact({
14918
15060
  auth,
@@ -14946,17 +15088,24 @@ function createTasksNamespace(context) {
14946
15088
  }));
14947
15089
  },
14948
15090
  async download(path, options) {
14949
- const result = await client.request({
15091
+ const request = {
14950
15092
  auth,
14951
15093
  headers: requiredTeamHeaders(options),
14952
15094
  method: "GET",
14953
15095
  parseAs: "stream",
14954
- path,
14955
15096
  security: [{
14956
15097
  scheme: "bearer",
14957
15098
  type: "http"
14958
- }],
15099
+ }]
15100
+ };
15101
+ const result = "attemptN" in path ? await client.request({
15102
+ ...request,
15103
+ path,
14959
15104
  url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
15105
+ }) : await client.request({
15106
+ ...request,
15107
+ path,
15108
+ url: "/tasks/{taskId}/artifacts/{cid}/content"
14960
15109
  });
14961
15110
  const normalizedStream = normalizeDownloadStream(unwrapResult(result));
14962
15111
  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>