@themoltnet/pi-extension 0.31.0 → 0.31.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/README.md CHANGED
@@ -216,8 +216,9 @@ Shell commands that run on every VM resume, after platform setup and before the
216
216
  agent session starts.
217
217
 
218
218
  Use this for per-session bootstrap that should not invalidate the snapshot
219
- cache: mounting tmpfs, warming package-manager state, lightweight repo-local
220
- setup.
219
+ cache: enabling package-manager shims or lightweight repo-local setup. Keep
220
+ network-heavy dependency fetches out of the default TUI startup path; run them
221
+ explicitly when a daemon/provisioning flow wants to prewarm a store.
221
222
 
222
223
  Important properties:
223
224
 
@@ -284,16 +285,22 @@ Dogfood trail:
284
285
  - `17f0ac6f-07f0-4e12-b5e5-d35a0fa2df6c` — first 100x pnpm recipe
285
286
  - `2e4e25a9-ef4b-46bf-a55d-6c2b1159ee61` — follow-up fix for per-workspace `node_modules`
286
287
 
287
- `vfs.shadow: ["node_modules"]` is still useful to hide host-built artifacts,
288
- but it does not solve the hot-path problem by itself. For fast pnpm setup, move
289
- both endpoints off the FUSE bridge:
288
+ The extension now always shadows any `node_modules` path into VM-local
289
+ executable storage. That is VFS policy rather than a resume command, so it also
290
+ covers worktrees the live agent creates after resume. Package-manager stores
291
+ are different: pnpm's content-addressed store should remain a reusable
292
+ guest-local directory, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`, not tmpfs.
293
+ Runtime profiles or `sandbox.json` should avoid network-heavy fetch/install
294
+ steps during the default TUI resume path. If a daemon/provisioning flow needs
295
+ fast first installs, prewarm the store explicitly with `pnpm fetch` after the
296
+ sandbox is available.
290
297
 
291
- - package store on guest-local disk, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`
292
- - install target on guest tmpfs via `resumeCommands`
298
+ Run the real VM integration check locally with:
293
299
 
294
- Current themoltnet `sandbox.json` does this by mounting tmpfs over the root and
295
- per-workspace `node_modules` directories before running `pnpm install
296
- --frozen-lockfile`.
300
+ ```bash
301
+ MOLTNET_PI_VM_INTEGRATION=1 \
302
+ pnpm exec nx run @themoltnet/pi-extension:test-ci--src/vm-manager.integration.test.ts
303
+ ```
297
304
 
298
305
  ### `env`
299
306
 
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { BashOperations } from '@earendil-works/pi-coding-agent';
4
4
  import { connect } from '@themoltnet/sdk';
5
5
  import { EditOperations } from '@earendil-works/pi-coding-agent';
6
6
  import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
+ import { ExtensionContext } from '@earendil-works/pi-coding-agent';
7
8
  import { LoadSkillsResult } from '@earendil-works/pi-coding-agent';
8
9
  import { Model } from '@earendil-works/pi-ai';
9
10
  import { Readable } from 'node:stream';
@@ -145,6 +146,8 @@ export declare function createMoltNetTools(config: MoltNetToolsConfig): ToolDefi
145
146
 
146
147
  export declare function createPiOtelExtension(options?: PiOtelOptions): (pi: ExtensionAPI) => void;
147
148
 
