@themoltnet/node-red-contrib-core 0.11.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 +20 -10
- package/dist/nodes/runtime-session-download.js +1 -1
- package/dist/nodes/runtime-session-upload.js +1 -1
- package/dist/nodes/src.js +185 -36
- package/dist/nodes/task-artifact-download.html +7 -2
- package/dist/nodes/task-artifact-download.js +10 -4
- package/dist/nodes/task-artifact-stage.html +93 -0
- package/dist/nodes/task-artifact-stage.js +54 -0
- package/dist/nodes/task-artifact-upload.js +1 -1
- package/dist/nodes/task-artifact-utils.js +35 -7
- package/dist/nodes/task-builder.js +2 -1
- package/package.json +3 -2
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
|
|
19
|
-
**action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
|
|
20
|
-
`moltnet-task-wait`, `moltnet-task-artifacts-list`,
|
|
21
|
-
`moltnet-task-artifact-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
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**
|
|
96
|
-
`outputRef` from `task: read`
|
|
97
|
-
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
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/
|
|
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(
|
|
10159
|
-
|
|
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
|
|
14404
|
-
*
|
|
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 -
|
|
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
|
|
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 ("
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
...source,
|
|
14421
|
-
|
|
14422
|
-
}
|
|
14423
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
|
76
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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,
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.12.0",
|
|
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.
|
|
49
|
+
"@themoltnet/sdk": "0.120.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/node": "^22.19.0",
|