@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 +17 -10
- package/dist/index.d.ts +51 -0
- package/dist/index.js +494 -206
- package/package.json +2 -2
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:
|
|
220
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
- install target on guest tmpfs via `resumeCommands`
|
|
298
|
+
Run the real VM integration check locally with:
|
|
293
299
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
--
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
25603
|
+
text: message
|
|
25285
25604
|
}],
|
|
25286
25605
|
details,
|
|
25287
25606
|
isError: true
|
|
25288
25607
|
};
|
|
25289
25608
|
}
|
|
25290
|
-
captured =
|
|
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
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@themoltnet/agent-runtime": "0.33.2",
|
|
40
40
|
"@themoltnet/sdk": "0.117.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|