149
+ export declare function createPiProviderErrorRetryUi(ctx: ExtensionContext): ProviderErrorRetryUi | undefined;
150
+
148
151
  export declare function createPiRetryTriage(options: {
149
152
  model: Model<Api>;
150
153
  thinkingLevel?: PiRetryTriageThinkingLevel | null;
@@ -373,6 +376,34 @@ export declare interface ExecutePiTaskOptions {
373
376
  * Default `3`. Set to `0` to disable. Closes part of #1094.
374
377
  */
375
378
  maxBashTimeouts?: number;
379
+ /**
380
+ * Number of correction turns allowed after the first invalid submit-output
381
+ * tool call. A value of 2 permits three invalid submit calls total before
382
+ * the attempt fails with output_validation_failed.
383
+ */
384
+ maxSubmitValidationRetries?: number;
385
+ /**
386
+ * Cap provider-error retries inside the same Pi session. A retry is attempted
387
+ * only after a Pi assistant turn ends with `stopReason: "error"` and the
388
+ * provider diagnostic is not a known credential/model/config failure. This is
389
+ * distinct from daemon attempt retry: the active session keeps its context and
390
+ * receives a short continuation prompt.
391
+ *
392
+ * Default `2`. Set to `0` to disable.
393
+ */
394
+ maxProviderErrorRetries?: number;
395
+ /** Base delay for same-session provider-error retries. Default `2000`. */
396
+ providerErrorRetryBaseDelayMs?: number;
397
+ /** Maximum delay for same-session provider-error retries. Default `30000`. */
398
+ providerErrorRetryMaxDelayMs?: number;
399
+ /** Continuation prompt sent after a retryable provider error. Default `Go on`. */
400
+ providerErrorRetryPrompt?: string;
401
+ /**
402
+ * Optional UI adapter for interactive pi/TUI callers. The daemon normally
403
+ * leaves this unset and consumes the structured `provider_error_retry` task
404
+ * message instead.
405
+ */
406
+ providerErrorRetryUi?: ProviderErrorRetryUi;
376
407
  /**
377
408
  * Skip per-call UI approval for matching `moltnet_host_exec` commands.
378
409
  * Keep false/undefined for interactive consumers. `true` skips every dialog
@@ -647,6 +678,26 @@ declare interface PiWorkspaceSeedPlan {
647
678
  source: 'producer';
648
679
  }
649
680
 
681
+ export declare interface ProviderErrorRetryEvent extends Record<string, unknown> {
682
+ event: 'provider_error_retry';
683
+ retry: number;
684
+ maxRetries: number;
685
+ delayMs: number;
686
+ reason: string;
687
+ }
688
+
689
+ export declare type ProviderErrorRetryLevel = 'info' | 'warning' | 'error';
690
+
691
+ export declare interface ProviderErrorRetryUi {
692
+ /**
693
+ * Mirrors pi's `ctx.hasUI`. Undefined means "UI adapter is present"; false
694
+ * lets callers pass a stable adapter from both TUI and headless contexts.
695
+ */
696
+ hasUI?: boolean;
697
+ setStatus?: (key: string, message: string) => void | Promise<void>;
698
+ notify?: (message: string, level: ProviderErrorRetryLevel) => void | Promise<void>;
699
+ }
700
+
650
701
  export declare function redactRetryTriageSecrets(value: string): string;
651
702
 
652
703
  export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { readFile, realpath, stat } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
11
  import { pipeline } from "node:stream/promises";
12
12
  import { Type, getModel } from "@earendil-works/pi-ai";
13
- import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
13
+ import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, isWriteFlag, loadGuestAssets } from "@earendil-works/gondolin";
14
14
  import { parseEnv } from "node:util";
15
15
  import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
16
16
  //#region \0rolldown/runtime.js
@@ -14179,7 +14179,7 @@ _Object_({
14179
14179
  * @param errors - Field-level errors from `@moltnet/tasks` validators.
14180
14180
  * @returns One line per error, joined by newlines.
14181
14181
  */
14182
- function formatValidationErrors(errors) {
14182
+ function formatValidationErrors$1(errors) {
14183
14183
  return errors.map((e) => `${e.field}: ${e.message}`).join("\n");
14184
14184
  }
14185
14185
  /**
@@ -14192,7 +14192,7 @@ var TaskBuildError = class extends Error {
14192
14192
  /** The field-level validation errors that caused the build to fail. */
14193
14193
  errors;
14194
14194
  constructor(errors) {
14195
- super(`Task build failed:\n${formatValidationErrors(errors)}`);
14195
+ super(`Task build failed:\n${formatValidationErrors$1(errors)}`);
14196
14196
  this.name = "TaskBuildError";
14197
14197
  this.errors = errors;
14198
14198
  }
@@ -14207,7 +14207,7 @@ var TaskResultError = class extends Error {
14207
14207
  /** The field-level errors describing why the result could not be read. */
14208
14208
  errors;
14209
14209
  constructor(errors) {
14210
- super(`Task result error:\n${formatValidationErrors(errors)}`);
14210
+ super(`Task result error:\n${formatValidationErrors$1(errors)}`);
14211
14211
  this.name = "TaskResultError";
14212
14212
  this.errors = errors;
14213
14213
  }
@@ -18744,6 +18744,37 @@ function shouldRunResumeCommand(entry, ctx) {
18744
18744
  if (workspaceModes && !workspaceModes.includes(ctx.workspaceMode)) return false;
18745
18745
  return true;
18746
18746
  }
18747
+ function shouldShadowNodeModulesPath(pathname) {
18748
+ const normalized = path.posix.normalize(pathname);
18749
+ return normalized === "/node_modules" || normalized.startsWith("/node_modules/") || normalized.endsWith("/node_modules") || normalized.includes("/node_modules/");
18750
+ }
18751
+ function isNodeModulesBinPath(pathname) {
18752
+ const normalized = path.posix.normalize(pathname);
18753
+ return normalized.includes("/node_modules/.bin/") || normalized.startsWith("/node_modules/.bin/");
18754
+ }
18755
+ var AutoParentMemoryProvider = class extends MemoryProvider {
18756
+ ensureParentDir(pathname) {
18757
+ const parent = path.posix.dirname(path.posix.normalize(pathname));
18758
+ if (!parent || parent === "/" || parent === ".") return;
18759
+ this.mkdirSync(parent, { recursive: true });
18760
+ }
18761
+ async mkdir(pathname, options) {
18762
+ this.ensureParentDir(pathname);
18763
+ return super.mkdir(pathname, options);
18764
+ }
18765
+ mkdirSync(pathname, options) {
18766
+ this.ensureParentDir(pathname);
18767
+ return super.mkdirSync(pathname, options);
18768
+ }
18769
+ async open(pathname, flags, mode) {
18770
+ if (isWriteFlag(flags)) this.ensureParentDir(pathname);
18771
+ return super.open(pathname, flags, isWriteFlag(flags) && isNodeModulesBinPath(pathname) ? (mode ?? 493) | 73 : mode);
18772
+ }
18773
+ openSync(pathname, flags, mode) {
18774
+ if (isWriteFlag(flags)) this.ensureParentDir(pathname);
18775
+ return super.openSync(pathname, flags, isWriteFlag(flags) && isNodeModulesBinPath(pathname) ? (mode ?? 493) | 73 : mode);
18776
+ }
18777
+ };
18747
18778
  /**
18748
18779
  * Resolve the main worktree root (where .moltnet/ lives — it's untracked,
18749
18780
  * only exists in the main worktree, not in git worktrees).
@@ -18894,6 +18925,12 @@ async function resumeVm(config) {
18894
18925
  vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
18895
18926
  const vfsConfig = config.sandboxConfig?.vfs;
18896
18927
  let workspaceProvider = new RealFSProvider(config.mountPath);
18928
+ workspaceProvider = new ShadowProvider(workspaceProvider, {
18929
+ shouldShadow: ({ path: shadowPath }) => shouldShadowNodeModulesPath(shadowPath),
18930
+ denySymlinkBypass: false,
18931
+ tmpfs: new AutoParentMemoryProvider(),
18932
+ writeMode: "tmpfs"
18933
+ });
18897
18934
  if (vfsConfig?.shadow?.length) {
18898
18935
  const predicate = createShadowPathPredicate(vfsConfig.shadow);
18899
18936
  workspaceProvider = new ShadowProvider(workspaceProvider, {
@@ -19632,17 +19669,17 @@ function createPiModelOptionsExtension(options) {
19632
19669
  };
19633
19670
  }
19634
19671
  function applyPiModelOptions(payload, options) {
19635
- if (!isRecord(payload)) return void 0;
19672
+ if (!isRecord$1(payload)) return void 0;
19636
19673
  if (!hasPiModelOptions(options)) return void 0;
19637
19674
  if (isGooglePayload(payload)) {
19638
- const config = isRecord(payload.config) ? payload.config : {};
19675
+ const config = isRecord$1(payload.config) ? payload.config : {};
19639
19676
  return {
19640
19677
  ...payload,
19641
19678
  config: applyConfigOptions(config, options)
19642
19679
  };
19643
19680
  }
19644
19681
  if (isBedrockPayload(payload)) {
19645
- const inferenceConfig = isRecord(payload.inferenceConfig) ? payload.inferenceConfig : {};
19682
+ const inferenceConfig = isRecord$1(payload.inferenceConfig) ? payload.inferenceConfig : {};
19646
19683
  return {
19647
19684
  ...payload,
19648
19685
  inferenceConfig: applyBedrockOptions(inferenceConfig, options)
@@ -19695,11 +19732,11 @@ function isAnthropicPayload(payload) {
19695
19732
  return "anthropic_version" in payload;
19696
19733
  }
19697
19734
  function hasActiveThinking(value) {
19698
- if (!isRecord(value)) return false;
19735
+ if (!isRecord$1(value)) return false;
19699
19736
  const type = value.type;
19700
19737
  return type !== "disabled" && type !== "off" && type !== false;
19701
19738
  }
19702
- function isRecord(value) {
19739
+ function isRecord$1(value) {
19703
19740
  return typeof value === "object" && value !== null && !Array.isArray(value);
19704
19741
  }
19705
19742
  //#endregion
@@ -19882,7 +19919,7 @@ function getSubmitOutputContract(taskType) {
19882
19919
  return {
19883
19920
  toolName: submitOutputToolName(taskType),
19884
19921
  taskType,
19885
- description: `Submit the structured output for this ${taskType} task. Call exactly once when done. The arguments below ARE the output payload — pass each top-level field of the task type's output schema directly. The runtime validates the args against the schema; mismatches return a tool error you can recover from in the same session. On a valid call the runtime captures the payload and ends the session — you do not need to repeat the JSON in your final assistant message.`,
19922
+ description: `Submit the structured output for this ${taskType} task. Call exactly once when done. The arguments below ARE the output payload — pass each top-level field of the task type's output schema directly. The runtime validates the args against the schema; mismatches return a tool error you can recover from in the same session. On a valid call the runtime captures the payload for attempt completion — you do not need to repeat the JSON in your final assistant message.`,
19886
19923
  parametersSchema: schema
19887
19924
  };
19888
19925
  }
@@ -19933,7 +19970,7 @@ function buildFinalOutputBlock(opts) {
19933
19970
  `output matching \`${outputSchemaName}\`.`,
19934
19971
  "",
19935
19972
  `Call \`${submitTool}\` exactly once with the payload.`,
19936
- `The runtime captures the validated arguments and ends the session.`,
19973
+ `The runtime captures the validated arguments for attempt completion.`,
19937
19974
  `Do NOT emit the output as plain assistant text. Do NOT rely on a`,
19938
19975
  `JSON-in-message fallback. If you do not call \`${submitTool}\`, the`,
19939
19976
  `attempt is recorded as failing the promised submit-output criterion`,
@@ -20672,7 +20709,7 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
20672
20709
  ` "targetTaskId": "${input.targetTaskId}",`,
20673
20710
  ` "targetAttemptN": ${input.targetAttemptN},`,
20674
20711
  " \"variantLabel\": \"<from producer input>\",",
20675
- " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]? } ],",
20712
+ " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]?, \"evidence\": { \"text\": \"...\" } } ],",
20676
20713
  " \"composite\": <Σ(weight × score), 0..1>,",
20677
20714
  " \"verdict\": \"<1-3 sentences>\",",
20678
20715
  " \"judgeModel\": \"<id>\", // optional",
@@ -24919,8 +24956,172 @@ async function resolvePriorContext(agent, continueFrom) {
24919
24956
  };
24920
24957
  }
24921
24958
  //#endregion
24959
+ //#region src/runtime/retry-triage.ts
24960
+ var MAX_TRIAGE_JSON_CHARS = 12e3;
24961
+ var MAX_TRIAGE_FIELD_CHARS = 2e3;
24962
+ var REDACTED = "[redacted]";
24963
+ var SECRET_KEY_PATTERN = /(?:api[_-]?key|token|secret|password|passwd|credential|authorization|private[_-]?key|access[_-]?token|refresh[_-]?token)/i;
24964
+ function createPiRetryTriage(options) {
24965
+ return async (input) => {
24966
+ const cwd = options.cwd ?? process.cwd();
24967
+ const capture = createRetryTriageTool();
24968
+ const resourceLoader = new DefaultResourceLoader({
24969
+ cwd,
24970
+ agentDir: options.piAgentDir,
24971
+ appendSystemPrompt: [TRIAGE_SYSTEM_PROMPT],
24972
+ skillsOverride: () => ({
24973
+ skills: [],
24974
+ diagnostics: []
24975
+ })
24976
+ });
24977
+ await resourceLoader.reload();
24978
+ const sessionManager = SessionManager.inMemory(cwd);
24979
+ const created = await createAgentSession({
24980
+ agentDir: options.piAgentDir,
24981
+ cwd,
24982
+ model: options.model,
24983
+ thinkingLevel: options.thinkingLevel ?? void 0,
24984
+ customTools: [capture.tool],
24985
+ sessionManager,
24986
+ resourceLoader
24987
+ });
24988
+ await withTimeout(created.session.prompt(buildTriagePrompt(input)), options.timeoutMs ?? 3e4, () => created.session.abort());
24989
+ const result = capture.getCaptured();
24990
+ if (!result) throw new Error("Retry triage did not submit a decision");
24991
+ return normalizeRetryTriageResult(result);
24992
+ };
24993
+ }
24994
+ function createRetryTriageTool() {
24995
+ let captured = null;
24996
+ return {
24997
+ tool: defineTool({
24998
+ name: "submit_retry_triage",
24999
+ label: "Submit retry triage",
25000
+ description: "Submit the retry decision for a failed MoltNet task attempt.",
25001
+ parameters: {
25002
+ type: "object",
25003
+ additionalProperties: false,
25004
+ required: [
25005
+ "decision",
25006
+ "confidence",
25007
+ "reason"
25008
+ ],
25009
+ properties: {
25010
+ decision: {
25011
+ type: "string",
25012
+ enum: ["retry", "do_not_retry"]
25013
+ },
25014
+ confidence: {
25015
+ type: "string",
25016
+ enum: [
25017
+ "low",
25018
+ "medium",
25019
+ "high"
25020
+ ]
25021
+ },
25022
+ reason: {
25023
+ type: "string",
25024
+ minLength: 1
25025
+ }
25026
+ }
25027
+ },
25028
+ execute(_id, params) {
25029
+ captured = normalizeRetryTriageResult(params);
25030
+ return Promise.resolve({
25031
+ content: [{
25032
+ type: "text",
25033
+ text: "Retry triage captured."
25034
+ }],
25035
+ details: captured,
25036
+ terminate: true
25037
+ });
25038
+ }
25039
+ }),
25040
+ getCaptured: () => captured
25041
+ };
25042
+ }
25043
+ function normalizeRetryTriageResult(value) {
25044
+ const record = value && typeof value === "object" ? value : {};
25045
+ return {
25046
+ decision: record.decision === "retry" ? "retry" : "do_not_retry",
25047
+ confidence: record.confidence === "high" || record.confidence === "medium" ? record.confidence : "low",
25048
+ reason: typeof record.reason === "string" && record.reason.trim() ? record.reason.trim().slice(0, 500) : "retry triage did not provide a reason"
25049
+ };
25050
+ }
25051
+ function buildTriagePrompt(input) {
25052
+ const payload = {
25053
+ task: {
25054
+ id: input.task.id,
25055
+ type: input.task.taskType,
25056
+ teamId: input.task.teamId,
25057
+ input: prepareTriagePayload(input.task.input)
25058
+ },
25059
+ attempt: {
25060
+ attemptN: input.attemptN,
25061
+ maxAttempts: input.maxAttempts ?? null,
25062
+ remainingAttempts: input.remainingAttempts ?? null
25063
+ },
25064
+ error: prepareTriagePayload(input.error),
25065
+ recentMessages: prepareTriagePayload((input.recentMessages ?? []).slice(-12))
25066
+ };
25067
+ return [
25068
+ "Classify whether this failed task attempt should be retried.",
25069
+ "",
25070
+ "Retry only when a fresh attempt can plausibly recover without changing the task input.",
25071
+ "Do not retry for policy, validation, credentials, cancellation, model/config, or task-contract failures.",
25072
+ "Submit-output validation errors should have been corrected inside the active Pi session; if one reaches retry triage, treat it as exhausted and choose do_not_retry.",
25073
+ "Use confidence=low when evidence is weak; low confidence must choose do_not_retry.",
25074
+ "Call submit_retry_triage exactly once.",
25075
+ "",
25076
+ truncateString(JSON.stringify(payload, null, 2), MAX_TRIAGE_JSON_CHARS)
25077
+ ].join("\n");
25078
+ }
25079
+ function prepareTriagePayload(value) {
25080
+ return redactAndTruncate(value, []);
25081
+ }
25082
+ function redactAndTruncate(value, path) {
25083
+ const currentKey = path[path.length - 1] ?? "";
25084
+ if (SECRET_KEY_PATTERN.test(currentKey)) return REDACTED;
25085
+ if (typeof value === "string") return truncateString(redactRetryTriageSecrets(value), MAX_TRIAGE_FIELD_CHARS);
25086
+ if (Array.isArray(value)) return value.map((item, index) => redactAndTruncate(item, [...path, String(index)]));
25087
+ if (value && typeof value === "object") {
25088
+ const entries = Object.entries(value).map(([key, child]) => [key, redactAndTruncate(child, [...path, key])]);
25089
+ return Object.fromEntries(entries);
25090
+ }
25091
+ return value;
25092
+ }
25093
+ function redactRetryTriageSecrets(value) {
25094
+ return value.replace(/((?:bearer|basic)\s+)[a-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`).replace(/\bgh[pousr]_[a-z0-9_]{20,}\b/gi, REDACTED).replace(/\bsk-[a-z0-9_-]{16,}\b/gi, REDACTED).replace(/\beyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\b/gi, REDACTED);
25095
+ }
25096
+ function truncateString(value, maxChars) {
25097
+ if (value.length <= maxChars) return value;
25098
+ return `${value.slice(0, maxChars)}...[truncated ${value.length - maxChars} chars]`;
25099
+ }
25100
+ var TRIAGE_SYSTEM_PROMPT = [
25101
+ "You are MoltNet retry triage.",
25102
+ "You classify one failed execution attempt, not the whole task.",
25103
+ "Return retry only for likely transient/runtime failures or clear evidence a new attempt can recover.",
25104
+ "The agent may have already tried local recovery; do not ask for more work."
25105
+ ].join("\n");
25106
+ async function withTimeout(promise, timeoutMs, onTimeout) {
25107
+ let timeout;
25108
+ const timeoutPromise = new Promise((_, reject) => {
25109
+ timeout = setTimeout(() => {
25110
+ Promise.resolve(onTimeout?.()).catch(() => {});
25111
+ reject(/* @__PURE__ */ new Error(`Retry triage timed out after ${timeoutMs}ms`));
25112
+ }, timeoutMs);
25113
+ });
25114
+ try {
25115
+ return await Promise.race([promise, timeoutPromise]);
25116
+ } finally {
25117
+ if (timeout) clearTimeout(timeout);
25118
+ }
25119
+ }
25120
+ //#endregion
24922
25121
  //#region src/runtime/subagent-tool.ts
