@themoltnet/pi-extension 0.28.1 → 0.29.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/dist/index.d.ts CHANGED
@@ -762,6 +762,13 @@ declare const Task: Type.TObject<{
762
762
  commit_sha: Type.TOptional<Type.TString>;
763
763
  snapshot_cid: Type.TOptional<Type.TString>;
764
764
  }>>;
765
+ artifact: Type.TOptional<Type.TObject<{
766
+ cid: Type.TString;
767
+ attemptN: Type.TInteger;
768
+ kind: Type.TOptional<Type.TString>;
769
+ title: Type.TOptional<Type.TString>;
770
+ contentType: Type.TOptional<Type.TString>;
771
+ }>>;
765
772
  }>>;
766
773
  correlationId: Type.TUnion<[Type.TString, Type.TNull]>;
767
774
  proposedByAgentId: Type.TUnion<[Type.TString, Type.TNull]>;
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
3
+ import { cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
4
  import path, { join, relative, sep } from "node:path";
5
5
  import { DEFAULT_MAX_BYTES, DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createFindTool, createFindToolDefinition, createGrepTool, createGrepToolDefinition, createLsTool, createLsToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { Readable } from "node:stream";
8
8
  import crypto, { createHash as createHash$1 } from "crypto";
9
- import { readFile } from "node:fs/promises";
9
+ import { readFile, realpath, stat } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
+ import { pipeline } from "node:stream/promises";
11
12
  import { Type, getModel } from "@earendil-works/pi-ai";
12
13
  import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
13
14
  import { parseEnv } from "node:util";
@@ -2308,6 +2309,55 @@ var claimTask = (options) => (options.client ?? client).post({
2308
2309
  }
2309
2310
  });
2310
2311
  /**
2312
+ * List task artifact metadata for the current team.
2313
+ */
2314
+ var listTaskArtifacts = (options) => (options.client ?? client).get({
2315
+ security: [
2316
+ {
2317
+ scheme: "bearer",
2318
+ type: "http"
2319
+ },
2320
+ {
2321
+ name: "X-Moltnet-Session-Token",
2322
+ type: "apiKey"
2323
+ },
2324
+ {
2325
+ in: "cookie",
2326
+ name: "ory_kratos_session",
2327
+ type: "apiKey"
2328
+ }
2329
+ ],
2330
+ url: "/tasks/{taskId}/artifacts",
2331
+ ...options
2332
+ });
2333
+ /**
2334
+ * Upload immutable content-addressed artifact content for a task attempt.
2335
+ */
2336
+ var uploadTaskArtifact = (options) => (options.client ?? client).put({
2337
+ bodySerializer: null,
2338
+ security: [
2339
+ {
2340
+ scheme: "bearer",
2341
+ type: "http"
2342
+ },
2343
+ {
2344
+ name: "X-Moltnet-Session-Token",
2345
+ type: "apiKey"
2346
+ },
2347
+ {
2348
+ in: "cookie",
2349
+ name: "ory_kratos_session",
2350
+ type: "apiKey"
2351
+ }
2352
+ ],
2353
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts",
2354
+ ...options,
2355
+ headers: {
2356
+ "Content-Type": "application/octet-stream",
2357
+ ...options.headers
2358
+ }
2359
+ });
2360
+ /**
2311
2361
  * List teams the caller belongs to.
2312
2362
  */
2313
2363
  var listTeams = (options) => (options?.client ?? client).get({
@@ -10073,6 +10123,97 @@ var VerificationRecord = _Object_({
10073
10123
  $id: "VerificationRecord",
10074
10124
  additionalProperties: false
10075
10125
  });
10126
+ _Object_({
10127
+ artifacts: _Array_(_Object_({
10128
+ id: String$1({ format: "uuid" }),
10129
+ teamId: String$1({ format: "uuid" }),
10130
+ taskId: String$1({ format: "uuid" }),
10131
+ attemptN: Integer({ minimum: 1 }),
10132
+ kind: String$1({
10133
+ minLength: 1,
10134
+ maxLength: 100
10135
+ }),
10136
+ title: String$1({
10137
+ minLength: 1,
10138
+ maxLength: 255
10139
+ }),
10140
+ contentType: String$1({
10141
+ minLength: 1,
10142
+ maxLength: 200
10143
+ }),
10144
+ contentEncoding: Union([String$1({
10145
+ minLength: 1,
10146
+ maxLength: 100
10147
+ }), Null()]),
10148
+ sizeBytes: Integer({ minimum: 0 }),
10149
+ cid: String$1({
10150
+ minLength: 1,
10151
+ maxLength: 100
10152
+ }),
10153
+ createdByAgentId: String$1({ format: "uuid" }),
10154
+ expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10155
+ createdAt: String$1({ format: "date-time" })
10156
+ }, { $id: "TaskArtifact" })),
10157
+ nextCursor: Union([String$1({ minLength: 1 }), Null()])
10158
+ }, { $id: "TaskArtifactList" });
10159
+ _Object_({
10160
+ limit: Optional(Integer({
10161
+ minimum: 1,
10162
+ maximum: 100
10163
+ })),
10164
+ cursor: Optional(String$1({ minLength: 1 }))
10165
+ }, {
10166
+ $id: "ListTaskArtifactsQuery",
10167
+ additionalProperties: false
10168
+ });
10169
+ _Object_({
10170
+ kind: String$1({
10171
+ minLength: 1,
10172
+ maxLength: 100
10173
+ }),
10174
+ title: String$1({
10175
+ minLength: 1,
10176
+ maxLength: 255
10177
+ }),
10178
+ contentType: Optional(String$1({
10179
+ minLength: 1,
10180
+ maxLength: 200
10181
+ })),
10182
+ contentEncoding: Optional(String$1({
10183
+ minLength: 1,
10184
+ maxLength: 100
10185
+ }))
10186
+ }, {
10187
+ $id: "UploadTaskArtifactQuery",
10188
+ additionalProperties: false
10189
+ });
10190
+ String$1({
10191
+ $id: "TaskArtifactContent",
10192
+ description: "Task artifact content stream.",
10193
+ format: "binary"
10194
+ });
10195
+ _Object_({ taskId: String$1({ format: "uuid" }) }, {
10196
+ $id: "TaskArtifactTaskParams",
10197
+ additionalProperties: false
10198
+ });
10199
+ _Object_({
10200
+ taskId: String$1({ format: "uuid" }),
10201
+ attemptN: Integer({ minimum: 1 })
10202
+ }, {
10203
+ $id: "TaskArtifactAttemptParams",
10204
+ additionalProperties: false
10205
+ });
10206
+ _Object_({
10207
+ taskId: String$1({ format: "uuid" }),
10208
+ attemptN: Integer({ minimum: 1 }),
10209
+ cid: String$1({
10210
+ minLength: 1,
10211
+ maxLength: 100
10212
+ })
10213
+ }, {
10214
+ $id: "TaskArtifactContentParams",
10215
+ additionalProperties: false
10216
+ });
10076
10217
  //#endregion
10077
10218
  //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/codecs/json.js
10078
10219
  var textEncoder$2 = new TextEncoder();
@@ -10339,6 +10480,10 @@ var FreeformArtifact = _Object_({
10339
10480
  description: Optional(String$1({ minLength: 1 })),
10340
10481
  url: Optional(String$1({ minLength: 1 })),
10341
10482
  path: Optional(String$1({ minLength: 1 })),
10483
+ cid: Optional(String$1({ minLength: 1 })),
10484
+ contentType: Optional(String$1({ minLength: 1 })),
10485
+ contentEncoding: Optional(String$1({ minLength: 1 })),
10486
+ sizeBytes: Optional(Integer({ minimum: 0 })),
10342
10487
  body: Optional(String$1({ maxLength: 65536 }))
10343
10488
  }, {
10344
10489
  $id: "FreeformArtifact",
@@ -13818,7 +13963,23 @@ var TaskRef = _Object_({
13818
13963
  url: Optional(String$1()),
13819
13964
  commit_sha: Optional(String$1()),
13820
13965
  snapshot_cid: Optional(Cid)
13821
- }))
13966
+ })),
13967
+ artifact: Optional(_Object_({
13968
+ cid: Cid,
13969
+ attemptN: Integer({ minimum: 1 }),
13970
+ kind: Optional(String$1({
13971
+ minLength: 1,
13972
+ maxLength: 100
13973
+ })),
13974
+ title: Optional(String$1({
13975
+ minLength: 1,
13976
+ maxLength: 255
13977
+ })),
13978
+ contentType: Optional(String$1({
13979
+ minLength: 1,
13980
+ maxLength: 200
13981
+ }))
13982
+ }, { additionalProperties: false }))
13822
13983
  }, {
13823
13984
  $id: "TaskRef",
13824
13985
  additionalProperties: false
@@ -14163,6 +14324,7 @@ var TaskBuilder = class {
14163
14324
  message: "reference is missing required outputCid"
14164
14325
  }]);
