@themoltnet/pi-extension 0.32.1 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -308,6 +308,12 @@ export declare interface ExecutePiTaskOptions {
308
308
  sandboxConfig?: SandboxConfig;
309
309
  /** Host environment variable names to forward into the Pi VM. */
310
310
  forwardEnv?: string[];
311
+ /**
312
+ * Runtime profile context defaults. Merged with task.input.context at
313
+ * execution time because the selected runtime profile is known only after
314
+ * claim. Task entries override profile entries with the same slug.
315
+ */
316
+ runtimeProfileContext?: readonly ContextRef[];
311
317
  /**
312
318
  * Forwarded to `buildTaskUserPrompt` for per-type builders. Static
313
319
  * across tasks. Today no built-in builder needs per-task `extras` —
@@ -480,14 +486,14 @@ export declare interface InjectedTaskContext {
480
486
  }
481
487
 
482
488
  /**
483
- * Resolve a task's `input.context[]` and inject the side effects pi
489
+ * Resolve effective runtime context and inject the side effects Pi
484
490
  * needs. Safe to call with an empty array — returns an inert result.
485
491
  */
486
492
  export declare function injectTaskContext(args: InjectTaskContextArgs): Promise<InjectedTaskContext>;
487
493
 
488
494
  export declare interface InjectTaskContextArgs {
489
495
  /** Empty array (the default for any non-eval task) is a no-op. */
490
- context: TaskContext;
496
+ context: readonly ContextRef[];
491
497
  /** Guest filesystem handle. In production this is `managed.vm.fs`. */
492
498
  fs: VmFsForContext;
493
499
  /** Guest path where the active host workspace is mounted. */
@@ -922,15 +928,6 @@ declare const Task: Type.TObject<{
922
928
 
923
929
  declare type Task = Static<typeof Task>;
924
930
 
925
- /** Reusable input fragment for any task type. Soft cap at 5 items. */
926
- declare const TaskContext: Type.TArray<Type.TObject<{
927
- slug: Type.TString;
928
- binding: Type.TUnion<[Type.TLiteral<"skill">, Type.TLiteral<"context_inline">, Type.TLiteral<"prompt_prefix">, Type.TLiteral<"user_inline">]>;
929
- content: Type.TString;
930
- }>>;
931
-
932
- declare type TaskContext = Static<typeof TaskContext>;
933
-
934
931
  declare const TaskMessage: Type.TObject<{
935
932
  taskId: Type.TString;
936
933
  attemptN: Type.TNumber;
package/dist/index.js CHANGED
@@ -1965,7 +1965,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1965
1965
  ...options
1966
1966
  });
1967
1967
  /**
1968
- * 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.
1968
+ * 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.
1969
1969
  */
1970
1970
  var batchDeleteTasks = (options) => (options.client ?? client).delete({
1971
1971
  security: [
@@ -10030,7 +10030,7 @@ _Object_({
10030
10030
  * (server-side schema check). Self-assessment is a truthful self-rating,
10031
10031
  * NOT enforcement — `verification.passed=false` does not block /complete
10032
10032
  * and does not affect `acceptedAttemptN`. See
10033
- * `docs/understand/agent-runtime.md` for the full producer/judge flow.
10033
+ * `docs/use/tasks-and-runtime.md` for the full producer/judge flow.
10034
10034
  *
10035
10035
  * **Binding evaluation** (judgment tasks: `assess_brief`, `judge_pack`).
10036
10036
  * A separate task whose IS the application of `successCriteria` to
@@ -18037,7 +18037,7 @@ function createMoltNetTools(config) {
18037
18037
  const searchEntries = defineTool({
18038
18038
  name: "moltnet_search_entries",
18039
18039
  label: "Search MoltNet Diary Entries",
18040
- description: "Hybrid (semantic + lexical) search over diary entries. Optional tags / excludeTags / entryTypes filters AND with the query; the taskFilter shorthand expands into task:* provenance tags so `taskFilter: { taskType: \"fulfill_brief\" }` returns only entries from fulfill_brief attempts. Filters apply server-side before ranking.",
18040
+ description: "Hybrid (semantic + lexical) search over diary entries. Use proactively before non-trivial investigation, code changes, review, or episodic incident capture so prior decisions and recurring failures surface before you act. Do not search randomly: pass taskFilter for task/correlation-local searches and tags or entryTypes for broader prior-knowledge searches. Optional tags / excludeTags / entryTypes filters AND with the query; the taskFilter shorthand expands into task:* provenance tags so `taskFilter: { taskType: \"fulfill_brief\" }` returns only entries from fulfill_brief attempts. Filters apply server-side before ranking.",
18041
18041
  parameters: Type.Object({
18042
18042
  query: Type.String({ description: "Natural language search query" }),
18043
18043
  limit: Type.Optional(Type.Number({ description: "Max results (default 5)" })),
@@ -18096,12 +18096,18 @@ function createMoltNetTools(config) {
18096
18096
  const createEntry = defineTool({
18097
18097
  name: "moltnet_create_entry",
18098
18098
  label: "Create MoltNet Diary Entry",
18099
- description: "Create a new diary entry to record decisions, findings, incidents, or reflections. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
18099
+ description: "Create a new diary entry to record decisions, findings, incidents, or reflections. Before creating an episodic incident entry, first call moltnet_search_entries with the title/root-cause/error/watch-for terms plus taskFilter, tags, or entryTypes filters, then reference close matches instead of creating an isolated duplicate. During an active task, the entry is forced into the task diary and tagged with the task:* provenance namespace (task:id:<id>, task:type:<type>, task:attempt:<n>, plus task:correlation:<id> when set); an explicit diaryId mismatching the task diary is rejected. Use this tool — NOT `moltnet entry create` / `moltnet entry create-signed` via bash. The CLI path bypasses task-tag auto-injection and leaves entries invisible to taskFilter queries.",
18100
18100
  parameters: Type.Object({
18101
18101
  title: Type.String({ description: "Entry title (concise, descriptive)" }),
18102
18102
  content: Type.String({ description: "Entry content (markdown)" }),
18103
18103
  tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization" })),
18104
18104
  importance: Type.Optional(Type.Number({ description: "Importance 1-10 (default 5)" })),
18105
+ entryType: Type.Optional(Type.Union([
18106
+ Type.Literal("episodic"),
18107
+ Type.Literal("semantic"),
18108
+ Type.Literal("procedural"),
18109
+ Type.Literal("reflection")
18110
+ ], { description: "Entry type. Use episodic for incidents, workarounds, bugs, or recurrence evidence; defaults to semantic." })),
18105
18111
  diaryId: Type.Optional(Type.String({ description: "Explicit diary id. During an active task, must match the task diary or the call is rejected. Outside a task, overrides the env-derived diary." }))
18106
18112
  }),
18107
18113
  async execute(_id, params) {
@@ -18125,7 +18131,8 @@ function createMoltNetTools(config) {
18125
18131
  title: params.title,
18126
18132
  content: params.content,
18127
18133
  tags: mergedTags,
18128
- importance: params.importance ?? 5
18134
+ importance: params.importance ?? 5,
18135
+ ...params.entryType ? { entryType: params.entryType } : {}
18129
18136
  });
18130
18137
  return {
18131
18138
  content: [{
@@ -18135,6 +18142,8 @@ function createMoltNetTools(config) {
18135
18142
  title: entry.title,
18136
18143
  createdAt: entry.createdAt,
18137
18144
  diaryId: targetDiaryId,
18145
+ entryType: entry.entryType,
18146
+ importance: entry.importance,
18138
18147
  tags: mergedTags
18139
18148
  }, null, 2)
18140
18149
  }],
@@ -18514,6 +18523,29 @@ function buildRuntimeInstructor(ctx) {
18514
18523
  " requires human approval and is unavailable in headless task runs;",
18515
18524
  " never use it for routine git/gh.",
18516
18525
  "",
18526
+ "## Proactive memory use",
18527
+ "",
18528
+ "- Before non-trivial investigation, debugging, code changes, or review,",
18529
+ " check the task diary for relevant prior knowledge instead of waiting",
18530
+ " for a human to ask. Use `moltnet_diary_tags` for cheap reconnaissance,",
18531
+ " `moltnet_list_entries` when tags or task provenance are known, and",
18532
+ " `moltnet_search_entries` for semantic similarity. Do not search",
18533
+ " randomly: pass `taskFilter` for task-local or correlation-local",
18534
+ " queries, and pass `tags` / `entryTypes` for broader prior-knowledge",
18535
+ " queries using known tags such as `incident`, `decision`, or",
18536
+ " `scope:<area>`. Broaden only after constrained searches miss.",
18537
+ "- Before creating an `episodic` incident entry, you MUST search for",
18538
+ " similar incidents using the proposed title, root cause, error text,",
18539
+ " affected subsystem, and watch-for terms, filtered by `entryTypes:",
18540
+ " [\"episodic\", \"semantic\"]` and any known `scope:*` / task provenance",
18541
+ " tags. If a close prior match exists, do not create an isolated",
18542
+ " duplicate: reference the prior entry in your response or diary content,",
18543
+ " update/link it when the new occurrence adds material evidence, or",
18544
+ " create a new recurrence entry only when the recurrence itself is",
18545
+ " important signal.",
18546
+ "- When you create a recurrence entry, include the prior matching entry",
18547
+ " id(s) in the content and explain what is new about this occurrence.",
18548
+ "",
18517
18549
  "## Diary discipline",
18518
18550
  "",
18519
18551
  `- During this task, every diary entry MUST land in \`${ctx.diaryId}\``,
@@ -19905,8 +19937,19 @@ async function resolvePersistentSessionManager(args) {
19905
19937
  //#endregion
19906
19938
  //#region ../agent-runtime/src/context-bindings.ts
19907
19939
  var PROMPT_SEPARATOR = "\n\n---\n\n";
19940
+ var MAX_MERGED_RUNTIME_CONTEXT_ENTRIES = 10;
19941
+ /**
19942
+ * Merge runtime-profile context defaults with task-scoped context. Profile
19943
+ * entries are defaults; task entries with the same slug override them.
19944
+ */
19945
+ function mergeRuntimeProfileContext(profileContext, taskContext) {
19946
+ const taskSlugs = new Set(taskContext.map((ref) => ref.slug));
19947
+ const merged = [...profileContext.filter((ref) => !taskSlugs.has(ref.slug)), ...taskContext];
19948
+ if (merged.length > MAX_MERGED_RUNTIME_CONTEXT_ENTRIES) throw new Error(`merged runtime context has ${merged.length} entries; maximum is ${MAX_MERGED_RUNTIME_CONTEXT_ENTRIES}`);
19949
+ return merged;
19950
+ }
19908
19951
  /**
19909
- * Resolve `task.input.context[]` into delivered side-effects (skills
19952
+ * Resolve runtime context entries into delivered side-effects (skills
19910
19953
  * persisted via `deliver.skill`) and prompt fragments
19911
19954
  * (`systemPromptPrefix`, `userInlineSuffix`) the caller weaves into the
19912
19955
  * built prompt.
@@ -19926,9 +19969,10 @@ var PROMPT_SEPARATOR = "\n\n---\n\n";
19926
19969
  * - `user_inline` → content appended to `userInlineSuffix` in
19927
19970
  * declared order, same separator.
19928
19971
  *
19929
- * No fetching, no hashing — bytes are inlined in `ContextRef.content`,
19930
- * and the task's `inputCid` already pins the entire input. The proposer
19931
- * chose these bytes; the resolver just dispatches them.
19972
+ * No fetching, no hashing — bytes are inlined in `ContextRef.content`.
19973
+ * Task-scoped entries are pinned by the task's `inputCid`; profile-scoped
19974
+ * entries are pinned by the runtime profile revision/source the daemon
19975
+ * resolved. The resolver just dispatches already-selected bytes.
19932
19976
  *
19933
19977
  * The function is pure with respect to its arguments: file writes are
19934
19978
  * confined to the injected `deliver` callback, which makes the
@@ -19974,12 +20018,13 @@ function formatInlineContextBlock(slug, content) {
19974
20018
  "### Injected Task Context",
19975
20019
  "",
19976
20020
  `Context id: \`${slug}\``,
19977
- "The following raw context was supplied by the task creator. Treat it",
19978
- "as task-relevant background that may override generic coding instincts",
19979
- "when it contains repo- or workflow-specific constraints.",
20021
+ "The following raw context was selected for this task by its task input",
20022
+ "or runtime profile. Treat it as task-relevant background that may",
20023
+ "override generic coding instincts when it contains repo- or",
20024
+ "workflow-specific constraints.",
19980
20025
  "The same content may also be materialized by the runtime under",
19981
20026
  "`/moltnet-task-context/context` for tool-based inspection. Do not",
19982
- "create or rely on workspace mirror files for this task context.",
20027
+ "create or rely on workspace mirror files for this runtime context.",
19983
20028
  "",
19984
20029
  "<context>",
19985
20030
  content,
@@ -20114,6 +20159,22 @@ function buildFinalOutputBlock(opts) {
20114
20159
  return lines.join("\n");
20115
20160
  }
20116
20161
  //#endregion
20162
+ //#region ../agent-runtime/src/prompts/proactive-memory.ts
20163
+ function buildProactiveMemoryWorkflowBlock() {
20164
+ return [
20165
+ "Before material work, apply the runtime instructor's proactive memory",
20166
+ "rules instead of waiting for a human prompt. Start with constrained",
20167
+ "diary context: inspect tags/list entries when task provenance or scope",
20168
+ "tags are known, then use `moltnet_search_entries` with `taskFilter`,",
20169
+ "`entryTypes`, and tags. Do not run broad unfiltered searches before",
20170
+ "constrained searches miss.",
20171
+ "",
20172
+ "For incident capture, follow the runtime instructor exactly: search",
20173
+ "for similar episodic/semantic entries first, reference close matches,",
20174
+ "and create a recurrence entry only when the repeat is useful signal."
20175
+ ].join("\n");
20176
+ }
20177
+ //#endregion
20117
20178
  //#region ../agent-runtime/src/prompts/rubric-common.ts
20118
20179
  function renderRubricCriteriaList(rubric) {
20119
20180
  return rubric.criteria.map((c, i) => `${i + 1}. **${c.id}** (weight ${c.weight}, scoring: \`${c.scoring}\`) — ${c.description}`).join("\n");
@@ -20215,6 +20276,12 @@ function buildAssessBriefUserPrompt(input, ctx) {
20215
20276
  header: "Querying the producer's diary entries",
20216
20277
  body: diaryQuery
20217
20278
  },
20279
+ {
20280
+ id: "assess_brief.proactive_memory",
20281
+ source: "discipline",
20282
+ header: "Proactive memory use",
20283
+ body: buildProactiveMemoryWorkflowBlock()
20284
+ },
20218
20285
  {
20219
20286
  id: "assess_brief.workspace",
20220
20287
  source: "workspace",
@@ -20542,11 +20609,13 @@ function buildFreeformUserPrompt(input, ctx) {
20542
20609
  const suggestedTaskType = input.suggestedTaskType ? [`The proposer suggested task type \`${input.suggestedTaskType}\`.`, "Use it as a hint, not as a contract."].join("\n") : "";
20543
20610
  const workflow = [
20544
20611
  "1. Clarify the real objective from the brief before acting.",
20545
- "2. Gather enough context to avoid guessing.",
20546
- "3. Complete the requested work when it is safe and bounded.",
20547
- "4. If the request reveals a recurring task shape, include a",
20612
+ "2. Search MoltNet diary memory for prior decisions, incidents, and",
20613
+ " recurring traps relevant to the brief.",
20614
+ "3. Gather enough context to avoid guessing.",
20615
+ "4. Complete the requested work when it is safe and bounded.",
20616
+ "5. If the request reveals a recurring task shape, include a",
20548
20617
  " `proposedTaskType` in the final output with a concise rationale.",
20549
- "5. If you changed code on a branch, include that branch in",
20618
+ "6. If you changed code on a branch, include that branch in",
20550
20619
  " `branch` so future continuations can recover git context."
20551
20620
  ].join("\n");
20552
20621
  const sections = [
@@ -20585,6 +20654,12 @@ function buildFreeformUserPrompt(input, ctx) {
20585
20654
  header: "Workflow",
20586
20655
  body: workflow
20587
20656
  },
20657
+ {
20658
+ id: "freeform.proactive_memory",
20659
+ source: "discipline",
20660
+ header: "Proactive memory use",
20661
+ body: buildProactiveMemoryWorkflowBlock()
20662
+ },
20588
20663
  {
20589
20664
  id: "freeform.verification",
20590
20665
  source: "verification",
@@ -20661,13 +20736,15 @@ function buildFulfillBriefUserPrompt(input, ctx) {
20661
20736
  ].join("\n") : "";
20662
20737
  const workflow = [
20663
20738
  ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
20664
- "2. Understand the problem read relevant code; do not speculate.",
20665
- "3. Implement the change. Keep commits small and coherent.",
20666
- "4. Add tests if applicable.",
20667
- "5. For every commit, create a signed diary entry first via",
20739
+ "2. Search MoltNet diary memory for prior decisions, incidents, and",
20740
+ " recurring traps relevant to the brief before changing code.",
20741
+ "3. Understand the problem — read relevant code; do not speculate.",
20742
+ "4. Implement the change. Keep commits small and coherent.",
20743
+ "5. Add tests if applicable.",
20744
+ "6. For every commit, create a signed diary entry first via",
20668
20745
  " `moltnet_create_entry` and embed its id in the commit trailer",
20669
20746
  " `MoltNet-Diary: <id>` (per the runtime instructor).",
20670
- "6. Push the branch and open a PR — run `git push` and `gh pr create`",
20747
+ "7. Push the branch and open a PR — run `git push` and `gh pr create`",
20671
20748
  " IN the VM with your normal `bash` tool (use the",
20672
20749
  " `GH_TOKEN=$(moltnet github token …) gh …` form from the runtime",
20673
20750
  " instructor). Do NOT use `moltnet_host_exec` for this; it needs human",
@@ -20709,6 +20786,12 @@ function buildFulfillBriefUserPrompt(input, ctx) {
20709
20786
  header: "Workflow",
20710
20787
  body: workflow
20711
20788
  },
20789
+ {
20790
+ id: "fulfill_brief.proactive_memory",
20791
+ source: "discipline",
20792
+ header: "Proactive memory use",
20793
+ body: buildProactiveMemoryWorkflowBlock()
20794
+ },
20712
20795
  {
20713
20796
  id: "fulfill_brief.verification",
20714
20797
  source: "verification",
@@ -21024,13 +21107,15 @@ function buildPrReviewUserPrompt(input, ctx) {
21024
21107
  const workflow = [
21025
21108
  "1. Read the subject summary, resources, inspection hints, and any",
21026
21109
  " task-specific instructions before scoring.",
21027
- "2. Inspect the target artefact directly using the tools and resources the",
21110
+ "2. Search MoltNet diary memory for prior decisions, incidents, and",
21111
+ " recurring review traps relevant to the subject.",
21112
+ "3. Inspect the target artefact directly using the tools and resources the",
21028
21113
  " task makes available.",
21029
- "3. If you are in a dedicated disposable worktree and need the review target",
21114
+ "4. If you are in a dedicated disposable worktree and need the review target",
21030
21115
  " checked out locally, do that work inside this disposable workspace only.",
21031
- "4. Apply the rubric strictly. This task is about complexity and",
21116
+ "5. Apply the rubric strictly. This task is about complexity and",
21032
21117
  " reviewability, not correctness or feature desirability.",
21033
- "5. Perform any required outward action before emitting the final",
21118
+ "6. Perform any required outward action before emitting the final",
21034
21119
  " structured output."
21035
21120
  ].join("\n");
21036
21121
  const taskPromptSection = input.taskPrompt ?? "";
@@ -21087,6 +21172,12 @@ function buildPrReviewUserPrompt(input, ctx) {
21087
21172
  header: "Review workflow",
21088
21173
  body: workflow
21089
21174
  },
21175
+ {
21176
+ id: "pr_review.proactive_memory",
21177
+ source: "discipline",
21178
+ header: "Proactive memory use",
21179
+ body: buildProactiveMemoryWorkflowBlock()
21180
+ },
21090
21181
  {
21091
21182
  id: "pr_review.task_prompt",
21092
21183
  source: "task_input",
@@ -21256,11 +21347,11 @@ function buildRenderPackUserPrompt(input, ctx) {
21256
21347
  * `judge_eval_attempt` task(s) grade against their own hidden rubric.
21257
21348
  *
21258
21349
  * Context delivery is handled by `resolveTaskContext` (see
21259
- * libs/agent-runtime/src/context-bindings.ts) and runs BEFORE this
21260
- * prompt is rendered: `prompt_prefix` items are concatenated ahead of
21261
- * the body, `skill` items are persisted at the runtime's skill path,
21262
- * and `user_inline` items are appended to the first user message. This
21263
- * builder does NOT inline `input.context[]` itself.
21350
+ * libs/agent-runtime/src/context-bindings.ts) and is selected BEFORE this
21351
+ * prompt is rendered. Task-scoped context lives in `input.context`; runtime
21352
+ * profile defaults arrive as `ctx.effectiveRuntimeContext` after the runtime
21353
+ * merges them with task context. This builder only renders context
21354
+ * discipline; it does NOT inline context bytes itself.
21264
21355
  *
21265
21356
  * Prompt-shape notes (issue #1175, area 1):
21266
21357
  * - No `Correlation` section: the agent never acts on it. The id is
@@ -21278,12 +21369,13 @@ function buildRenderPackUserPrompt(input, ctx) {
21278
21369
  */
21279
21370
  function buildRunEvalUserPrompt(input, ctx) {
21280
21371
  const { scenario, variantLabel, successCriteria } = input;
21281
- const hasContext = input.context.length > 0;
21282
- const hasInlineContext = input.context.some((entry) => entry.binding === "context_inline");
21372
+ const effectiveRuntimeContext = ctx.effectiveRuntimeContext ?? input.context;
21373
+ const hasContext = effectiveRuntimeContext.length > 0;
21374
+ const hasInlineContext = effectiveRuntimeContext.some((entry) => entry.binding === "context_inline");
21283
21375
  const header = `# Run Eval Agent\n\nYou are running an evaluation scenario as variant \`${variantLabel}\`.\nTask id: \`${ctx.taskId}\``;
21284
21376
  const contextDiscipline = hasContext ? [
21285
21377
  "This task includes Injected Task Context supplied by the task",
21286
- "creator. You MUST inspect it BEFORE you write solution files or",
21378
+ "input or runtime profile. You MUST inspect it BEFORE you write solution files or",
21287
21379
  "draft your final answer — not after.",
21288
21380
  "",
21289
21381
  "Reconcile every constraint from that context **into the code path",
@@ -21456,7 +21548,8 @@ function buildTaskUserPrompt(task, ctx) {
21456
21548
  return buildRunEvalUserPrompt(task.input, {
21457
21549
  diaryId: ctx.diaryId,
21458
21550
  taskId: ctx.taskId,
21459
- correlationId: task.correlationId
21551
+ correlationId: task.correlationId,
21552
+ effectiveRuntimeContext: ctx.effectiveRuntimeContext
21460
21553
  });
21461
21554
  default: throw new Error(`No prompt builder registered for taskType="${task.taskType}"`);
21462
21555
  }
@@ -24929,119 +25022,6 @@ var require_multistream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
24929
25022
  module.exports.pino = pino;
24930
25023
  })))();
24931
25024
  //#endregion
24932
- //#region src/runtime/inject-task-context.ts
24933
- /**
24934
- * Slice 1.5 of #943 — wire the agent-runtime resolver into the
24935
- * pi-extension execution path.
24936
- *
24937
- * `resolveTaskContext` is a pure dispatcher; this module provides the
24938
- * Gondolin-aware deliverer and the post-resolution shape the
24939
- * `execute-pi-task` caller needs to splice into pi's setup:
24940
- *
24941
- * - `systemPromptPrefix` → fed into `appendSystemPrompt` alongside
24942
- * the runtime instructor (it IS a system-prompt fragment).
24943
- * - `userInlineSuffix` → appended to the `buildTaskUserPrompt`
24944
- * output BEFORE `session.prompt(text)`.
24945
- * - `skills` → spliced into the `skillsOverride` callback's
24946
- * return value. pi includes them in `<available_skills>` in the
24947
- * system prompt; the agent fetches the body on demand via the
24948
- * Read tool.
24949
- *
24950
- * Task-context files are written into a memory-backed VM mount. pi only reads
24951
- * `<available_skills>` metadata (name, description, location), never the skill
24952
- * body, so we construct synthetic `Skill` objects pointing at the in-VM path
24953
- * without ever materialising the file on the host.
24954
- */
24955
- /**
24956
- * Where in the VM we write task-context bodies — the memory-backed mount
24957
- * declared in `vm-manager.ts`. See the comment on
24958
- * `GUEST_TASK_CONTEXT_MOUNT` there for the full rationale (ephemeral by
24959
- * intent + the worktree symlink interaction with Gondolin's sandbox-escape
24960
- * protection). The agent's Gondolin Read tool accepts paths under this mount
24961
- * via `toGuestPath` in `tool-operations.ts`.
24962
- */
24963
- var SKILL_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/skills`;
24964
- var INLINE_CONTEXT_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/context`;
24965
- /** Bounds borrowed from pi's skill validation; conservative caps so a
24966
- * malformed SKILL.md doesn't bloat the system prompt. */
24967
- var MAX_SKILL_NAME = 64;
24968
- var MAX_SKILL_DESCRIPTION = 1024;
24969
- /**
24970
- * Resolve a task's `input.context[]` and inject the side effects pi
24971
- * needs. Safe to call with an empty array — returns an inert result.
24972
- */
24973
- async function injectTaskContext(args) {
24974
- const skills = [];
24975
- args.guestWorkspace;
24976
- const resolved = await resolveTaskContext({
24977
- context: args.context,
24978
- deliver: {
24979
- skill: async ({ slug, content }) => {
24980
- const dir = `${SKILL_ROOT_IN_VM}/${slug}`;
24981
- const filePath = `${dir}/SKILL.md`;
24982
- await args.fs.mkdir(dir, { recursive: true });
24983
- await args.fs.writeFile(filePath, content, { mode: 420 });
24984
- skills.push(buildSyntheticSkill({
24985
- slug,
24986
- content,
24987
- filePath,
24988
- dir
24989
- }));
24990
- },
24991
- contextFile: async ({ suggestedFileName, content }) => {
24992
- await args.fs.mkdir(INLINE_CONTEXT_ROOT_IN_VM, { recursive: true });
24993
- const filePath = `${INLINE_CONTEXT_ROOT_IN_VM}/${suggestedFileName}`;
24994
- await args.fs.writeFile(filePath, content, { mode: 420 });
24995
- }
24996
- }
24997
- });
24998
- return {
24999
- injected: resolved.injected,
25000
- skills,
25001
- systemPromptPrefix: resolved.systemPromptPrefix,
25002
- userInlineSuffix: resolved.userInlineSuffix
25003
- };
25004
- }
25005
- /**
25006
- * Build a `Skill` object pi will faithfully render in
25007
- * `<available_skills>`. We extract `name` and `description` from the
25008
- * skill content's YAML frontmatter using pi's own `parseFrontmatter`
25009
- * helper (proper YAML, not a regex hack) and fall back to the slug +
25010
- * a generic description so a SKILL.md without frontmatter still
25011
- * renders something meaningful.
25012
- *
25013
- * Frontmatter parsing is best-effort: a malformed YAML block is
25014
- * optional metadata, not a reason to fail the task. We swallow parser
25015
- * errors and fall back to the slug-derived metadata; the skill body
25016
- * is unaffected.
25017
- *
25018
- * pi's `formatSkillsForPrompt` only reads `name`, `description`, and
25019
- * `filePath` — `sourceInfo`/`baseDir` exist on the type but never
25020
- * surface in the prompt, so a synthetic `SourceInfo` is enough.
25021
- */
25022
- function buildSyntheticSkill(args) {
25023
- let fm = {};
25024
- try {
25025
- fm = parseFrontmatter(args.content).frontmatter;
25026
- } catch {}
25027
- return {
25028
- name: clip(typeof fm.name === "string" && fm.name.trim().length > 0 ? fm.name.trim() : args.slug, MAX_SKILL_NAME),
25029
- description: clip(typeof fm.description === "string" && fm.description.trim().length > 0 ? fm.description.trim() : `Task-injected context skill (${args.slug})`, MAX_SKILL_DESCRIPTION),
25030
- filePath: args.filePath,
25031
- baseDir: args.dir,
25032
- sourceInfo: createSyntheticSourceInfo(args.filePath, {
25033
- source: "moltnet:task-context",
25034
- scope: "temporary",
25035
- origin: "top-level",
25036
- baseDir: args.dir
25037
- }),
25038
- disableModelInvocation: fm["disable-model-invocation"] === true
25039
- };
25040
- }
25041
- function clip(s, max) {
25042
- return s.length > max ? s.slice(0, max) : s;
25043
- }
25044
- //#endregion
25045
25025
  //#region src/runtime/resolve-prior-context.ts
25046
25026
  /**
25047
25027
  * Fetch the named attempt's output and project it into the prompt's
@@ -25228,6 +25208,113 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
25228
25208
  }
25229
25209
  }
25230
25210
  //#endregion
25211
+ //#region src/runtime/runtime-context.ts
25212
+ /**
25213
+ * Pi-specific runtime context handling.
25214
+ *
25215
+ * `@themoltnet/agent-runtime` owns generic context semantics: merge profile
25216
+ * defaults with task context, resolve bindings, and produce prompt fragments.
25217
+ * This module owns the Pi/Gondolin boundary: validate effective context for an
25218
+ * attempt, write skill/context files into the VM, and build synthetic Pi Skill
25219
+ * metadata for injected skill bindings.
25220
+ */
25221
+ /**
25222
+ * Where in the VM we write runtime-context bodies — the memory-backed mount
25223
+ * declared in `vm-manager.ts`. See the comment on
25224
+ * `GUEST_TASK_CONTEXT_MOUNT` there for the full rationale (ephemeral by
25225
+ * intent + the worktree symlink interaction with Gondolin's sandbox-escape
25226
+ * protection). The agent's Gondolin Read tool accepts paths under this mount
25227
+ * via `toGuestPath` in `tool-operations.ts`.
25228
+ */
25229
+ var SKILL_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/skills`;
25230
+ var INLINE_CONTEXT_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/context`;
25231
+ /** Bounds borrowed from pi's skill validation; conservative caps so a
25232
+ * malformed SKILL.md doesn't bloat the system prompt. */
25233
+ var MAX_SKILL_NAME = 64;
25234
+ var MAX_SKILL_DESCRIPTION = 1024;
25235
+ function resolveEffectiveRuntimeContext(args) {
25236
+ const taskContext = args.rawTaskContext === void 0 ? [] : args.rawTaskContext;
25237
+ if (!Check(TaskContext, taskContext)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, taskContext)].slice(0, 3))}`);
25238
+ const profileContext = args.runtimeProfileContext ?? [];
25239
+ if (!Check(TaskContext, profileContext)) throw new Error(`runtime profile context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, profileContext)].slice(0, 3))}`);
25240
+ return mergeRuntimeProfileContext(profileContext, taskContext);
25241
+ }
25242
+ /**
25243
+ * Resolve effective runtime context and inject the side effects Pi
25244
+ * needs. Safe to call with an empty array — returns an inert result.
25245
+ */
25246
+ async function injectRuntimeContext(args) {
25247
+ const skills = [];
25248
+ args.guestWorkspace;
25249
+ const resolved = await resolveTaskContext({
25250
+ context: args.context,
25251
+ deliver: {
25252
+ skill: async ({ slug, content }) => {
25253
+ const dir = `${SKILL_ROOT_IN_VM}/${slug}`;
25254
+ const filePath = `${dir}/SKILL.md`;
25255
+ await args.fs.mkdir(dir, { recursive: true });
25256
+ await args.fs.writeFile(filePath, content, { mode: 420 });
25257
+ skills.push(buildSyntheticSkill({
25258
+ slug,
25259
+ content,
25260
+ filePath,
25261
+ dir
25262
+ }));
25263
+ },
25264
+ contextFile: async ({ suggestedFileName, content }) => {
25265
+ await args.fs.mkdir(INLINE_CONTEXT_ROOT_IN_VM, { recursive: true });
25266
+ const filePath = `${INLINE_CONTEXT_ROOT_IN_VM}/${suggestedFileName}`;
25267
+ await args.fs.writeFile(filePath, content, { mode: 420 });
25268
+ }
25269
+ }
25270
+ });
25271
+ return {
25272
+ injected: resolved.injected,
25273
+ skills,
25274
+ systemPromptPrefix: resolved.systemPromptPrefix,
25275
+ userInlineSuffix: resolved.userInlineSuffix
25276
+ };
25277
+ }
25278
+ /**
25279
+ * Build a `Skill` object pi will faithfully render in
25280
+ * `<available_skills>`. We extract `name` and `description` from the
25281
+ * skill content's YAML frontmatter using pi's own `parseFrontmatter`
25282
+ * helper (proper YAML, not a regex hack) and fall back to the slug +
25283
+ * a generic description so a SKILL.md without frontmatter still
25284
+ * renders something meaningful.
25285
+ *
25286
+ * Frontmatter parsing is best-effort: a malformed YAML block is
25287
+ * optional metadata, not a reason to fail the task. We swallow parser
25288
+ * errors and fall back to the slug-derived metadata; the skill body
25289
+ * is unaffected.
25290
+ *
25291
+ * pi's `formatSkillsForPrompt` only reads `name`, `description`, and
25292
+ * `filePath` — `sourceInfo`/`baseDir` exist on the type but never
25293
+ * surface in the prompt, so a synthetic `SourceInfo` is enough.
25294
+ */
25295
+ function buildSyntheticSkill(args) {
25296
+ let fm = {};
25297
+ try {
25298
+ fm = parseFrontmatter(args.content).frontmatter;
25299
+ } catch {}
25300
+ return {
25301
+ name: clip(typeof fm.name === "string" && fm.name.trim().length > 0 ? fm.name.trim() : args.slug, MAX_SKILL_NAME),
25302
+ description: clip(typeof fm.description === "string" && fm.description.trim().length > 0 ? fm.description.trim() : `Runtime-injected context skill (${args.slug})`, MAX_SKILL_DESCRIPTION),
25303
+ filePath: args.filePath,
25304
+ baseDir: args.dir,
25305
+ sourceInfo: createSyntheticSourceInfo(args.filePath, {
25306
+ source: "moltnet:runtime-context",
25307
+ scope: "temporary",
25308
+ origin: "top-level",
25309
+ baseDir: args.dir
25310
+ }),
25311
+ disableModelInvocation: fm["disable-model-invocation"] === true
25312
+ };
25313
+ }
25314
+ function clip(s, max) {
25315
+ return s.length > max ? s.slice(0, max) : s;
25316
+ }
25317
+ //#endregion
25231
25318
  //#region src/runtime/subagent-tool.ts
25232
25319
  var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
25233
25320
  var DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES = 2;
@@ -25763,6 +25850,46 @@ function resolveSubmitTools(taskType, opts = {}) {
25763
25850
  };
25764
25851
  }
25765
25852
  //#endregion
25853
+ //#region src/runtime/task-event-emitter.ts
25854
+ var LOG_TRUNCATE_LIMIT = 4 * 1024;
25855
+ async function emitTaskEvent(input) {
25856
+ try {
25857
+ input.onTurnEvent(input.kind, summarizePayloadForLog(input.kind, input.payload));
25858
+ } catch (err) {
25859
+ process.stderr.write(`[emit] onTurnEvent threw for kind="${input.kind}": ${err instanceof Error ? err.message : String(err)}\n`);
25860
+ }
25861
+ try {
25862
+ await input.reporter.record({
25863
+ kind: input.kind,
25864
+ payload: input.payload
25865
+ });
25866
+ } catch (err) {
25867
+ const detail = err instanceof Error ? err.message : String(err);
25868
+ input.log(`executePiTask: reporter.record() failed for task ${input.taskId} attempt ${input.attemptN} kind="${input.kind}": ${detail}`);
25869
+ }
25870
+ }
25871
+ function summarizePayloadForLog(kind, payload) {
25872
+ switch (kind) {
25873
+ case "text_delta": {
25874
+ const delta = payload.delta;
25875
+ return { chars: typeof delta === "string" ? delta.length : 0 };
25876
+ }
25877
+ case "tool_call_start": return { tool: payload.tool_name };
25878
+ case "tool_call_end": return {
25879
+ tool: payload.tool_name,
25880
+ is_error: payload.is_error === true,
25881
+ ...payload.is_error === true && payload.result !== void 0 ? { result: payload.result } : {}
25882
+ };
25883
+ case "turn_end": return { stop_reason: payload.stop_reason };
25884
+ case "error": return {
25885
+ phase: payload.phase,
25886
+ message: typeof payload.message === "string" ? payload.message.slice(0, LOG_TRUNCATE_LIMIT) : payload.message
25887
+ };
25888
+ case "info": return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, typeof v === "string" ? v.slice(0, LOG_TRUNCATE_LIMIT) : v]));
25889
+ default: return payload;
25890
+ }
25891
+ }
25892
+ //#endregion
25766
25893
  //#region src/runtime/task-workspace.ts
25767
25894
  function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
25768
25895
  const branch = executionPlan?.worktreeBranch ?? null;
@@ -26131,15 +26258,17 @@ async function executePiTask(claimedTask, reporter, opts) {
26131
26258
  onTurnEvent = noopTurnEventHandler;
26132
26259
  }
26133
26260
  else onTurnEvent = opts.onTurnEvent ?? noopTurnEventHandler;
26134
- const emit = (kind, payload) => {
26135
- try {
26136
- onTurnEvent(kind, summarizePayloadForLog(kind, payload));
26137
- } catch (err) {
26138
- process.stderr.write(`[emit] onTurnEvent threw for kind="${kind}": ${err instanceof Error ? err.message : String(err)}\n`);
26139
- }
26140
- return reporter.record({
26261
+ const emit = async (kind, payload) => {
26262
+ await emitTaskEvent({
26141
26263
  kind,
26142
- payload
26264
+ payload,
26265
+ onTurnEvent,
26266
+ reporter,
26267
+ taskId: task.id,
26268
+ attemptN,
26269
+ log: (message) => {
26270
+ process.stderr.write(`${message}\n`);
26271
+ }
26143
26272
  });
26144
26273
  };
26145
26274
  const emitError = async (phase, message, extra = {}) => {
@@ -26243,6 +26372,21 @@ async function executePiTask(claimedTask, reporter, opts) {
26243
26372
  message
26244
26373
  });
26245
26374
  }
26375
+ const rawContext = task.input.context;
26376
+ let effectiveRuntimeContext;
26377
+ try {
26378
+ effectiveRuntimeContext = resolveEffectiveRuntimeContext({
26379
+ rawTaskContext: rawContext,
26380
+ runtimeProfileContext: opts.runtimeProfileContext
26381
+ });
26382
+ } catch (err) {
26383
+ const message = err instanceof Error ? err.message : String(err);
26384
+ await emit("error", {
26385
+ message,
26386
+ phase: "context_resolution"
26387
+ });
26388
+ return makeFailedOutput("context_resolution_failed", message);
26389
+ }
26246
26390
  let taskPrompt;
26247
26391
  try {
26248
26392
  const assembled = buildTaskUserPrompt(task, {
@@ -26255,7 +26399,8 @@ async function executePiTask(claimedTask, reporter, opts) {
26255
26399
  source: executionPlan?.workspaceSeed?.source === "producer" ? "producer_copy" : executionPlan?.workspaceAttachment !== void 0 ? "producer_attachment" : void 0
26256
26400
  },
26257
26401
  extras: opts.promptExtras,
26258
- priorContext: resolvedPriorContext
26402
+ priorContext: resolvedPriorContext,
26403
+ effectiveRuntimeContext
26259
26404
  });
26260
26405
  taskPrompt = assembled.text;
26261
26406
  await emit("info", {
@@ -26272,13 +26417,10 @@ async function executePiTask(claimedTask, reporter, opts) {
26272
26417
  });
26273
26418
  return makeFailedOutput("prompt_build_failed", message);
26274
26419
  }
26275
- const rawContext = task.input.context;
26276
26420
  let injectedContext;
26277
26421
  try {
26278
- const contextArray = rawContext === void 0 ? [] : rawContext;
26279
- if (!Check(TaskContext, contextArray)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, contextArray)].slice(0, 3))}`);
26280
- injectedContext = await injectTaskContext({
26281
- context: contextArray,
26422
+ injectedContext = await injectRuntimeContext({
26423
+ context: effectiveRuntimeContext,
26282
26424
  fs: managed.vm.fs,
26283
26425
  guestWorkspace: managed.guestWorkspace
26284
26426
  });
@@ -26870,27 +27012,6 @@ function wireSessionAbort(cancelSignal, session) {
26870
27012
  * `task_messages.payload` row. Bodies above 4 KiB are replaced with a
26871
27013
  * `{ truncated, original_size }` marker so the JSONL/DB size stays bounded.
26872
27014
  */
26873
- function summarizePayloadForLog(kind, payload) {
26874
- switch (kind) {
26875
- case "text_delta": {
26876
- const delta = payload.delta;
26877
- return { chars: typeof delta === "string" ? delta.length : 0 };
26878
- }
26879
- case "tool_call_start": return { tool: payload.tool_name };
26880
- case "tool_call_end": return {
26881
- tool: payload.tool_name,
26882
- is_error: payload.is_error === true,
26883
- ...payload.is_error === true && payload.result !== void 0 ? { result: payload.result } : {}
26884
- };
26885
- case "turn_end": return { stop_reason: payload.stop_reason };
26886
- case "error": return {
26887
- phase: payload.phase,
26888
- message: typeof payload.message === "string" ? payload.message.slice(0, TRUNCATE_LIMIT) : payload.message
26889
- };
26890
- case "info": return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, typeof v === "string" ? v.slice(0, TRUNCATE_LIMIT) : v]));
26891
- default: return payload;
26892
- }
26893
- }
26894
27015
  /**
26895
27016
  * Classify a `tool_execution_end` event for telemetry purposes.
26896
27017
  *
@@ -27487,4 +27608,4 @@ function moltnetExtension(pi) {
27487
27608
  registerMoltnetReflectCommand(pi, state);
27488
27609
  }
27489
27610
  //#endregion
27490
- export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiProviderErrorRetryUi, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
27611
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiProviderErrorRetryUi, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectRuntimeContext as injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.32.1",
3
+ "version": "0.33.1",
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/sdk": "0.119.0",
40
- "@themoltnet/agent-runtime": "0.34.1"
39
+ "@themoltnet/agent-runtime": "0.35.1",
40
+ "@themoltnet/sdk": "0.119.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",