24923
25122
  var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
25123
+ var DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES = 2;
25124
+ var RecoverableSubagentSubmitParameters = _Object_({}, { additionalProperties: Unknown() });
24924
25125
  /**
24925
25126
  * Parameters shape the parent LLM sees when calling the subagent tool.
24926
25127
  *
@@ -24964,21 +25165,44 @@ function createSubagentTool(args) {
24964
25165
  callCount += 1;
24965
25166
  const callIndex = callCount;
24966
25167
  let captured = null;
25168
+ let innerInvalidSubmitCount = 0;
25169
+ let innerValidationFailure = null;
25170
+ let innerValidationExhausted = false;
24967
25171
  const submitTool = defineTool({
24968
25172
  name: SUBAGENT_SUBMIT_TOOL_NAME,
24969
25173
  label: `Submit ${output_schema}`,
24970
25174
  description: `Submit your structured output for this subagent task. Call exactly once when done. Args MUST match the ${output_schema} contract; mismatches return a tool error you can recover from in the same session.`,
24971
- parameters: contract.parametersSchema,
25175
+ promptSnippet: `${SUBAGENT_SUBMIT_TOOL_NAME}: submit the final structured ${output_schema} payload.`,
25176
+ promptGuidelines: [`Call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` with the exact \`${output_schema}\` contract shape.`, "If the submit tool returns a validation error, fix every listed field and call the same tool again."],
25177
+ parameters: RecoverableSubagentSubmitParameters,
24972
25178
  async execute(_innerId, innerParams) {
24973
- if (!Check(contract.parametersSchema, innerParams)) return toolError(`submit_subagent_output: schema validation failed: ${[...Errors(contract.parametersSchema, innerParams)].slice(0, 3).map((e) => `${e.instancePath}: ${e.message}`).join("; ")}. Re-call with a corrected payload.`);
25179
+ if (innerValidationExhausted) return toolError("submit_subagent_output validation retry budget is already exhausted.", {
25180
+ captured: false,
25181
+ error: "output_validation_failed",
25182
+ invalidCallCount: innerInvalidSubmitCount,
25183
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25184
+ });
25185
+ if (!Check(contract.parametersSchema, innerParams)) {
25186
+ innerInvalidSubmitCount += 1;
25187
+ const maxInvalidCalls = DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES + 1;
25188
+ const exhausted = innerInvalidSubmitCount >= maxInvalidCalls;
25189
+ const errs = [...Errors(contract.parametersSchema, innerParams)].map((e) => `${e.instancePath}: ${e.message}`).join("; ");
25190
+ innerValidationFailure = `submit_subagent_output validation failed (${innerInvalidSubmitCount}/${maxInvalidCalls}): ${errs}. ` + (exhausted ? "Validation retry budget exhausted." : "Re-call with a corrected payload.");
25191
+ if (exhausted) innerValidationExhausted = true;
25192
+ return toolError(innerValidationFailure, {
25193
+ captured: false,
25194
+ error: "output_validation_failed",
25195
+ invalidCallCount: innerInvalidSubmitCount,
25196
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25197
+ });
25198
+ }
24974
25199
  captured = innerParams;
24975
25200
  return {
24976
25201
  content: [{
24977
25202
  type: "text",
24978
- text: "Output captured. Subagent session will terminate; no further action needed."
25203
+ text: "Output captured. No further action needed for subagent output reporting."
24979
25204
  }],
24980
- details: { captured: true },
24981
- terminate: true
25205
+ details: { captured: true }
24982
25206
  };
24983
25207
  }
24984
25208
  });
@@ -25042,7 +25266,16 @@ function createSubagentTool(args) {
25042
25266
  if (cancelListener) cancelListener();
25043
25267
  }
25044
25268
  if (abortReason !== null) return toolError(`subagent: ${abortReason === "subagent_timed_out" ? `subagent timed out after ${timeoutMs}ms` : "parent task was cancelled"}. The parent should fail this task or retry with a clearer scope.`);
25045
- if (captured === null) return toolError(`subagent: inner session ended without calling ${SUBAGENT_SUBMIT_TOOL_NAME}. The parent should retry with clearer instructions or fail the task.`);
25269
+ if (captured === null) {
25270
+ const exhaustedFailure = innerValidationFailure;
25271
+ if (innerValidationExhausted && exhaustedFailure) return toolError(`subagent: ${exhaustedFailure}`, {
25272
+ captured: false,
25273
+ error: "output_validation_failed",
25274
+ invalidCallCount: innerInvalidSubmitCount,
25275
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25276
+ });
25277
+ return toolError(`subagent: inner session ended without calling ${SUBAGENT_SUBMIT_TOOL_NAME}. The parent should retry with clearer instructions or fail the task.`);
25278
+ }
25046
25279
  return {
25047
25280
  content: [{
25048
25281
  type: "text",
@@ -25087,8 +25320,7 @@ function buildSubagentInstructor(args) {
25087
25320
  "Rules for this session:",
25088
25321
  "",
25089
25322
  `- You MUST call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` exactly once with a `,
25090
- " payload matching the contract above. Your session terminates on ",
25091
- " the valid call.",
25323
+ " payload matching the contract above as your final task action.",
25092
25324
  "- The parent's message above is your task. Do not invent additional ",
25093
25325
  " steps the parent did not request.",
25094
25326
  "- All MoltNet runtime invariants from the parent runtime instructor ",
@@ -25099,13 +25331,13 @@ function buildSubagentInstructor(args) {
25099
25331
  " delegation; do the work yourself."
25100
25332
  ].join("\n");
25101
25333
  }
25102
- function toolError(text) {
25334
+ function toolError(text, details = { captured: false }) {
25103
25335
  return {
25104
25336
  content: [{
25105
25337
  type: "text",
25106
25338
  text
25107
25339
  }],
25108
- details: { captured: false },
25340
+ details,
25109
25341
  isError: true
25110
25342
  };
25111
25343
  }
@@ -25252,25 +25484,112 @@ var UnknownTaskTypeForSubmitToolError = class extends Error {
25252
25484
  this.name = "UnknownTaskTypeForSubmitToolError";
25253
25485
  }
25254
25486
  };
25487
+ var DEFAULT_MAX_SUBMIT_VALIDATION_RETRIES = 2;
25488
+ var RecoverableSubmitToolParameters = _Object_({}, { additionalProperties: Unknown() });
25489
+ function formatValidationErrors(errors) {
25490
+ return errors.map((err) => `${err.field}: ${err.message}`).join("; ");
25491
+ }
25492
+ function submitOutputRepairHint(taskType, errors) {
25493
+ const fields = new Set(errors.map((err) => err.field));
25494
+ const hints = ["Tool args must be the output object directly, not wrapped in { output: ... }."];
25495
+ if (fields.has("output/artifacts")) hints.push("`artifacts` must be an array; omit it when there are no artifacts, use [], or use objects like { \"kind\": \"note\", \"title\": \"Result\", \"body\": \"...\" }.");
25496
+ if (fields.has("output/verification")) hints.push("`verification` must be an object with inputCid, results[], and passed; do not send it as text or an array.");
25497
+ if (taskType === "freeform" && (fields.has("output/artifacts") || fields.has("output/verification"))) hints.push("Minimal valid freeform retry: { \"summary\": \"completed\", \"artifacts\": [], \"verification\": { \"inputCid\": \"<task inputCid>\", \"results\": [{ \"id\": \"submit-output\", \"kind\": \"gate\", \"status\": \"pass\", \"detail\": \"submit_freeform_output accepted valid args\" }], \"passed\": true } }.");
25498
+ if (hints.length === 1) hints.push("Fix every listed field before re-calling this same tool.");
25499
+ return hints.join(" ");
25500
+ }
25501
+ function isRecord(value) {
25502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25503
+ }
25504
+ function onlySubmitOutputGate(input) {
25505
+ if (!isRecord(input) || !isRecord(input.successCriteria)) return false;
25506
+ const criteria = input.successCriteria;
25507
+ const gates = criteria.gates;
25508
+ if (!Array.isArray(gates) || gates.length !== 1) return false;
25509
+ const [gate] = gates;
25510
+ if (!isRecord(gate) || gate.id !== "submit-output") return false;
25511
+ const assertions = criteria.assertions;
25512
+ if (Array.isArray(assertions) && assertions.length > 0) return false;
25513
+ return criteria.rubric === void 0 && criteria.sideEffects === void 0 && criteria.minComposite === void 0;
25514
+ }
25515
+ function repairFreeformSubmitOutput(params, opts) {
25516
+ if (!isRecord(params) || !opts.inputCid || !onlySubmitOutputGate(opts.input)) return null;
25517
+ const repaired = { ...params };
25518
+ if ("artifacts" in repaired && !Array.isArray(repaired.artifacts)) if (isRecord(repaired.artifacts)) repaired.artifacts = [repaired.artifacts];
25519
+ else delete repaired.artifacts;
25520
+ if ("proposedTaskType" in repaired && !isRecord(repaired.proposedTaskType)) if (typeof repaired.proposedTaskType === "string" && repaired.proposedTaskType.length > 0) repaired.proposedTaskType = {
25521
+ name: repaired.proposedTaskType,
25522
+ rationale: "Suggested by the model during freeform execution."
25523
+ };
25524
+ else delete repaired.proposedTaskType;
25525
+ repaired.verification = {
25526
+ inputCid: opts.inputCid,
25527
+ results: [{
25528
+ id: SUBMIT_OUTPUT_GATE_ID,
25529
+ kind: "gate",
25530
+ status: "pass",
25531
+ detail: "submit_freeform_output accepted valid args"
25532
+ }],
25533
+ passed: true
25534
+ };
25535
+ return repaired;
25536
+ }
25537
+ function maybeRepairSubmitOutput(taskType, params, opts) {
25538
+ if (taskType !== "freeform") return null;
25539
+ const repaired = repairFreeformSubmitOutput(params, opts);
25540
+ if (!repaired) return null;
25541
+ return validateTaskOutput(taskType, repaired, opts.input).length === 0 ? repaired : null;
25542
+ }
25255
25543
  function createSubmitOutputTool(taskType, opts = {}) {
25256
25544
  const contract = getSubmitOutputContract(taskType);
25257
25545
  if (!contract) throw new UnknownTaskTypeForSubmitToolError(taskType);
25258
- const schema = contract.parametersSchema;
25546
+ const maxSubmitValidationRetries = opts.maxSubmitValidationRetries ?? DEFAULT_MAX_SUBMIT_VALIDATION_RETRIES;
25259
25547
  let captured = null;
25260
25548
  let callCount = 0;
25549
+ let invalidCallCount = 0;
25550
+ let lastValidationFailure = null;
25551
+ let exhaustedValidationFailure = null;
25261
25552
  return {
25262
25553
  tool: defineTool({
25263
25554
  name: contract.toolName,
25264
25555
  label: `Submit ${taskType} output`,
25265
25556
  description: contract.description,
25266
- parameters: schema,
25557
+ promptSnippet: `${contract.toolName}: submit the final structured ${taskType} output using the schema shown in the task prompt.`,
25558
+ promptGuidelines: [`Call \`${contract.toolName}\` with the exact ${taskType} output shape shown in the task prompt.`, "If the submit tool returns a validation error, fix every listed field and call the same tool again."],
25559
+ parameters: RecoverableSubmitToolParameters,
25267
25560
  async execute(_id, params) {
25268
- const errors = validateTaskOutput(taskType, params, opts.input);
25561
+ if (exhaustedValidationFailure) return {
25562
+ content: [{
25563
+ type: "text",
25564
+ text: "Submit-output validation retry budget is already exhausted; the attempt will fail."
25565
+ }],
25566
+ details: {
25567
+ captured: false,
25568
+ callCount,
25569
+ invalidCallCount,
25570
+ maxSubmitValidationRetries,
25571
+ error: "output_validation_failed"
25572
+ },
25573
+ isError: true
25574
+ };
25575
+ const candidateParams = maybeRepairSubmitOutput(taskType, params, opts) ?? params;
25576
+ const errors = validateTaskOutput(taskType, candidateParams, opts.input);
25269
25577
  if (errors.length > 0) {
25270
- const detailMsg = errors.slice(0, 3).map((err) => `${err.field}: ${err.message}`).join("; ");
25578
+ invalidCallCount += 1;
25579
+ const detailMsg = formatValidationErrors(errors);
25580
+ const maxInvalidCalls = maxSubmitValidationRetries + 1;
25581
+ const exhausted = invalidCallCount >= maxInvalidCalls;
25582
+ const message = `Output failed validation (${invalidCallCount}/${maxInvalidCalls}): ${detailMsg}. ${submitOutputRepairHint(taskType, errors)} ` + (exhausted ? "Submit-output validation retry budget exhausted; the attempt will fail." : "Re-call this tool with a corrected output.");
25583
+ lastValidationFailure = {
25584
+ code: "output_validation_failed",
25585
+ message
25586
+ };
25587
+ if (exhausted) exhaustedValidationFailure = lastValidationFailure;
25271
25588
  const details = {
25272
25589
  captured: false,
25273
25590
  callCount,
25591
+ invalidCallCount,
25592
+ maxSubmitValidationRetries,
25274
25593
  error: "output_validation_failed"
25275
25594
  };
25276
25595
  recordTaskOutputParseResult({
@@ -25281,13 +25600,13 @@ function createSubmitOutputTool(taskType, opts = {}) {
25281
25600
  return {
25282
25601
  content: [{
25283
25602
  type: "text",
25284
- text: `Output failed validation: ${detailMsg}. Re-call this tool with a corrected output.`
25603
+ text: message
25285
25604
  }],
25286
25605
  details,
25287
25606
  isError: true
25288
25607
  };
25289
25608
  }
25290
- captured = params;
25609
+ captured = candidateParams;
25291
25610
  callCount += 1;
25292
25611
  return {
25293
25612
  content: [{
@@ -25298,13 +25617,15 @@ function createSubmitOutputTool(taskType, opts = {}) {
25298
25617
  captured: true,
25299
25618
  callCount,
25300
25619
  error: null
25301
- },
25302
- terminate: true
25620
+ }
25303
25621
  };
25304
25622
  }
25305
25623
  }),
25306
25624
  getCaptured: () => captured,
25307
- getCallCount: () => callCount
25625
+ getCallCount: () => callCount,
25626
+ getInvalidCallCount: () => invalidCallCount,
25627
+ getLastValidationFailure: () => lastValidationFailure,
25628
+ getExhaustedValidationFailure: () => exhaustedValidationFailure
25308
25629
  };
25309
25630
  }
25310
25631
  /**
@@ -25873,7 +26194,9 @@ async function executePiTask(claimedTask, reporter, opts) {
25873
26194
  });
25874
26195
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
25875
26196
  model: opts.model,
25876
- input: task.input
26197
+ input: task.input,
26198
+ inputCid: task.inputCid,
26199
+ maxSubmitValidationRetries: opts.maxSubmitValidationRetries
25877
26200
  });
25878
26201
  const submitTools = submitToolDefs;
25879
26202
  try {
@@ -26060,19 +26383,28 @@ async function executePiTask(claimedTask, reporter, opts) {
26060
26383
  }
26061
26384
  });
26062
26385
  let runError = null;
26063
- try {
26064
- await session.prompt(taskPrompt);
26065
- } catch (err) {
26066
- const message = err instanceof Error ? err.message : String(err);
26067
- runError = {
26068
- code: "session_prompt_failed",
26069
- message
26070
- };
26071
- await emit("error", {
26386
+ runError = (await promptWithProviderErrorRetries({
26387
+ session,
26388
+ initialPrompt: taskPrompt,
26389
+ cancelSignal: reporter.cancelSignal,
26390
+ isCapAborted: () => capAbort !== null,
26391
+ getProviderErrorState: () => ({
26392
+ llmAbort,
26393
+ llmErrorMessage
26394
+ }),
26395
+ maxRetries: opts.maxProviderErrorRetries ?? 2,
26396
+ baseDelayMs: opts.providerErrorRetryBaseDelayMs ?? 2e3,
26397
+ maxDelayMs: opts.providerErrorRetryMaxDelayMs ?? 3e4,
26398
+ retryPrompt: opts.providerErrorRetryPrompt ?? "Go on",
26399
+ onRetry: async (event) => {
26400
+ await emit("info", event);
26401
+ await notifyProviderErrorRetryUi(opts.providerErrorRetryUi, event);
26402
+ },
26403
+ onPromptError: (message) => emit("error", {
26072
26404
  message,
26073
26405
  phase: "session_prompt"
26074
- });
26075
- }
26406
+ })
26407
+ })).runError;
26076
26408
  if (subagentHandle && subagentHandle.getCallCount() > 0) await emit("info", {
26077
26409
  event: "subagent_summary",
26078
26410
  callCount: subagentHandle.getCallCount()
@@ -26111,7 +26443,7 @@ async function executePiTask(claimedTask, reporter, opts) {
26111
26443
  });
26112
26444
  }
26113
26445
  else if (submitToolHandle) {
26114
- parseError = {
26446
+ parseError = submitToolHandle.getExhaustedValidationFailure() ?? {
26115
26447
  code: "submit_output_missing",
26116
26448
  message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
26117
26449
  };
@@ -26303,6 +26635,110 @@ function shouldEmitToolCallError(event) {
26303
26635
  if (event.toolName === "bash") return false;
26304
26636
  return true;
26305
26637
  }
26638
+ var PROVIDER_ERROR_NON_RETRYABLE_PATTERNS = [
26639
+ /\b401\b/i,
26640
+ /\b403\b/i,
26641
+ /\bunauthori[sz]ed\b/i,
26642
+ /\bforbidden\b/i,
26643
+ /\binvalid (?:api )?key\b/i,
26644
+ /\bmissing credentials?\b/i,
26645
+ /\binsufficient[_\s-]?quota\b/i,
26646
+ /\bbilling\b/i,
26647
+ /\bmodel .*not (?:found|registered|available)\b/i,
26648
+ /\bunknown model\b/i
26649
+ ];
26650
+ var PROVIDER_ERROR_RETRYABLE_PATTERNS = [
26651
+ /\b429\b/i,
26652
+ /\b5(?:02|03|04)\b/i,
26653
+ /\btimeout\b/i,
26654
+ /\btimed out\b/i,
26655
+ /\brate limit/i,
26656
+ /\btemporar(?:y|ily)\b/i,
26657
+ /\bunavailable\b/i,
26658
+ /\boverloaded\b/i,
26659
+ /\bECONNRESET\b/i,
26660
+ /\bECONNREFUSED\b/i,
26661
+ /\bETIMEDOUT\b/i,
26662
+ /\bENOTFOUND\b/i,
26663
+ /\bEAI_AGAIN\b/i,
26664
+ /\bDNS\b/i
26665
+ ];
26666
+ function shouldRetryProviderErrorMessage(message) {
26667
+ if (!message || !message.trim()) return true;
26668
+ if (PROVIDER_ERROR_NON_RETRYABLE_PATTERNS.some((pattern) => pattern.test(message))) return false;
26669
+ if (PROVIDER_ERROR_RETRYABLE_PATTERNS.some((pattern) => pattern.test(message))) return true;
26670
+ return true;
26671
+ }
26672
+ function computeProviderErrorRetryDelay(attempt, baseDelayMs, maxDelayMs) {
26673
+ return Math.min(Math.max(0, baseDelayMs) * 2 ** (attempt - 1), Math.max(0, maxDelayMs));
26674
+ }
26675
+ function formatProviderErrorRetryStatus(event) {
26676
+ const seconds = Math.ceil(event.delayMs / 1e3);
26677
+ return `Provider retry ${event.retry}/${event.maxRetries} in ${seconds}s`;
26678
+ }
26679
+ function formatProviderErrorRetryNotification(event) {
26680
+ return `Provider error; retrying same Pi session (${event.retry}/${event.maxRetries}).`;
26681
+ }
26682
+ async function notifyProviderErrorRetryUi(ui, event) {
26683
+ if (!ui || ui.hasUI === false) return;
26684
+ await ui.setStatus?.("provider_retry", formatProviderErrorRetryStatus(event));
26685
+ await ui.notify?.(formatProviderErrorRetryNotification(event), "warning");
26686
+ }
26687
+ async function promptWithProviderErrorRetries(args) {
26688
+ let retryCount = 0;
26689
+ let promptText = args.initialPrompt;
26690
+ while (true) {
26691
+ try {
26692
+ await args.session.prompt(promptText);
26693
+ } catch (err) {
26694
+ const message = err instanceof Error ? err.message : String(err);
26695
+ await args.onPromptError?.(message);
26696
+ return {
26697
+ runError: {
26698
+ code: "session_prompt_failed",
26699
+ message
26700
+ },
26701
+ retryCount
26702
+ };
26703
+ }
26704
+ const { llmAbort, llmErrorMessage } = args.getProviderErrorState();
26705
+ if (!llmAbort || args.cancelSignal.aborted || args.isCapAborted?.() || retryCount >= args.maxRetries || !shouldRetryProviderErrorMessage(llmErrorMessage)) return {
26706
+ runError: null,
26707
+ retryCount
26708
+ };
26709
+ retryCount += 1;
26710
+ const delayMs = computeProviderErrorRetryDelay(retryCount, args.baseDelayMs, args.maxDelayMs);
26711
+ await args.onRetry?.({
26712
+ event: "provider_error_retry",
26713
+ retry: retryCount,
26714
+ maxRetries: args.maxRetries,
26715
+ delayMs,
26716
+ reason: sanitizeProviderErrorRetryReason(llmErrorMessage)
26717
+ });
26718
+ await sleepUnlessAborted(delayMs, args.cancelSignal);
26719
+ if (args.cancelSignal.aborted || args.isCapAborted?.()) return {
26720
+ runError: null,
26721
+ retryCount
26722
+ };
26723
+ promptText = args.retryPrompt;
26724
+ }
26725
+ }
26726
+ function sanitizeProviderErrorRetryReason(value) {
26727
+ return redactRetryTriageSecrets(value ?? "Pi turn ended with stopReason=error").slice(0, 500);
26728
+ }
26729
+ async function sleepUnlessAborted(delayMs, signal) {
26730
+ if (delayMs <= 0 || signal.aborted) return;
26731
+ await new Promise((resolve) => {
26732
+ const timeout = setTimeout(done, delayMs);
26733
+ const onAbort = () => done();
26734
+ function done() {
26735
+ clearTimeout(timeout);
26736
+ signal.removeEventListener("abort", onAbort);
26737
+ resolve();
26738
+ }
26739
+ signal.addEventListener("abort", onAbort, { once: true });
26740
+ });
26741
+ }
26306
26742
  /**
26307
26743
  * Detect pi's bash-timeout error wrapper in a `tool_execution_end`
26308
26744
  * result. The bash tool surfaces a timeout as a structured tool result
@@ -26364,167 +26800,6 @@ function describeToolErrorMessage(result) {
26364
26800
  }
26365
26801
  }
26366
26802
  //#endregion
26367
- //#region src/runtime/retry-triage.ts
26368
- var MAX_TRIAGE_JSON_CHARS = 12e3;
26369
- var MAX_TRIAGE_FIELD_CHARS = 2e3;
26370
- var REDACTED = "[redacted]";
26371
- var SECRET_KEY_PATTERN = /(?:api[_-]?key|token|secret|password|passwd|credential|authorization|private[_-]?key|access[_-]?token|refresh[_-]?token)/i;
26372
- function createPiRetryTriage(options) {
26373
- return async (input) => {
26374
- const cwd = options.cwd ?? process.cwd();
26375
- const capture = createRetryTriageTool();
26376
- const resourceLoader = new DefaultResourceLoader({
26377
- cwd,
26378
- agentDir: options.piAgentDir,
26379
- appendSystemPrompt: [TRIAGE_SYSTEM_PROMPT],
26380
- skillsOverride: () => ({
26381
- skills: [],
26382
- diagnostics: []
26383
- })
26384
- });
26385
- await resourceLoader.reload();
26386
- const sessionManager = SessionManager.inMemory(cwd);
26387
- const created = await createAgentSession({
26388
- agentDir: options.piAgentDir,
26389
- cwd,
26390
- model: options.model,
26391
- thinkingLevel: options.thinkingLevel ?? void 0,
26392
- customTools: [capture.tool],
26393
- sessionManager,
26394
- resourceLoader
26395
- });
26396
- await withTimeout(created.session.prompt(buildTriagePrompt(input)), options.timeoutMs ?? 3e4, () => created.session.abort());
26397
- const result = capture.getCaptured();
26398
- if (!result) throw new Error("Retry triage did not submit a decision");
26399
- return normalizeRetryTriageResult(result);
26400
- };
26401
- }
26402
- function createRetryTriageTool() {
26403
- let captured = null;
26404
- return {
26405
- tool: defineTool({
26406
- name: "submit_retry_triage",
26407
- label: "Submit retry triage",
26408
- description: "Submit the retry decision for a failed MoltNet task attempt.",
26409
- parameters: {
26410
- type: "object",
26411
- additionalProperties: false,
26412
- required: [
26413
- "decision",
26414
- "confidence",
26415
- "reason"
26416
- ],
26417
- properties: {
26418
- decision: {
26419
- type: "string",
26420
- enum: ["retry", "do_not_retry"]
26421
- },
26422
- confidence: {
26423
- type: "string",
26424
- enum: [
26425
- "low",
26426
- "medium",
26427
- "high"
26428
- ]
26429
- },
26430
- reason: {
26431
- type: "string",
26432
- minLength: 1
26433
- }
26434
- }
26435
- },
26436
- execute(_id, params) {
26437
- captured = normalizeRetryTriageResult(params);
26438
- return Promise.resolve({
26439
- content: [{
26440
- type: "text",
26441
- text: "Retry triage captured."
26442
- }],
26443
- details: captured,
26444
- terminate: true
26445
- });
26446
- }
26447
- }),
26448
- getCaptured: () => captured
26449
- };
26450
- }
26451
- function normalizeRetryTriageResult(value) {
26452
- const record = value && typeof value === "object" ? value : {};
26453
- return {
26454
- decision: record.decision === "retry" ? "retry" : "do_not_retry",
26455
- confidence: record.confidence === "high" || record.confidence === "medium" ? record.confidence : "low",
26456
- reason: typeof record.reason === "string" && record.reason.trim() ? record.reason.trim().slice(0, 500) : "retry triage did not provide a reason"
26457
- };
26458
- }
26459
- function buildTriagePrompt(input) {
26460
- const payload = {
26461
- task: {
26462
- id: input.task.id,
26463
- type: input.task.taskType,
26464
- teamId: input.task.teamId,
26465
- input: prepareTriagePayload(input.task.input)
26466
- },
26467
- attempt: {
26468
- attemptN: input.attemptN,
26469
- maxAttempts: input.maxAttempts ?? null,
26470
- remainingAttempts: input.remainingAttempts ?? null
26471
- },
26472
- error: prepareTriagePayload(input.error),
26473
- recentMessages: prepareTriagePayload((input.recentMessages ?? []).slice(-12))
26474
- };
26475
- return [
26476
- "Classify whether this failed task attempt should be retried.",
26477
- "",
26478
- "Retry only when a fresh attempt can plausibly recover without changing the task input.",
26479
- "Do not retry for policy, validation, credentials, cancellation, model/config, or task-contract failures.",
26480
- "Use confidence=low when evidence is weak; low confidence must choose do_not_retry.",
26481
- "Call submit_retry_triage exactly once.",
26482
- "",
26483
- truncateString(JSON.stringify(payload, null, 2), MAX_TRIAGE_JSON_CHARS)
26484
- ].join("\n");
26485
- }
26486
- function prepareTriagePayload(value) {
26487
- return redactAndTruncate(value, []);
26488
- }
26489
- function redactAndTruncate(value, path) {
26490
- const currentKey = path[path.length - 1] ?? "";
26491
- if (SECRET_KEY_PATTERN.test(currentKey)) return REDACTED;
26492
- if (typeof value === "string") return truncateString(redactRetryTriageSecrets(value), MAX_TRIAGE_FIELD_CHARS);
26493
- if (Array.isArray(value)) return value.map((item, index) => redactAndTruncate(item, [...path, String(index)]));
26494
- if (value && typeof value === "object") {
26495
- const entries = Object.entries(value).map(([key, child]) => [key, redactAndTruncate(child, [...path, key])]);
26496
- return Object.fromEntries(entries);
26497
- }
26498
- return value;
26499
- }
26500
- function redactRetryTriageSecrets(value) {
26501
- return value.replace(/((?:bearer|basic)\s+)[a-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`).replace(/\bgh[pousr]_[a-z0-9_]{20,}\b/gi, REDACTED).replace(/\bsk-[a-z0-9_-]{16,}\b/gi, REDACTED).replace(/\beyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\b/gi, REDACTED);
26502
- }
26503
- function truncateString(value, maxChars) {
26504
- if (value.length <= maxChars) return value;
26505
- return `${value.slice(0, maxChars)}...[truncated ${value.length - maxChars} chars]`;
26506
- }
26507
- var TRIAGE_SYSTEM_PROMPT = [
26508
- "You are MoltNet retry triage.",
26509
- "You classify one failed execution attempt, not the whole task.",
26510
- "Return retry only for likely transient/runtime failures or clear evidence a new attempt can recover.",
26511
- "The agent may have already tried local recovery; do not ask for more work."
26512
- ].join("\n");
26513
- async function withTimeout(promise, timeoutMs, onTimeout) {
26514
- let timeout;
26515
- const timeoutPromise = new Promise((_, reject) => {
26516
- timeout = setTimeout(() => {
26517
- Promise.resolve(onTimeout?.()).catch(() => {});
26518
- reject(/* @__PURE__ */ new Error(`Retry triage timed out after ${timeoutMs}ms`));
26519
- }, timeoutMs);
26520
- });
26521
- try {
26522
- return await Promise.race([promise, timeoutPromise]);
26523
- } finally {
26524
- if (timeout) clearTimeout(timeout);
26525
- }
26526
- }
26527
- //#endregion
26528
26803
  //#region src/index.ts