14165
14326
  ref = {
14327
+ ...s,
14166
14328
  taskId: s.taskId ?? null,
14167
14329
  outputCid: s.outputCid,
14168
14330
  role
@@ -14172,6 +14334,60 @@ var TaskBuilder = class {
14172
14334
  return this;
14173
14335
  }
14174
14336
  /**
14337
+ * Add a reference to a persistent task artifact while retaining the accepted
14338
+ * output CID as the provenance anchor.
14339
+ *
14340
+ * @param source - A result reader, raw artifact reference, or `TaskRef`.
14341
+ * @param role - The role the referenced artifact plays.
14342
+ * @returns This builder, for chaining.
14343
+ * @throws {TaskBuildError} when output or artifact CID is missing.
14344
+ */
14345
+ artifactReference(source, role) {
14346
+ let ref;
14347
+ if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
14348
+ else if ("artifact" in source && source.artifact?.cid) {
14349
+ if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14350
+ field: "references/artifact/attemptN",
14351
+ message: "artifact reference is missing required attemptN"
14352
+ }]);
14353
+ ref = {
14354
+ ...source,
14355
+ role
14356
+ };
14357
+ } else {
14358
+ const s = source;
14359
+ const errors = [];
14360
+ if (!s.outputCid) errors.push({
14361
+ field: "references/outputCid",
14362
+ message: "reference is missing required outputCid"
14363
+ });
14364
+ if (!s.artifactCid) errors.push({
14365
+ field: "references/artifact/cid",
14366
+ message: "artifact reference is missing required cid"
14367
+ });
14368
+ if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14369
+ field: "references/artifact/attemptN",
14370
+ message: "artifact reference is missing required attemptN"
14371
+ });
14372
+ if (errors.length > 0) throw new TaskBuildError(errors);
14373
+ const attemptN = s.attemptN;
14374
+ ref = {
14375
+ taskId: s.taskId ?? null,
14376
+ outputCid: s.outputCid,
14377
+ role,
14378
+ artifact: {
14379
+ cid: s.artifactCid,
14380
+ attemptN,
14381
+ ...s.kind ? { kind: s.kind } : {},
14382
+ ...s.title ? { title: s.title } : {},
14383
+ ...s.contentType ? { contentType: s.contentType } : {}
14384
+ }
14385
+ };
14386
+ }
14387
+ this.refs.push(ref);
14388
+ return this;
14389
+ }
14390
+ /**
14175
14391
  * Set the owning team (required by the wire schema).
14176
14392
  *
14177
14393
  * @param teamId - Team UUID.
@@ -14549,6 +14765,34 @@ var TaskResultReader = class {
14549
14765
  role
14550
14766
  };
14551
14767
  }
14768
+ /**
14769
+ * Build a `TaskRef` that anchors a downstream task to this accepted output
14770
+ * and points at one persistent task artifact by CID.
14771
+ *
14772
+ * @param filter - Artifact object or a filter resolved against output artifacts.
14773
+ * @param role - The role this artifact plays in the downstream task.
14774
+ * @returns A `TaskRef` with `artifact.cid` populated.
14775
+ * @throws {TaskResultError} if no matching artifact has a CID.
14776
+ */
14777
+ artifactRef(filter, role) {
14778
+ const artifact = typeof filter === "object" && "cid" in filter && "kind" in filter ? filter : this.artifact(filter);
14779
+ if (!artifact?.cid) throw new TaskResultError([{
14780
+ field: "artifacts/cid",
14781
+ message: "no matching artifact with a cid"
14782
+ }]);
14783
+ return {
14784
+ taskId: this.taskId,
14785
+ outputCid: this.outputCid,
14786
+ role,
14787
+ artifact: {
14788
+ cid: artifact.cid,
14789
+ attemptN: this.accepted.attemptN,
14790
+ kind: artifact.kind,
14791
+ title: artifact.title,
14792
+ ...artifact.contentType ? { contentType: artifact.contentType } : {}
14793
+ }
14794
+ };
14795
+ }
14552
14796
  };
