@themoltnet/pi-extension 0.16.2 → 0.17.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
@@ -32,9 +32,11 @@ import { WriteOperations } from '@earendil-works/pi-coding-agent';
32
32
  export declare function activateAgentEnv(agentEnv: Record<string, string | undefined>, repoRoot: string): void;
33
33
 
34
34
  /**
35
- * Construct an in-memory `AgentSession`. The caller is responsible for
36
- * eventually invoking `session.prompt(...)` and for tearing down — the
37
- * helper does no lifecycle management beyond construction.
35
+ * Construct an `AgentSession`. By default it is in-memory; callers may opt
36
+ * parent sessions into daemon-owned file persistence via `sessionPersistence`.
37
+ * The caller is responsible for eventually invoking `session.prompt(...)` and
38
+ * for tearing down — the helper does no lifecycle management beyond
39
+ * construction.
38
40
  */
39
41
  export declare function buildAgentSession(args: BuildAgentSessionArgs): Promise<AgentSession>;
40
42
 
@@ -56,6 +58,13 @@ declare interface BuildAgentSessionArgs {
56
58
  otelSpanAttrs: Record<string, string | number | boolean>;
57
59
  /** Agent name for `gen_ai.agent.name` on the root span. */
58
60
  agentName: string;
61
+ /**
62
+ * Parent sessions may persist their conversation history in a daemon-owned
63
+ * directory. Subagents should leave this unset and stay in-memory.
64
+ */
65
+ sessionPersistence?: {
66
+ sessionDir: string;
67
+ };
59
68
  }
60
69
 
61
70
  declare interface ClaimedTask {
@@ -277,6 +286,12 @@ export declare interface ExecutePiTaskOptions {
277
286
  * after HOST_EXEC_ALLOWED; an array limits auto-approval to matching rules.
278
287
  */
279
288
  hostExecAutoApprove?: HostExecAutoApproveConfig;
289
+ /**
290
+ * Optional daemon-supplied execution plan. Keeps task semantics out of
291
+ * `pi-extension` while still letting callers opt into stable worktrees and
292
+ * file-backed Pi sessions for selected task classes.
293
+ */
294
+ makeExecutionPlan?: PiTaskExecutionPlanFactory;
280
295
  }
281
296
 
282
297
  /**
@@ -415,6 +430,42 @@ export declare interface PiOtelOptions {
415
430
  spanAttributes?: Record<string, string | number | boolean>;
416
431
  }
417
432
 
433
+ export declare interface PiSessionPersistencePlan {
434
+ sessionDir: string;
435
+ }
436
+
437
+ export declare interface PiTaskExecutionPlan {
438
+ /**
439
+ * Daemon-local reuse key. When set alongside `workspaceScope: 'session'`,
440
+ * dedicated worktrees may be retained and reopened across related tasks.
441
+ */
442
+ sessionKey: string | null;
443
+ /**
444
+ * Workspace identity selected by the daemon. `null` means the task should
445
+ * run against the shared mount path.
446
+ */
447
+ workspaceId: string | null;
448
+ /**
449
+ * Branch to create or reopen for the workspace. `null` means no dedicated
450
+ * worktree is required.
451
+ */
452
+ worktreeBranch: string | null;
453
+ /**
454
+ * Lifetime of the task workspace from the daemon's point of view.
455
+ * `attempt` = disposable; `session` = keep stable for the reuse key.
456
+ */
457
+ workspaceScope: 'attempt' | 'session';
458
+ /**
459
+ * Optional location for file-backed Pi session history. When omitted,
460
+ * the executor keeps the conversation in memory for this attempt only.
461
+ */
462
+ sessionPersistence?: PiSessionPersistencePlan | null;
463
+ }
464
+
465
+ export declare type PiTaskExecutionPlanFactory = (claimedTask: ClaimedTask) => PiTaskExecutionPlan | null;
466
+
467
+ export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
468
+
418
469
  /**
419
470
  * Resume a VM from a checkpoint, inject credentials, configure egress +
420
471
  * TLS. Returns the managed VM handle.
package/dist/index.js CHANGED
@@ -8633,9 +8633,11 @@ var NO_SKILLS = () => ({
8633
8633
  diagnostics: []
8634
8634
  });
8635
8635
  /**
8636
- * Construct an in-memory `AgentSession`. The caller is responsible for
8637
- * eventually invoking `session.prompt(...)` and for tearing down — the
8638
- * helper does no lifecycle management beyond construction.
8636
+ * Construct an `AgentSession`. By default it is in-memory; callers may opt
8637
+ * parent sessions into daemon-owned file persistence via `sessionPersistence`.
8638
+ * The caller is responsible for eventually invoking `session.prompt(...)` and
8639
+ * for tearing down — the helper does no lifecycle management beyond
8640
+ * construction.
8639
8641
  */