26529
26804
  /**
26530
26805
  * @themoltnet/pi-extension — MoltNet pi extension
@@ -26535,6 +26810,19 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
26535
26810
  * See README.md for credential injection flow, tool split, sandbox.json
26536
26811
  * reference, and headless/programmatic usage.
26537
26812
  */
26813
+ function createPiProviderErrorRetryUi(ctx) {
26814
+ const hasUI = Boolean(ctx.hasUI);
26815
+ if (!hasUI) return void 0;
26816
+ return {
26817
+ hasUI,
26818
+ setStatus: (key, message) => {
26819
+ ctx.ui.setStatus(key, ctx.ui.theme.fg("muted", message));
26820
+ },
26821
+ notify: (message, level) => {
26822
+ ctx.ui.notify?.(message, level);
26823
+ }
26824
+ };
26825
+ }
26538
26826
  function moltnetExtension(pi) {
26539
26827
  pi.registerFlag("agent", {
26540
26828
  description: "MoltNet agent name (required — pass --agent <name>)",
@@ -26825,4 +27113,4 @@ function moltnetExtension(pi) {
26825
27113
  registerMoltnetReflectCommand(pi, state);
26826
27114
  }
26827
27115
  //#endregion
26828
- export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
27116
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.31.0",
3
+ "version": "0.31.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,7 +36,7 @@
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.33.1",
39
+ "@themoltnet/agent-runtime": "0.33.2",
40
40
  "@themoltnet/sdk": "0.117.0"
41
41
  },
42
42
  "peerDependencies": {