@themoltnet/pi-extension 0.30.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 +105 -0
- package/dist/index.js +521 -50
- package/package.json +3 -3
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,16 @@ 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
|
+
|
|
151
|
+
export declare function createPiRetryTriage(options: {
|
|
152
|
+
model: Model<Api>;
|
|
153
|
+
thinkingLevel?: PiRetryTriageThinkingLevel | null;
|
|
154
|
+
piAgentDir: string;
|
|
155
|
+
timeoutMs?: number;
|
|
156
|
+
cwd?: string;
|
|
157
|
+
}): PiRetryTriage;
|
|
158
|
+
|
|
148
159
|
/**
|
|
149
160
|
* Factory that builds a pi-specific `executeTask` function suitable for
|
|
150
161
|
* injection into `AgentRuntime`. The returned function caches the resolved
|
|
@@ -365,6 +376,34 @@ export declare interface ExecutePiTaskOptions {
|
|
|
365
376
|
* Default `3`. Set to `0` to disable. Closes part of #1094.
|
|
366
377
|
*/
|
|
367
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;
|
|
368
407
|
/**
|
|
369
408
|
* Skip per-call UI approval for matching `moltnet_host_exec` commands.
|
|
370
409
|
* Keep false/undefined for interactive consumers. `true` skips every dialog
|
|
@@ -524,6 +563,8 @@ declare interface MoltNetToolsConfig {
|
|
|
524
563
|
getTaskContext?(): MoltNetTaskContext | null;
|
|
525
564
|
}
|
|
526
565
|
|
|
566
|
+
export declare function normalizeRetryTriageResult(value: unknown): PiRetryTriageResult;
|
|
567
|
+
|
|
527
568
|
export declare interface PiOtelOptions {
|
|
528
569
|
/** Agent name for `gen_ai.agent.name` on the root span. */
|
|
529
570
|
agentName?: string;
|
|
@@ -535,6 +576,38 @@ export declare interface PiOtelOptions {
|
|
|
535
576
|
spanAttributes?: Record<string, string | number | boolean>;
|
|
536
577
|
}
|
|
537
578
|
|
|
579
|
+
export declare type PiRetryTriage = (input: PiRetryTriageInput) => Promise<PiRetryTriageResult>;
|
|
580
|
+
|
|
581
|
+
export declare type PiRetryTriageConfidence = RetryTriageConfidence;
|
|
582
|
+
|
|
583
|
+
export declare type PiRetryTriageDecision = RetryTriageDecision;
|
|
584
|
+
|
|
585
|
+
export declare interface PiRetryTriageInput {
|
|
586
|
+
task: {
|
|
587
|
+
id: string;
|
|
588
|
+
taskType: string;
|
|
589
|
+
teamId: string;
|
|
590
|
+
input: unknown;
|
|
591
|
+
};
|
|
592
|
+
attemptN: number;
|
|
593
|
+
maxAttempts?: number | null;
|
|
594
|
+
remainingAttempts?: number | null;
|
|
595
|
+
error: unknown;
|
|
596
|
+
recentMessages?: {
|
|
597
|
+
timestamp: string;
|
|
598
|
+
kind: string;
|
|
599
|
+
payload: unknown;
|
|
600
|
+
}[];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export declare interface PiRetryTriageResult {
|
|
604
|
+
decision: RetryTriageDecision;
|
|
605
|
+
confidence: RetryTriageConfidence;
|
|
606
|
+
reason: string;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export declare type PiRetryTriageThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
610
|
+
|
|
538
611
|
export declare interface PiSessionPersistencePlan {
|
|
539
612
|
sessionDir: string;
|
|
540
613
|
forkFromSessionPath?: string | null;
|
|
@@ -605,6 +678,28 @@ declare interface PiWorkspaceSeedPlan {
|
|
|
605
678
|
source: 'producer';
|
|
606
679
|
}
|
|
607
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
|
+
|
|
701
|
+
export declare function redactRetryTriageSecrets(value: string): string;
|
|
702
|
+
|
|
608
703
|
export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
|
|
609
704
|
|
|
610
705
|
export declare interface ResumeCommand {
|
|
@@ -635,6 +730,10 @@ declare interface ResumeCommandWhen {
|
|
|
635
730
|
*/
|
|
636
731
|
export declare function resumeVm(config: VmConfig): Promise<ManagedVm>;
|
|
637
732
|
|
|
733
|
+
export declare type RetryTriageConfidence = 'low' | 'medium' | 'high';
|
|
734
|
+
|
|
735
|
+
export declare type RetryTriageDecision = 'retry' | 'do_not_retry';
|
|
736
|
+
|
|
638
737
|
export declare interface SandboxConfig {
|
|
639
738
|
/** Snapshot build settings. */
|
|
640
739
|
snapshot?: {
|
|
@@ -853,6 +952,12 @@ declare const TaskOutput: Type.TObject<{
|
|
|
853
952
|
message: Type.TString;
|
|
854
953
|
stack: Type.TOptional<Type.TString>;
|
|
855
954
|
retryable: Type.TOptional<Type.TBoolean>;
|
|
955
|
+
retry: Type.TOptional<Type.TObject<{
|
|
956
|
+
source: Type.TUnion<[Type.TLiteral<"explicit">, Type.TLiteral<"deterministic">, Type.TLiteral<"attempts_exhausted">, Type.TLiteral<"triage">, Type.TLiteral<"triage_failed">]>;
|
|
957
|
+
decision: Type.TOptional<Type.TUnion<[Type.TLiteral<"retry">, Type.TLiteral<"do_not_retry">]>>;
|
|
958
|
+
confidence: Type.TOptional<Type.TUnion<[Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">]>>;
|
|
959
|
+
reason: Type.TOptional<Type.TString>;
|
|
960
|
+
}>>;
|
|
856
961
|
}>>;
|
|
857
962
|
contentSignature: Type.TOptional<Type.TString>;
|
|
858
963
|
}>;
|
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
|
|
@@ -1159,7 +1159,7 @@ var listDiaryPacks = (options) => (options.client ?? client).get({
|
|
|
1159
1159
|
...options
|
|
1160
1160
|
});
|
|
1161
1161
|
/**
|
|
1162
|
-
* Create and persist a custom context pack from an explicit entry selection.
|
|
1162
|
+
* Create and persist a custom context pack from an explicit entry selection. Returns 409 if any selected entry is flagged as a prompt-injection risk; the response lists the flagged entries. Set `force: true` to override and persist anyway.
|
|
1163
1163
|
*/
|
|
1164
1164
|
var createDiaryCustomPack = (options) => (options.client ?? client).post({
|
|
1165
1165
|
security: [
|
|
@@ -2159,7 +2159,7 @@ var completeTask = (options) => (options.client ?? client).post({
|
|
|
2159
2159
|
/**
|
|
2160
2160
|
* Mark an attempt as failed with error details.
|
|
2161
2161
|
*/
|
|
2162
|
-
var
|
|
2162
|
+
var failTaskAttempt = (options) => (options.client ?? client).post({
|
|
2163
2163
|
security: [
|
|
2164
2164
|
{
|
|
2165
2165
|
scheme: "bearer",
|
|
@@ -14019,6 +14019,27 @@ var TaskUsage = _Object_({
|
|
|
14019
14019
|
$id: "TaskUsage",
|
|
14020
14020
|
additionalProperties: false
|
|
14021
14021
|
});
|
|
14022
|
+
var TaskRetryDecision = Union([Literal("retry"), Literal("do_not_retry")]);
|
|
14023
|
+
var TaskRetryConfidence = Union([
|
|
14024
|
+
Literal("low"),
|
|
14025
|
+
Literal("medium"),
|
|
14026
|
+
Literal("high")
|
|
14027
|
+
]);
|
|
14028
|
+
var TaskRetryInfo = _Object_({
|
|
14029
|
+
source: Union([
|
|
14030
|
+
Literal("explicit"),
|
|
14031
|
+
Literal("deterministic"),
|
|
14032
|
+
Literal("attempts_exhausted"),
|
|
14033
|
+
Literal("triage"),
|
|
14034
|
+
Literal("triage_failed")
|
|
14035
|
+
]),
|
|
14036
|
+
decision: Optional(TaskRetryDecision),
|
|
14037
|
+
confidence: Optional(TaskRetryConfidence),
|
|
14038
|
+
reason: Optional(String$1())
|
|
14039
|
+
}, {
|
|
14040
|
+
$id: "TaskRetryInfo",
|
|
14041
|
+
additionalProperties: false
|
|
14042
|
+
});
|
|
14022
14043
|
/**
|
|
14023
14044
|
* Structured error returned from a failed attempt.
|
|
14024
14045
|
*/
|
|
@@ -14026,7 +14047,8 @@ var TaskError = _Object_({
|
|
|
14026
14047
|
code: String$1(),
|
|
14027
14048
|
message: String$1(),
|
|
14028
14049
|
stack: Optional(String$1()),
|
|
14029
|
-
retryable: Optional(Boolean$1())
|
|
14050
|
+
retryable: Optional(Boolean$1()),
|
|
14051
|
+
retry: Optional(TaskRetryInfo)
|
|
14030
14052
|
}, {
|
|
14031
14053
|
$id: "TaskError",
|
|
14032
14054
|
additionalProperties: false
|
|
@@ -14157,7 +14179,7 @@ _Object_({
|
|
|
14157
14179
|
* @param errors - Field-level errors from `@moltnet/tasks` validators.
|
|
14158
14180
|
* @returns One line per error, joined by newlines.
|
|
14159
14181
|
*/
|
|
14160
|
-
function formatValidationErrors(errors) {
|
|
14182
|
+
function formatValidationErrors$1(errors) {
|
|
14161
14183
|
return errors.map((e) => `${e.field}: ${e.message}`).join("\n");
|
|
14162
14184
|
}
|
|
14163
14185
|
/**
|
|
@@ -14170,7 +14192,7 @@ var TaskBuildError = class extends Error {
|
|
|
14170
14192
|
/** The field-level validation errors that caused the build to fail. */
|
|
14171
14193
|
errors;
|
|
14172
14194
|
constructor(errors) {
|
|
14173
|
-
super(`Task build failed:\n${formatValidationErrors(errors)}`);
|
|
14195
|
+
super(`Task build failed:\n${formatValidationErrors$1(errors)}`);
|
|
14174
14196
|
this.name = "TaskBuildError";
|
|
14175
14197
|
this.errors = errors;
|
|
14176
14198
|
}
|
|
@@ -14185,7 +14207,7 @@ var TaskResultError = class extends Error {
|
|
|
14185
14207
|
/** The field-level errors describing why the result could not be read. */
|
|
14186
14208
|
errors;
|
|
14187
14209
|
constructor(errors) {
|
|
14188
|
-
super(`Task result error:\n${formatValidationErrors(errors)}`);
|
|
14210
|
+
super(`Task result error:\n${formatValidationErrors$1(errors)}`);
|
|
14189
14211
|
this.name = "TaskResultError";
|
|
14190
14212
|
this.errors = errors;
|
|
14191
14213
|
}
|
|
@@ -14992,8 +15014,8 @@ function createTasksNamespace(context) {
|
|
|
14992
15014
|
body
|
|
14993
15015
|
}));
|
|
14994
15016
|
},
|
|
14995
|
-
async
|
|
14996
|
-
return unwrapResult(await
|
|
15017
|
+
async failAttempt(id, n, body) {
|
|
15018
|
+
return unwrapResult(await failTaskAttempt({
|
|
14997
15019
|
client,
|
|
14998
15020
|
auth,
|
|
14999
15021
|
path: {
|
|
@@ -18722,6 +18744,37 @@ function shouldRunResumeCommand(entry, ctx) {
|
|
|
18722
18744
|
if (workspaceModes && !workspaceModes.includes(ctx.workspaceMode)) return false;
|
|
18723
18745
|
return true;
|
|
18724
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
|
+
};
|
|
18725
18778
|
/**
|
|
18726
18779
|
* Resolve the main worktree root (where .moltnet/ lives — it's untracked,
|
|
18727
18780
|
* only exists in the main worktree, not in git worktrees).
|
|
@@ -18872,6 +18925,12 @@ async function resumeVm(config) {
|
|
|
18872
18925
|
vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
|
|
18873
18926
|
const vfsConfig = config.sandboxConfig?.vfs;
|
|
18874
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
|
+
});
|
|
18875
18934
|
if (vfsConfig?.shadow?.length) {
|
|
18876
18935
|
const predicate = createShadowPathPredicate(vfsConfig.shadow);
|
|
18877
18936
|
workspaceProvider = new ShadowProvider(workspaceProvider, {
|
|
@@ -19610,17 +19669,17 @@ function createPiModelOptionsExtension(options) {
|
|
|
19610
19669
|
};
|
|
19611
19670
|
}
|
|
19612
19671
|
function applyPiModelOptions(payload, options) {
|
|
19613
|
-
if (!isRecord(payload)) return void 0;
|
|
19672
|
+
if (!isRecord$1(payload)) return void 0;
|
|
19614
19673
|
if (!hasPiModelOptions(options)) return void 0;
|
|
19615
19674
|
if (isGooglePayload(payload)) {
|
|
19616
|
-
const config = isRecord(payload.config) ? payload.config : {};
|
|
19675
|
+
const config = isRecord$1(payload.config) ? payload.config : {};
|
|
19617
19676
|
return {
|
|
19618
19677
|
...payload,
|
|
19619
19678
|
config: applyConfigOptions(config, options)
|
|
19620
19679
|
};
|
|
19621
19680
|
}
|
|
19622
19681
|
if (isBedrockPayload(payload)) {
|
|
19623
|
-
const inferenceConfig = isRecord(payload.inferenceConfig) ? payload.inferenceConfig : {};
|
|
19682
|
+
const inferenceConfig = isRecord$1(payload.inferenceConfig) ? payload.inferenceConfig : {};
|
|
19624
19683
|
return {
|
|
19625
19684
|
...payload,
|
|
19626
19685
|
inferenceConfig: applyBedrockOptions(inferenceConfig, options)
|
|
@@ -19673,11 +19732,11 @@ function isAnthropicPayload(payload) {
|
|
|
19673
19732
|
return "anthropic_version" in payload;
|
|
19674
19733
|
}
|
|
19675
19734
|
function hasActiveThinking(value) {
|
|
19676
|
-
if (!isRecord(value)) return false;
|
|
19735
|
+
if (!isRecord$1(value)) return false;
|
|
19677
19736
|
const type = value.type;
|
|
19678
19737
|
return type !== "disabled" && type !== "off" && type !== false;
|
|
19679
19738
|
}
|
|
19680
|
-
function isRecord(value) {
|
|
19739
|
+
function isRecord$1(value) {
|
|
19681
19740
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19682
19741
|
}
|
|
19683
19742
|
//#endregion
|
|
@@ -19860,7 +19919,7 @@ function getSubmitOutputContract(taskType) {
|
|
|
19860
19919
|
return {
|
|
19861
19920
|
toolName: submitOutputToolName(taskType),
|
|
19862
19921
|
taskType,
|
|
19863
|
-
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.`,
|
|
19864
19923
|
parametersSchema: schema
|
|
19865
19924
|
};
|
|
19866
19925
|
}
|
|
@@ -19911,7 +19970,7 @@ function buildFinalOutputBlock(opts) {
|
|
|
19911
19970
|
`output matching \`${outputSchemaName}\`.`,
|
|
19912
19971
|
"",
|
|
19913
19972
|
`Call \`${submitTool}\` exactly once with the payload.`,
|
|
19914
|
-
`The runtime captures the validated arguments
|
|
19973
|
+
`The runtime captures the validated arguments for attempt completion.`,
|
|
19915
19974
|
`Do NOT emit the output as plain assistant text. Do NOT rely on a`,
|
|
19916
19975
|
`JSON-in-message fallback. If you do not call \`${submitTool}\`, the`,
|
|
19917
19976
|
`attempt is recorded as failing the promised submit-output criterion`,
|
|
@@ -20650,7 +20709,7 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
|
|
|
20650
20709
|
` "targetTaskId": "${input.targetTaskId}",`,
|
|
20651
20710
|
` "targetAttemptN": ${input.targetAttemptN},`,
|
|
20652
20711
|
" \"variantLabel\": \"<from producer input>\",",
|
|
20653
|
-
" \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]
|
|
20712
|
+
" \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]?, \"evidence\": { \"text\": \"...\" } } ],",
|
|
20654
20713
|
" \"composite\": <Σ(weight × score), 0..1>,",
|
|
20655
20714
|
" \"verdict\": \"<1-3 sentences>\",",
|
|
20656
20715
|
" \"judgeModel\": \"<id>\", // optional",
|
|
@@ -24897,8 +24956,172 @@ async function resolvePriorContext(agent, continueFrom) {
|
|
|
24897
24956
|
};
|
|
24898
24957
|
}
|
|
24899
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
|
|
24900
25121
|
//#region src/runtime/subagent-tool.ts
|
|
24901
25122
|
var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
|
|
25123
|
+
var DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES = 2;
|
|
25124
|
+
var RecoverableSubagentSubmitParameters = _Object_({}, { additionalProperties: Unknown() });
|
|
24902
25125
|
/**
|
|
24903
25126
|
* Parameters shape the parent LLM sees when calling the subagent tool.
|
|
24904
25127
|
*
|
|
@@ -24942,21 +25165,44 @@ function createSubagentTool(args) {
|
|
|
24942
25165
|
callCount += 1;
|
|
24943
25166
|
const callIndex = callCount;
|
|
24944
25167
|
let captured = null;
|
|
25168
|
+
let innerInvalidSubmitCount = 0;
|
|
25169
|
+
let innerValidationFailure = null;
|
|
25170
|
+
let innerValidationExhausted = false;
|
|
24945
25171
|
const submitTool = defineTool({
|
|
24946
25172
|
name: SUBAGENT_SUBMIT_TOOL_NAME,
|
|
24947
25173
|
label: `Submit ${output_schema}`,
|
|
24948
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.`,
|
|
24949
|
-
|
|
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,
|
|
24950
25178
|
async execute(_innerId, innerParams) {
|
|
24951
|
-
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
|
+
}
|
|
24952
25199
|
captured = innerParams;
|
|
24953
25200
|
return {
|
|
24954
25201
|
content: [{
|
|
24955
25202
|
type: "text",
|
|
24956
|
-
text: "Output captured.
|
|
25203
|
+
text: "Output captured. No further action needed for subagent output reporting."
|
|
24957
25204
|
}],
|
|
24958
|
-
details: { captured: true }
|
|
24959
|
-
terminate: true
|
|
25205
|
+
details: { captured: true }
|
|
24960
25206
|
};
|
|
24961
25207
|
}
|
|
24962
25208
|
});
|
|
@@ -25020,7 +25266,16 @@ function createSubagentTool(args) {
|
|
|
25020
25266
|
if (cancelListener) cancelListener();
|
|
25021
25267
|
}
|
|
25022
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.`);
|
|
25023
|
-
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
|
+
}
|
|
25024
25279
|
return {
|
|
25025
25280
|
content: [{
|
|
25026
25281
|
type: "text",
|
|
@@ -25065,8 +25320,7 @@ function buildSubagentInstructor(args) {
|
|
|
25065
25320
|
"Rules for this session:",
|
|
25066
25321
|
"",
|
|
25067
25322
|
`- You MUST call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` exactly once with a `,
|
|
25068
|
-
" payload matching the contract above
|
|
25069
|
-
" the valid call.",
|
|
25323
|
+
" payload matching the contract above as your final task action.",
|
|
25070
25324
|
"- The parent's message above is your task. Do not invent additional ",
|
|
25071
25325
|
" steps the parent did not request.",
|
|
25072
25326
|
"- All MoltNet runtime invariants from the parent runtime instructor ",
|
|
@@ -25077,13 +25331,13 @@ function buildSubagentInstructor(args) {
|
|
|
25077
25331
|
" delegation; do the work yourself."
|
|
25078
25332
|
].join("\n");
|
|
25079
25333
|
}
|
|
25080
|
-
function toolError(text) {
|
|
25334
|
+
function toolError(text, details = { captured: false }) {
|
|
25081
25335
|
return {
|
|
25082
25336
|
content: [{
|
|
25083
25337
|
type: "text",
|
|
25084
25338
|
text
|
|
25085
25339
|
}],
|
|
25086
|
-
details
|
|
25340
|
+
details,
|
|
25087
25341
|
isError: true
|
|
25088
25342
|
};
|
|
25089
25343
|
}
|
|
@@ -25230,25 +25484,112 @@ var UnknownTaskTypeForSubmitToolError = class extends Error {
|
|
|
25230
25484
|
this.name = "UnknownTaskTypeForSubmitToolError";
|
|
25231
25485
|
}
|
|
25232
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
|
+
}
|
|
25233
25543
|
function createSubmitOutputTool(taskType, opts = {}) {
|
|
25234
25544
|
const contract = getSubmitOutputContract(taskType);
|
|
25235
25545
|
if (!contract) throw new UnknownTaskTypeForSubmitToolError(taskType);
|
|
25236
|
-
const
|
|
25546
|
+
const maxSubmitValidationRetries = opts.maxSubmitValidationRetries ?? DEFAULT_MAX_SUBMIT_VALIDATION_RETRIES;
|
|
25237
25547
|
let captured = null;
|
|
25238
25548
|
let callCount = 0;
|
|
25549
|
+
let invalidCallCount = 0;
|
|
25550
|
+
let lastValidationFailure = null;
|
|
25551
|
+
let exhaustedValidationFailure = null;
|
|
25239
25552
|
return {
|
|
25240
25553
|
tool: defineTool({
|
|
25241
25554
|
name: contract.toolName,
|
|
25242
25555
|
label: `Submit ${taskType} output`,
|
|
25243
25556
|
description: contract.description,
|
|
25244
|
-
|
|
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,
|
|
25245
25560
|
async execute(_id, params) {
|
|
25246
|
-
|
|
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);
|
|
25247
25577
|
if (errors.length > 0) {
|
|
25248
|
-
|
|
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;
|
|
25249
25588
|
const details = {
|
|
25250
25589
|
captured: false,
|
|
25251
25590
|
callCount,
|
|
25591
|
+
invalidCallCount,
|
|
25592
|
+
maxSubmitValidationRetries,
|
|
25252
25593
|
error: "output_validation_failed"
|
|
25253
25594
|
};
|
|
25254
25595
|
recordTaskOutputParseResult({
|
|
@@ -25259,13 +25600,13 @@ function createSubmitOutputTool(taskType, opts = {}) {
|
|
|
25259
25600
|
return {
|
|
25260
25601
|
content: [{
|
|
25261
25602
|
type: "text",
|
|
25262
|
-
text:
|
|
25603
|
+
text: message
|
|
25263
25604
|
}],
|
|
25264
25605
|
details,
|
|
25265
25606
|
isError: true
|
|
25266
25607
|
};
|
|
25267
25608
|
}
|
|
25268
|
-
captured =
|
|
25609
|
+
captured = candidateParams;
|
|
25269
25610
|
callCount += 1;
|
|
25270
25611
|
return {
|
|
25271
25612
|
content: [{
|
|
@@ -25276,13 +25617,15 @@ function createSubmitOutputTool(taskType, opts = {}) {
|
|
|
25276
25617
|
captured: true,
|
|
25277
25618
|
callCount,
|
|
25278
25619
|
error: null
|
|
25279
|
-
}
|
|
25280
|
-
terminate: true
|
|
25620
|
+
}
|
|
25281
25621
|
};
|
|
25282
25622
|
}
|
|
25283
25623
|
}),
|
|
25284
25624
|
getCaptured: () => captured,
|
|
25285
|
-
getCallCount: () => callCount
|
|
25625
|
+
getCallCount: () => callCount,
|
|
25626
|
+
getInvalidCallCount: () => invalidCallCount,
|
|
25627
|
+
getLastValidationFailure: () => lastValidationFailure,
|
|
25628
|
+
getExhaustedValidationFailure: () => exhaustedValidationFailure
|
|
25286
25629
|
};
|
|
25287
25630
|
}
|
|
25288
25631
|
/**
|
|
@@ -25851,7 +26194,9 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
25851
26194
|
});
|
|
25852
26195
|
const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
|
|
25853
26196
|
model: opts.model,
|
|
25854
|
-
input: task.input
|
|
26197
|
+
input: task.input,
|
|
26198
|
+
inputCid: task.inputCid,
|
|
26199
|
+
maxSubmitValidationRetries: opts.maxSubmitValidationRetries
|
|
25855
26200
|
});
|
|
25856
26201
|
const submitTools = submitToolDefs;
|
|
25857
26202
|
try {
|
|
@@ -26038,19 +26383,28 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26038
26383
|
}
|
|
26039
26384
|
});
|
|
26040
26385
|
let runError = null;
|
|
26041
|
-
|
|
26042
|
-
|
|
26043
|
-
|
|
26044
|
-
|
|
26045
|
-
|
|
26046
|
-
|
|
26047
|
-
|
|
26048
|
-
|
|
26049
|
-
|
|
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", {
|
|
26050
26404
|
message,
|
|
26051
26405
|
phase: "session_prompt"
|
|
26052
|
-
})
|
|
26053
|
-
}
|
|
26406
|
+
})
|
|
26407
|
+
})).runError;
|
|
26054
26408
|
if (subagentHandle && subagentHandle.getCallCount() > 0) await emit("info", {
|
|
26055
26409
|
event: "subagent_summary",
|
|
26056
26410
|
callCount: subagentHandle.getCallCount()
|
|
@@ -26089,7 +26443,7 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26089
26443
|
});
|
|
26090
26444
|
}
|
|
26091
26445
|
else if (submitToolHandle) {
|
|
26092
|
-
parseError = {
|
|
26446
|
+
parseError = submitToolHandle.getExhaustedValidationFailure() ?? {
|
|
26093
26447
|
code: "submit_output_missing",
|
|
26094
26448
|
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26095
26449
|
};
|
|
@@ -26281,6 +26635,110 @@ function shouldEmitToolCallError(event) {
|
|
|
26281
26635
|
if (event.toolName === "bash") return false;
|
|
26282
26636
|
return true;
|
|
26283
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
|
+
}
|
|
26284
26742
|
/**
|
|
26285
26743
|
* Detect pi's bash-timeout error wrapper in a `tool_execution_end`
|
|
26286
26744
|
* result. The bash tool surfaces a timeout as a structured tool result
|
|
@@ -26352,6 +26810,19 @@ function describeToolErrorMessage(result) {
|
|
|
26352
26810
|
* See README.md for credential injection flow, tool split, sandbox.json
|
|
26353
26811
|
* reference, and headless/programmatic usage.
|
|
26354
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
|
+
}
|
|
26355
26826
|
function moltnetExtension(pi) {
|
|
26356
26827
|
pi.registerFlag("agent", {
|
|
26357
26828
|
description: "MoltNet agent name (required — pass --agent <name>)",
|
|
@@ -26642,4 +27113,4 @@ function moltnetExtension(pi) {
|
|
|
26642
27113
|
registerMoltnetReflectCommand(pi, state);
|
|
26643
27114
|
}
|
|
26644
27115
|
//#endregion
|
|
26645
|
-
export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resolveTaskWorktreePath, resumeVm, toGuestPath };
|
|
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.
|
|
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,8 +36,8 @@
|
|
|
36
36
|
"@earendil-works/gondolin": "^0.9.1",
|
|
37
37
|
"@opentelemetry/api": "^1.9.0",
|
|
38
38
|
"typebox": "^1.2.8",
|
|
39
|
-
"@themoltnet/agent-runtime": "0.33.
|
|
40
|
-
"@themoltnet/sdk": "0.
|
|
39
|
+
"@themoltnet/agent-runtime": "0.33.2",
|
|
40
|
+
"@themoltnet/sdk": "0.117.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|