8640
8642
  async function buildAgentSession(args) {
8641
8643
  const piOtelExtension = createPiOtelExtension({
@@ -8650,15 +8652,23 @@ async function buildAgentSession(args) {
8650
8652
  skillsOverride: args.skillsOverride ?? NO_SKILLS
8651
8653
  });
8652
8654
  await resourceLoader.reload();
8655
+ const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
8656
+ cwd: args.mountPath,
8657
+ sessionDir: args.sessionPersistence.sessionDir
8658
+ }) : SessionManager.inMemory(args.mountPath);
8653
8659
  return (await createAgentSession({
8654
8660
  agentDir: args.piAuthDir,
8655
8661
  cwd: args.mountPath,
8656
8662
  model: args.modelHandle,
8657
8663
  customTools: args.customTools,
8658
- sessionManager: SessionManager.inMemory(),
8664
+ sessionManager,
8659
8665
  resourceLoader
8660
8666
  })).session;
8661
8667
  }
8668
+ async function resolvePersistentSessionManager(args) {
8669
+ await SessionManager.list(args.cwd, args.sessionDir);
8670
+ return SessionManager.continueRecent(args.cwd, args.sessionDir);
8671
+ }
8662
8672
  //#endregion
8663
8673
  //#region ../tasks/src/formats.ts