14553
14797
  /**
14554
14798
  * Validate and construct a {@link TaskResultReader} from a task and its
@@ -14573,6 +14817,63 @@ function createTasksNamespace(context) {
14573
14817
  auth
14574
14818
  }));
14575
14819
  },
14820
+ artifacts: {
14821
+ async upload(path, body, query, options) {
14822
+ return unwrapResult(await uploadTaskArtifact({
14823
+ auth,
14824
+ body,
14825
+ client,
14826
+ duplex: "half",
14827
+ headers: {
14828
+ ...requiredTeamHeaders(options),
14829
+ "content-type": "application/octet-stream"
14830
+ },
14831
+ path,
14832
+ query
14833
+ }));
14834
+ },
14835
+ async list(taskId, options, query) {
14836
+ return unwrapResult(await listTaskArtifacts({
14837
+ client,
14838
+ auth,
14839
+ headers: requiredTeamHeaders(options),
14840
+ path: { taskId },
14841
+ query
14842
+ })).artifacts;
14843
+ },
14844
+ async listPage(taskId, query, options) {
14845
+ return unwrapResult(await listTaskArtifacts({
14846
+ client,
14847
+ auth,
14848
+ headers: requiredTeamHeaders(options),
14849
+ path: { taskId },
14850
+ query
14851
+ }));
14852
+ },
14853
+ async download(path, options) {
14854
+ const result = await client.request({
14855
+ auth,
14856
+ headers: requiredTeamHeaders(options),
14857
+ method: "GET",
14858
+ parseAs: "stream",
14859
+ path,
14860
+ security: [{
14861
+ scheme: "bearer",
14862
+ type: "http"
14863
+ }],
14864
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
14865
+ });
14866
+ const normalizedStream = normalizeDownloadStream(unwrapResult(result));
14867
+ if (normalizedStream) return {
14868
+ artifactId: header(result.response, "x-moltnet-task-artifact-id"),
14869
+ cid: header(result.response, "x-moltnet-task-artifact-cid"),
14870
+ contentEncoding: header(result.response, "x-moltnet-task-artifact-content-encoding"),
14871
+ contentType: header(result.response, "x-moltnet-task-artifact-content-type"),
14872
+ stream: normalizedStream
14873
+ };
14874
+ throw new MoltNetError("Unexpected task artifact download response stream", { code: "INVALID_RESPONSE" });
14875
+ }
14876
+ },
14576
14877
  async list(query, options) {
14577
14878
  return unwrapResult(await listTasks({
14578
14879
  client,
@@ -14741,6 +15042,33 @@ function createTasksNamespace(context) {
14741
15042
  }
14742
15043
  };
14743
15044
  }
15045
+ function header(response, name) {
15046
+ const value = response?.headers.get(name) ?? null;
15047
+ return value === "" ? null : value;
15048
+ }
15049
+ function normalizeDownloadStream(stream) {
15050
+ if (isAsyncIterable(stream)) return stream;
15051
+ if (isReadableStream(stream)) return readableStreamToAsyncIterable(stream);
15052
+ return null;
15053
+ }
15054
+ function isAsyncIterable(value) {
15055
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
15056
+ }
15057
+ function isReadableStream(value) {
15058
+ return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
15059
+ }
15060
+ async function* readableStreamToAsyncIterable(stream) {
15061
+ const reader = stream.getReader();
15062
+ try {
15063
+ while (true) {
15064
+ const result = await reader.read();
15065
+ if (result.done) return;
15066
+ yield result.value;
15067
+ }
15068
+ } finally {
15069
+ reader.releaseLock();
15070
+ }
15071
+ }
14744
15072
  //#endregion
14745
15073
  //#region ../sdk/src/namespaces/teams.ts
14746
15074
  function createTeamsNamespace(context) {
@@ -17212,6 +17540,20 @@ function shouldAutoApproveHostExec(params, config) {
17212
17540
  if (!Array.isArray(policy)) return false;
17213
17541
  return policy.some((rule) => hostExecMatchesAutoApproveRule(params, rule));
17214
17542
  }
17543
+ async function resolveWorkspaceFilePath(cwd, filePath) {
17544
+ const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17545
+ const [realCwd, realResolved] = await Promise.all([realpath(cwd), realpath(resolved)]);
17546
+ const rel = path.relative(realCwd, realResolved);
17547
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact path escapes workspace: ${filePath}`);
17548
+ return realResolved;
17549
+ }
17550
+ async function resolveWorkspaceOutputPath(cwd, filePath) {
17551
+ const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17552
+ const [realCwd, realParent] = await Promise.all([realpath(cwd), realpath(path.dirname(resolved))]);
17553
+ const rel = path.relative(realCwd, realParent);
17554
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact output path escapes workspace: ${filePath}`);
17555
+ return resolved;
17556
+ }
17215
17557
  /**
17216
17558
  * Expand the `taskFilter` shorthand on the diary list/search tools into
17217
17559
  * the matching `task:*` provenance tags emitted by `moltnet_create_entry`
@@ -17696,6 +18038,127 @@ function createMoltNetTools(config) {
17696
18038
  };
17697
18039
  }
17698
18040
  });
18041
+ const uploadTaskArtifact = defineTool({
18042
+ name: "moltnet_upload_task_artifact",
18043
+ label: "Upload MoltNet Task Artifact",
18044
+ description: "Upload a file from the current task workspace as an immutable task artifact. Only available during an active task attempt; the tool attaches the artifact to the active taskId/attemptN and returns metadata including cid, sizeBytes, kind, and title. Use this for large logs, reports, build outputs, screenshots, generated files, or other bytes that should be referenced by CID instead of pasted into structured task output.",
18045
+ parameters: Type.Object({
18046
+ filePath: Type.String({ description: "Path to a file under the current task workspace. Relative paths are resolved from the workspace root." }),
18047
+ kind: Type.String({ description: "Artifact category, e.g. log, report, patch, screenshot, bundle, dataset, trace." }),
18048
+ title: Type.String({ description: "Human-readable artifact title, usually the file name." }),
18049
+ contentType: Type.Optional(Type.String({ description: "MIME type. Defaults to application/octet-stream when omitted." })),
18050
+ contentEncoding: Type.Optional(Type.String({ description: "Optional content encoding if the file is already encoded, e.g. gzip." }))
18051
+ }),
18052
+ async execute(_id, params) {
18053
+ const { agent, teamId } = ensureConnected(config);
18054
+ if (!teamId) throw new Error("moltnet_upload_task_artifact requires a team context");
18055
+ const taskCtx = config.getTaskContext?.() ?? null;
18056
+ if (!taskCtx) throw new Error("moltnet_upload_task_artifact is only available during an active task attempt");
18057
+ const cwd = config.getHostCwd?.() ?? process.cwd();
18058
+ const resolved = await resolveWorkspaceFilePath(cwd, params.filePath);
18059
+ const info = await stat(resolved);
18060
+ if (!info.isFile()) throw new Error(`task artifact path is not a file: ${params.filePath}`);
18061
+ const artifact = await agent.tasks.artifacts.upload({
18062
+ taskId: taskCtx.taskId,
18063
+ attemptN: taskCtx.attemptN
18064
+ }, createReadStream(resolved), {
18065
+ kind: params.kind,
18066
+ title: params.title,
18067
+ contentType: params.contentType ?? "application/octet-stream",
18068
+ contentEncoding: params.contentEncoding
18069
+ }, { teamId });
18070
+ return {
18071
+ content: [{
18072
+ type: "text",
18073
+ text: JSON.stringify({
18074
+ ...artifact,
18075
+ filePath: path.relative(cwd, resolved),
18076
+ localSizeBytes: info.size
18077
+ }, null, 2)
18078
+ }],
18079
+ details: {}
18080
+ };
18081
+ }
18082
+ });
18083
+ const listTaskArtifacts = defineTool({
18084
+ name: "moltnet_list_task_artifacts",
18085
+ label: "List MoltNet Task Artifacts",
18086
+ description: "List immutable artifacts attached to a task, including each artifact CID, attempt number, kind, title, content type, size, uploader, and creation time. Use this when judging or continuing work that references task artifacts.",
18087
+ parameters: Type.Object({
18088
+ taskId: Type.Optional(Type.String({ description: "Task ID. Defaults to the active task when running inside a task attempt." })),
18089
+ limit: Type.Optional(Type.Integer({
18090
+ minimum: 1,
18091
+ maximum: 100,
18092
+ description: "Maximum artifacts to return. Defaults to the server page size."
18093
+ })),
18094
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor returned by a previous moltnet_list_task_artifacts call." }))
18095
+ }),
18096
+ async execute(_id, params) {
18097
+ const { agent, teamId } = ensureConnected(config);
18098
+ if (!teamId) throw new Error("moltnet_list_task_artifacts requires a team context");
18099
+ const taskId = params.taskId ?? config.getTaskContext?.()?.taskId;
18100
+ if (!taskId) throw new Error("moltnet_list_task_artifacts requires taskId outside an active task");
18101
+ const page = await agent.tasks.artifacts.listPage(taskId, {
18102
+ cursor: params.cursor,
18103
+ limit: params.limit
18104
+ }, { teamId });
18105
+ return {
18106
+ content: [{
18107
+ type: "text",
18108
+ text: JSON.stringify(page, null, 2)
18109
+ }],
18110
+ details: {}
18111
+ };
18112
+ }
18113
+ });
18114
+ const downloadTaskArtifact = defineTool({
18115
+ name: "moltnet_download_task_artifact",
18116
+ label: "Download MoltNet Task Artifact",
18117
+ description: "Download immutable task artifact bytes by taskId, attemptN, and CID into a new file in the current task workspace. Use moltnet_list_task_artifacts first to choose the correct CID for referenced task inputs.",
18118
+ parameters: Type.Object({
18119
+ taskId: Type.Optional(Type.String({ description: "Task ID. Defaults to the active task when running inside a task attempt." })),
18120
+ attemptN: Type.Integer({
18121
+ minimum: 1,
18122
+ description: "Attempt number that produced the artifact."
18123
+ }),
18124
+ cid: Type.String({
18125
+ minLength: 1,
18126
+ description: "Artifact CID returned by moltnet_list_task_artifacts."
18127
+ }),
18128
+ outputPath: Type.String({ description: "New file path under the current task workspace. The tool refuses to overwrite existing files." })
18129
+ }),
18130
+ async execute(_id, params) {
18131
+ const { agent, teamId } = ensureConnected(config);
18132
+ if (!teamId) throw new Error("moltnet_download_task_artifact requires a team context");
18133
+ const taskId = params.taskId ?? config.getTaskContext?.()?.taskId;
18134
+ if (!taskId) throw new Error("moltnet_download_task_artifact requires taskId outside an active task");
18135
+ const cwd = config.getHostCwd?.() ?? process.cwd();
18136
+ const outputPath = await resolveWorkspaceOutputPath(cwd, params.outputPath);
18137
+ const download = await agent.tasks.artifacts.download({
18138
+ taskId,
18139
+ attemptN: params.attemptN,
18140
+ cid: params.cid
18141
+ }, { teamId });
18142
+ await pipeline(download.stream, createWriteStream(outputPath, { flags: "wx" }));
18143
+ const info = await stat(outputPath);
18144
+ return {
18145
+ content: [{
18146
+ type: "text",
18147
+ text: JSON.stringify({
18148
+ taskId,
18149
+ attemptN: params.attemptN,
18150
+ cid: params.cid,
18151
+ artifactId: download.artifactId,
18152
+ contentType: download.contentType,
18153
+ contentEncoding: download.contentEncoding,
18154
+ outputPath: path.relative(cwd, outputPath),
18155
+ sizeBytes: info.size
18156
+ }, null, 2)
18157
+ }],
18158
+ details: {}
18159
+ };
18160
+ }
18161
+ });
17699
18162
  const reviewSessionErrors = defineTool({
17700
18163
  name: "moltnet_review_session_errors",
17701
18164
  label: "Review Session Tool Errors",
@@ -17745,6 +18208,9 @@ function createMoltNetTools(config) {
17745
18208
  getTask,
17746
18209
  listTaskAttempts,
17747
18210
  listTaskMessages,
18211
+ uploadTaskArtifact,
18212
+ listTaskArtifacts,
18213
+ downloadTaskArtifact,
17748
18214
  reviewSessionErrors,
17749
18215
  defineTool({
17750
18216
  name: "moltnet_host_exec",
@@ -19408,6 +19874,18 @@ function buildFinalOutputBlock(opts) {
19408
19874
  `Your final assistant text before that tool call may explain your work,`,
19409
19875
  `but the submit-tool call itself must be your VERY LAST action.`,
19410
19876
  "",
19877
+ `Task artifacts: when you produce large files, binary files, logs, reports,`,
19878
+ `screenshots, traces, bundles, or datasets, save them in the task workspace`,
19879
+ `and call \`moltnet_upload_task_artifact\` before the submit-output tool.`,
19880
+ `Put the returned artifact CID in the structured output where the schema`,
19881
+ `allows artifact metadata (for example \`artifacts[].cid\`). Do not paste`,
19882
+ `large bytes into structured output.`,
19883
+ "",
19884
+ `Referenced inputs: if this task depends on prior task artifacts, call`,
19885
+ `\`moltnet_list_task_artifacts\` for the referenced task and download the`,
19886
+ `specific CID you need with \`moltnet_download_task_artifact\` before judging`,
19887
+ `or continuing that work.`,
19888
+ "",
19411
19889
  `Output shape:`,
19412
19890
  "",
19413
19891
  "```json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "keywords": [
@@ -36,8 +36,8 @@
36
36
  "@earendil-works/gondolin": "^0.9.1",
37
37
  "@opentelemetry/api": "^1.9.0",
38
38
  "typebox": "^1.2.8",
39
- "@themoltnet/agent-runtime": "0.31.1",
40
- "@themoltnet/sdk": "0.114.0"
39
+ "@themoltnet/agent-runtime": "0.32.0",
40
+ "@themoltnet/sdk": "0.115.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",