8664
8674
  /**
@@ -9702,7 +9712,10 @@ var BUILT_IN_TASK_TYPES = {
9702
9712
  inputSchema: FulfillBriefInput,
9703
9713
  outputSchema: FulfillBriefOutput,
9704
9714
  outputKind: "artifact",
9715
+ resumable: true,
9705
9716
  workspaceMode: "dedicated_worktree",
9717
+ workspaceScope: "session",
9718
+ sessionScope: "correlation",
9706
9719
  requiresReferences: false,
9707
9720
  validateOutput: requireVerificationWhenCriteriaPresent
9708
9721
  },
@@ -9712,6 +9725,8 @@ var BUILT_IN_TASK_TYPES = {
9712
9725
  outputSchema: AssessBriefOutput,
9713
9726
  outputKind: "judgment",
9714
9727
  workspaceMode: "dedicated_worktree",
9728
+ workspaceScope: "attempt",
9729
+ sessionScope: "none",
9715
9730
  requiresReferences: true,
9716
9731
  validateInput: validateJudgmentInput,
9717
9732
  validateInputAsync: validateAssessBriefInputAsync
@@ -9721,6 +9736,8 @@ var BUILT_IN_TASK_TYPES = {
9721
9736
  inputSchema: CuratePackInput,
9722
9737
  outputSchema: CuratePackOutput,
9723
9738
  outputKind: "artifact",
9739
+ workspaceScope: "attempt",
9740
+ sessionScope: "none",
9724
9741
  requiresReferences: false,
9725
9742
  validateOutput: requireVerificationWhenCriteriaPresent
9726
9743
  },
@@ -9729,6 +9746,8 @@ var BUILT_IN_TASK_TYPES = {
9729
9746
  inputSchema: RenderPackInput,
9730
9747
  outputSchema: RenderPackOutput,
9731
9748
  outputKind: "artifact",
9749
+ workspaceScope: "attempt",
9750
+ sessionScope: "none",
9732
9751
  requiresReferences: false,
9733
9752
  validateOutput: requireVerificationWhenCriteriaPresent,
9734
9753
  validateInputAsync: validateRenderPackInputAsync
@@ -9738,6 +9757,8 @@ var BUILT_IN_TASK_TYPES = {
9738
9757
  inputSchema: JudgePackInput,
9739
9758
  outputSchema: JudgePackOutput,
9740
9759
  outputKind: "judgment",
9760
+ workspaceScope: "attempt",
9761
+ sessionScope: "none",
9741
9762
  requiresReferences: true,
9742
9763
  validateInput: validateJudgmentInput,
9743
9764
  validateOutput: validateJudgePackOutput,
@@ -9748,6 +9769,8 @@ var BUILT_IN_TASK_TYPES = {
9748
9769
  inputSchema: RunEvalInput,
9749
9770
  outputSchema: RunEvalOutput,
9750
9771
  outputKind: "artifact",
9772
+ workspaceScope: "attempt",
9773
+ sessionScope: "custom",
9751
9774
  requiresReferences: false,
9752
9775
  validateOutput: validateRunEvalOutput
9753
9776
  },
@@ -9756,6 +9779,8 @@ var BUILT_IN_TASK_TYPES = {
9756
9779
  inputSchema: JudgeEvalVariantInput,
9757
9780
  outputSchema: JudgeEvalVariantOutput,
9758
9781
  outputKind: "judgment",
9782
+ workspaceScope: "attempt",
9783
+ sessionScope: "custom",
9759
9784
  requiresReferences: false,
9760
9785
  validateInput: validateJudgeEvalVariantInput,
9761
9786
  validateOutput: validateJudgeEvalVariantOutput,
@@ -9825,15 +9850,6 @@ function getTaskOutputSchema(taskType) {
9825
9850
  function taskTypeUsesSubagents(taskType) {
9826
9851
  return getTaskTypeEntry(taskType)?.usesSubagents === true;
9827
9852
  }
9828
- /**
9829
- * Filesystem isolation policy requested by the task type.
9830
- *
9831
- * Unknown task types and task types without an explicit policy default to the
9832
- * legacy/shared behaviour.
9833
- */
9834
- function taskTypeWorkspaceMode(taskType) {
9835
- return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
9836
- }
9837
9853
  //#endregion
9838
9854
  //#region ../tasks/src/wire.ts
9839
9855
  /**
@@ -15198,6 +15214,113 @@ function resolveSubmitTools(taskType, opts = {}) {
15198
15214
  };
15199
15215
  }
15200
15216
  //#endregion
15217
+ //#region src/runtime/task-workspace.ts
15218
+ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
15219
+ const branch = executionPlan?.worktreeBranch ?? null;
15220
+ if (!branch) return {
15221
+ mountPath: requestedMountPath,
15222
+ mode: "shared_mount",
15223
+ branch: null,
15224
+ cleanup: () => {}
15225
+ };
15226
+ const mainRepo = findMainWorktree();
15227
+ const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
15228
+ const relMount = relative(mainRepo, requestedMountPath);
15229
+ const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
15230
+ const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
15231
+ if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
15232
+ else {
15233
+ removeExistingTaskWorktree(mainRepo, worktreeDir);
15234
+ addTaskWorktree(mainRepo, worktreeDir, branch);
15235
+ }
15236
+ return {
15237
+ mountPath,
15238
+ mode: "dedicated_worktree",
15239
+ branch,
15240
+ cleanup: keepWorkspace ? () => {} : () => {
15241
+ execFileSync("git", [
15242
+ "-C",
15243
+ mainRepo,
15244
+ "worktree",
15245
+ "remove",
15246
+ "--force",
15247
+ worktreeDir
15248
+ ], { stdio: "pipe" });
15249
+ }
15250
+ };
15251
+ }
15252
+ function resolveTaskWorktreePath(mainRepo, workspaceId) {
15253
+ return join(mainRepo, ".worktrees", workspaceId);
15254
+ }
15255
+ function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
15256
+ if (isRegisteredWorktree(mainRepo, worktreeDir)) return;
15257
+ if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
15258
+ addTaskWorktree(mainRepo, worktreeDir, branch);
15259
+ }
15260
+ function addTaskWorktree(mainRepo, worktreeDir, branch) {
15261
+ const baseRef = resolveWorktreeBaseRef(mainRepo);
15262
+ execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
15263
+ "-C",
15264
+ mainRepo,
15265
+ "worktree",
15266
+ "add",
15267
+ worktreeDir,
15268
+ branch
15269
+ ] : [
15270
+ "-C",
15271
+ mainRepo,
15272
+ "worktree",
15273
+ "add",
15274
+ "-b",
15275
+ branch,
15276
+ worktreeDir,
15277
+ baseRef
15278
+ ], { stdio: "pipe" });
15279
+ }
15280
+ function removeExistingTaskWorktree(mainRepo, worktreeDir) {
15281
+ if (!existsSync(worktreeDir) || !isRegisteredWorktree(mainRepo, worktreeDir)) return;
15282
+ execFileSync("git", [
15283
+ "-C",
15284
+ mainRepo,
15285
+ "worktree",
15286
+ "remove",
15287
+ "--force",
15288
+ worktreeDir
15289
+ ], { stdio: "pipe" });
15290
+ }
15291
+ function isRegisteredWorktree(mainRepo, worktreeDir) {
15292
+ const list = execFileSync("git", [
15293
+ "-C",
15294
+ mainRepo,
15295
+ "worktree",
15296
+ "list",
15297
+ "--porcelain"
15298
+ ], {
15299
+ encoding: "utf8",
15300
+ stdio: "pipe"
15301
+ });
15302
+ const marker = `worktree ${worktreeDir}\n`;
15303
+ return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
15304
+ }
15305
+ function resolveWorktreeBaseRef(mainRepo) {
15306
+ return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
15307
+ }
15308
+ function gitRefExists(mainRepo, ref) {
15309
+ try {
15310
+ execFileSync("git", [
15311
+ "-C",
15312
+ mainRepo,
15313
+ "show-ref",
15314
+ "--verify",
15315
+ "--quiet",
15316
+ ref
15317
+ ], { stdio: "pipe" });
15318
+ return true;
15319
+ } catch {
15320
+ return false;
15321
+ }
15322
+ }
15323
+ //#endregion
15201
15324
  //#region src/runtime/execute-pi-task.ts
15202
15325
  /**
15203
15326
  * executePiTask — run a single Task attempt using pi-coding-agent inside a
@@ -15246,7 +15369,9 @@ async function executePiTask(claimedTask, reporter, opts) {
15246
15369
  const task = claimedTask.task;
15247
15370
  const attemptN = claimedTask.attemptN;
15248
15371
  const startTime = Date.now();
15249
- const workspace = prepareTaskWorkspace(task, opts.mountPath ?? process.cwd());
15372
+ const requestedMountPath = opts.mountPath ?? process.cwd();
15373
+ const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
15374
+ const workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
15250
15375
  const mountPath = workspace.mountPath;
15251
15376
  if (reporter.cancelSignal.aborted) return {
15252
15377
  taskId: task.id,
@@ -15462,7 +15587,8 @@ async function executePiTask(claimedTask, reporter, opts) {
15462
15587
  "moltnet.task.id": task.id,
15463
15588
  "moltnet.task.attempt": attemptN,
15464
15589
  "moltnet.task.type": task.taskType
15465
- }
15590
+ },
15591
+ sessionPersistence: executionPlan?.sessionPersistence ?? void 0
15466
15592
  });
15467
15593
  } catch (err) {
15468
15594
  const message = err instanceof Error ? err.message : String(err);
@@ -15690,107 +15816,6 @@ async function executePiTask(claimedTask, reporter, opts) {
15690
15816
  }
15691
15817
  }
15692
15818
  }
15693
- function resolveTaskWorktreeBranch(task) {
15694
- if (taskTypeWorkspaceMode(task.taskType) !== "dedicated_worktree") return null;
15695
- if (task.taskType === "fulfill_brief") {
15696
- const input = task.input;
15697
- const slug = slugifyBranchComponent(typeof input.title === "string" && input.title.trim().length > 0 ? input.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType) || "task";
15698
- if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
15699
- return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyBranchComponent(input.scopeHint) : "task") || "task"}-${slug}`;
15700
- }
15701
- return `task/${slugifyBranchComponent(task.taskType) || "task"}-${task.id.slice(0, 8)}`;
15702
- }
15703
- function slugifyBranchComponent(input) {
15704
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
15705
- }
15706
- function prepareTaskWorkspace(task, requestedMountPath) {
15707
- const branch = resolveTaskWorktreeBranch(task);
15708
- if (!branch) return {
15709
- mountPath: requestedMountPath,
15710
- mode: "shared_mount",
15711
- branch: null,
15712
- cleanup: () => {}
15713
- };
15714
- const mainRepo = findMainWorktree();
15715
- const worktreeDir = join(mainRepo, ".worktrees", `task-${task.id}`);
15716
- removeExistingTaskWorktree(mainRepo, worktreeDir);
15717
- const relMount = relative(mainRepo, requestedMountPath);
15718
- const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
15719
- const baseRef = resolveWorktreeBaseRef(mainRepo);
15720
- execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
15721
- "-C",
15722
- mainRepo,
15723
- "worktree",
15724
- "add",
15725
- worktreeDir,
15726
- branch
15727
- ] : [
15728
- "-C",
15729
- mainRepo,
15730
- "worktree",
15731
- "add",
15732
- "-b",
15733
- branch,
15734
- worktreeDir,
15735
- baseRef
15736
- ], { stdio: "pipe" });
15737
- return {
15738
- mountPath,
15739
- mode: "dedicated_worktree",
15740
- branch,
15741
- cleanup: () => {
15742
- execFileSync("git", [
15743
- "-C",
15744
- mainRepo,
15745
- "worktree",
15746
- "remove",
15747
- "--force",
15748
- worktreeDir
15749
- ], { stdio: "pipe" });
15750
- }
15751
- };
15752
- }
15753
- function removeExistingTaskWorktree(mainRepo, worktreeDir) {
15754
- if (!existsSync(worktreeDir)) return;
15755
- const list = execFileSync("git", [
15756
- "-C",
15757
- mainRepo,
15758
- "worktree",
15759
- "list",
15760
- "--porcelain"
15761
- ], {
15762
- encoding: "utf8",
15763
- stdio: "pipe"
15764
- });
15765
- const marker = `worktree ${worktreeDir}\n`;
15766
- if (!list.includes(marker) && !list.endsWith(`worktree ${worktreeDir}`)) return;
15767
- execFileSync("git", [
15768
- "-C",
15769
- mainRepo,
15770
- "worktree",
15771
- "remove",
15772
- "--force",
15773
- worktreeDir
15774
- ], { stdio: "pipe" });
15775
- }
15776
- function resolveWorktreeBaseRef(mainRepo) {
15777
- return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
15778
- }
15779
- function gitRefExists(mainRepo, ref) {
15780
- try {
15781
- execFileSync("git", [
15782
- "-C",
15783
- mainRepo,
15784
- "show-ref",
15785
- "--verify",
15786
- "--quiet",
15787
- ref
15788
- ], { stdio: "pipe" });
15789
- return true;
15790
- } catch {
15791
- return false;
15792
- }
15793
- }
15794
15819
  function emptyUsage(provider, model) {
15795
15820
  return {
15796
15821
  inputTokens: 0,
@@ -16170,4 +16195,4 @@ function moltnetExtension(pi) {
16170
16195
  registerMoltnetReflectCommand(pi, state);
16171
16196
  }
16172
16197
  //#endregion
16173
- export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resumeVm, toGuestPath };
16198
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.16.2",
3
+ "version": "0.17.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
  "license": "MIT",
@@ -51,7 +51,7 @@
51
51
  "@earendil-works/pi-coding-agent": "^0.74.0",
52
52
  "@opentelemetry/sdk-metrics": "^2.5.1",
53
53
  "@opentelemetry/sdk-trace-base": "^2.5.1",
54
- "@types/node": "^20.11.0",
54
+ "@types/node": "^22.19.0",
55
55
  "typescript": "^5.3.3",
56
56
  "vite": "^8.0.0",
57
57
  "vite-plugin-dts": "^4.5.4",