@themoltnet/pi-extension 0.31.1 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +16 -0
- package/dist/index.js +134 -5
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -382,6 +382,22 @@ export declare interface ExecutePiTaskOptions {
|
|
|
382
382
|
* the attempt fails with output_validation_failed.
|
|
383
383
|
*/
|
|
384
384
|
maxSubmitValidationRetries?: number;
|
|
385
|
+
/**
|
|
386
|
+
* Number of same-session re-prompts when the model ends its turn WITHOUT
|
|
387
|
+
* calling the submit-output tool at all (no captured payload and no
|
|
388
|
+
* exhausted validation budget). Distinct from
|
|
389
|
+
* `maxSubmitValidationRetries`, which recovers *invalid-args* submit calls.
|
|
390
|
+
* Each re-prompt names the submit tool and forbids a prose reply. When the
|
|
391
|
+
* budget is spent the attempt still fails with `submit_output_missing`.
|
|
392
|
+
* Only applies to task types that register a submit tool. Default `3`. Set
|
|
393
|
+
* to `0` to disable. See #1528.
|
|
394
|
+
*/
|
|
395
|
+
maxSubmitMissingReprompts?: number;
|
|
396
|
+
/**
|
|
397
|
+
* Continuation prompt sent when a turn ends without a submit call. Defaults
|
|
398
|
+
* to `buildSubmitMissingPrompt(<tool name>)`.
|
|
399
|
+
*/
|
|
400
|
+
submitMissingPrompt?: string;
|
|
385
401
|
/**
|
|
386
402
|
* Cap provider-error retries inside the same Pi session. A retry is attempted
|
|
387
403
|
* only after a Pi assistant turn ends with `stopReason: "error"` and the
|
package/dist/index.js
CHANGED
|
@@ -25621,6 +25621,7 @@ function createSubmitOutputTool(taskType, opts = {}) {
|
|
|
25621
25621
|
};
|
|
25622
25622
|
}
|
|
25623
25623
|
}),
|
|
25624
|
+
toolName: contract.toolName,
|
|
25624
25625
|
getCaptured: () => captured,
|
|
25625
25626
|
getCallCount: () => callCount,
|
|
25626
25627
|
getInvalidCallCount: () => invalidCallCount,
|
|
@@ -26383,9 +26384,9 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26383
26384
|
}
|
|
26384
26385
|
});
|
|
26385
26386
|
let runError = null;
|
|
26386
|
-
|
|
26387
|
-
session,
|
|
26388
|
-
initialPrompt:
|
|
26387
|
+
const runPrompt = (promptText) => promptWithProviderErrorRetries({
|
|
26388
|
+
session: liveSession,
|
|
26389
|
+
initialPrompt: promptText,
|
|
26389
26390
|
cancelSignal: reporter.cancelSignal,
|
|
26390
26391
|
isCapAborted: () => capAbort !== null,
|
|
26391
26392
|
getProviderErrorState: () => ({
|
|
@@ -26404,7 +26405,33 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26404
26405
|
message,
|
|
26405
26406
|
phase: "session_prompt"
|
|
26406
26407
|
})
|
|
26407
|
-
})
|
|
26408
|
+
});
|
|
26409
|
+
const submitMissingConfig = resolveSubmitMissingConfig({
|
|
26410
|
+
submitToolHandle,
|
|
26411
|
+
maxSubmitMissingReprompts: opts.maxSubmitMissingReprompts,
|
|
26412
|
+
submitMissingPrompt: opts.submitMissingPrompt
|
|
26413
|
+
});
|
|
26414
|
+
const promptResult = await promptUntilSubmitted({
|
|
26415
|
+
runPrompt,
|
|
26416
|
+
initialPrompt: taskPrompt,
|
|
26417
|
+
submitMissingPrompt: submitMissingConfig.submitMissingPrompt,
|
|
26418
|
+
maxSubmitMissingReprompts: submitMissingConfig.maxSubmitMissingReprompts,
|
|
26419
|
+
getSubmitState: submitMissingConfig.getSubmitState,
|
|
26420
|
+
isStopped: () => submitRepromptStopped({
|
|
26421
|
+
cancelled: reporter.cancelSignal.aborted,
|
|
26422
|
+
capAborted: capAbort !== null,
|
|
26423
|
+
llmAbort
|
|
26424
|
+
}),
|
|
26425
|
+
onSubmitReprompt: async (event) => {
|
|
26426
|
+
await emit("info", event);
|
|
26427
|
+
}
|
|
26428
|
+
});
|
|
26429
|
+
runError = promptResult.runError;
|
|
26430
|
+
if (promptResult.submitReprompts > 0) await emit("info", {
|
|
26431
|
+
event: "submit_missing_summary",
|
|
26432
|
+
submitReprompts: promptResult.submitReprompts,
|
|
26433
|
+
captured: submitToolHandle ? submitToolHandle.getCaptured() !== null : false
|
|
26434
|
+
});
|
|
26408
26435
|
if (subagentHandle && subagentHandle.getCallCount() > 0) await emit("info", {
|
|
26409
26436
|
event: "subagent_summary",
|
|
26410
26437
|
callCount: subagentHandle.getCallCount()
|
|
@@ -26443,10 +26470,16 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
26443
26470
|
});
|
|
26444
26471
|
}
|
|
26445
26472
|
else if (submitToolHandle) {
|
|
26446
|
-
|
|
26473
|
+
const exhausted = submitToolHandle.getExhaustedValidationFailure();
|
|
26474
|
+
parseError = exhausted ?? {
|
|
26447
26475
|
code: "submit_output_missing",
|
|
26448
26476
|
message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
|
|
26449
26477
|
};
|
|
26478
|
+
if (!exhausted) recordTaskOutputParseResult({
|
|
26479
|
+
taskType: task.taskType,
|
|
26480
|
+
model: opts.model,
|
|
26481
|
+
code: "output_missing"
|
|
26482
|
+
});
|
|
26450
26483
|
await emit("error", {
|
|
26451
26484
|
message: parseError.message,
|
|
26452
26485
|
phase: "output_validation"
|
|
@@ -26723,6 +26756,102 @@ async function promptWithProviderErrorRetries(args) {
|
|
|
26723
26756
|
promptText = args.retryPrompt;
|
|
26724
26757
|
}
|
|
26725
26758
|
}
|
|
26759
|
+
/**
|
|
26760
|
+
* Continuation prompt used to recover a session that ended without calling
|
|
26761
|
+
* the submit-output tool. Names the exact tool and forbids a prose reply so a
|
|
26762
|
+
* model that "answered" in text is pushed to actually emit the tool call.
|
|
26763
|
+
*/
|
|
26764
|
+
function buildSubmitMissingPrompt(toolName) {
|
|
26765
|
+
return `You ended your turn but did not call the required \`${toolName}\` tool, so no output was captured and the task is not yet complete. Call \`${toolName}\` now with the final structured output exactly as described in the task prompt. Do not reply with prose, a summary, or an apology — the only way to finish is to call the tool.`;
|
|
26766
|
+
}
|
|
26767
|
+
/**
|
|
26768
|
+
* Whether the submit-missing re-prompt loop must stop before the next nudge.
|
|
26769
|
+
*
|
|
26770
|
+
* `llmAbort` matters as much as cancel/cap: when a turn ended with
|
|
26771
|
+
* `stopReason: 'error'` and the provider-error retry budget is spent (or the
|
|
26772
|
+
* error is non-retryable), `promptWithProviderErrorRetries` returns
|
|
26773
|
+
* `runError: null` yet leaves `llmAbort` set. Re-prompting then would nudge a
|
|
26774
|
+
* dead provider N more times (extra prompts + backoff) and emit misleading
|
|
26775
|
+
* `submit_missing_reprompt` events. We only re-prompt after a genuinely clean
|
|
26776
|
+
* `end_turn`.
|
|
26777
|
+
*/
|
|
26778
|
+
function submitRepromptStopped(state) {
|
|
26779
|
+
return state.cancelled || state.capAborted || state.llmAbort;
|
|
26780
|
+
}
|
|
26781
|
+
/**
|
|
26782
|
+
* Resolve the submit-missing recovery config from the registered submit tool
|
|
26783
|
+
* (if any) plus caller overrides. Extracted as a pure function so the
|
|
26784
|
+
* default-budget / disable-when-no-tool / gate-mapping logic is unit-tested —
|
|
26785
|
+
* `executePiTask` itself needs a booted VM and can't cover this seam.
|
|
26786
|
+
*
|
|
26787
|
+
* The default budget (3) is deliberately one higher than the invalid-args
|
|
26788
|
+
* correction budget (`maxSubmitValidationRetries`, default 2): a model that
|
|
26789
|
+
* never called the tool just needs a clear nudge, which converts more cheaply
|
|
26790
|
+
* and more often than fixing a malformed payload, so the extra attempt is
|
|
26791
|
+
* worth it.
|
|
26792
|
+
*/
|
|
26793
|
+
function resolveSubmitMissingConfig(args) {
|
|
26794
|
+
const handle = args.submitToolHandle;
|
|
26795
|
+
if (!handle) return {
|
|
26796
|
+
maxSubmitMissingReprompts: 0,
|
|
26797
|
+
submitMissingPrompt: "",
|
|
26798
|
+
getSubmitState: () => null
|
|
26799
|
+
};
|
|
26800
|
+
return {
|
|
26801
|
+
maxSubmitMissingReprompts: args.maxSubmitMissingReprompts ?? 3,
|
|
26802
|
+
submitMissingPrompt: args.submitMissingPrompt ?? buildSubmitMissingPrompt(handle.toolName),
|
|
26803
|
+
getSubmitState: () => ({
|
|
26804
|
+
captured: handle.getCaptured() !== null,
|
|
26805
|
+
exhausted: handle.getExhaustedValidationFailure() !== null
|
|
26806
|
+
})
|
|
26807
|
+
};
|
|
26808
|
+
}
|
|
26809
|
+
/**
|
|
26810
|
+
* Drive a Pi session until it either captures a valid submit-output call or
|
|
26811
|
+
* exhausts the submit-missing re-prompt budget.
|
|
26812
|
+
*
|
|
26813
|
+
* This is the third same-session recovery path, complementing the two that
|
|
26814
|
+
* already existed:
|
|
26815
|
+
* 1. Invalid submit args → the submit tool returns `isError`, the model
|
|
26816
|
+
* re-calls within the same turn (see `submit-output-tool.ts`).
|
|
26817
|
+
* 2. Provider/LLM API errors → `promptWithProviderErrorRetries` re-prompts.
|
|
26818
|
+
*
|
|
26819
|
+
* The gap this closes: a model (typically a weaker one) that ends its turn
|
|
26820
|
+
* cleanly with a prose answer and *never calls the submit tool at all*. With
|
|
26821
|
+
* neither a captured payload nor an exhausted validation budget, the executor
|
|
26822
|
+
* would otherwise fail straight to `submit_output_missing` with no chance to
|
|
26823
|
+
* recover. Here we nudge the model — up to `maxSubmitMissingReprompts` times —
|
|
26824
|
+
* to call the submit tool. Pi cannot force `toolChoice`, so this re-prompt is
|
|
26825
|
+
* the only in-session lever short of patching Pi. See issue #1528.
|
|
26826
|
+
*/
|
|
26827
|
+
async function promptUntilSubmitted(args) {
|
|
26828
|
+
const first = await args.runPrompt(args.initialPrompt);
|
|
26829
|
+
if (first.runError) return {
|
|
26830
|
+
runError: first.runError,
|
|
26831
|
+
submitReprompts: 0
|
|
26832
|
+
};
|
|
26833
|
+
let submitReprompts = 0;
|
|
26834
|
+
while (submitReprompts < args.maxSubmitMissingReprompts) {
|
|
26835
|
+
if (args.isStopped()) break;
|
|
26836
|
+
const state = args.getSubmitState();
|
|
26837
|
+
if (!state || state.captured || state.exhausted) break;
|
|
26838
|
+
submitReprompts += 1;
|
|
26839
|
+
await args.onSubmitReprompt?.({
|
|
26840
|
+
event: "submit_missing_reprompt",
|
|
26841
|
+
retry: submitReprompts,
|
|
26842
|
+
maxReprompts: args.maxSubmitMissingReprompts
|
|
26843
|
+
});
|
|
26844
|
+
const pass = await args.runPrompt(args.submitMissingPrompt);
|
|
26845
|
+
if (pass.runError) return {
|
|
26846
|
+
runError: pass.runError,
|
|
26847
|
+
submitReprompts
|
|
26848
|
+
};
|
|
26849
|
+
}
|
|
26850
|
+
return {
|
|
26851
|
+
runError: null,
|
|
26852
|
+
submitReprompts
|
|
26853
|
+
};
|
|
26854
|
+
}
|
|
26726
26855
|
function sanitizeProviderErrorRetryReason(value) {
|
|
26727
26856
|
return redactRetryTriageSecrets(value ?? "Pi turn ended with stopReason=error").slice(0, 500);
|
|
26728
26857
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/pi-extension",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
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/
|
|
40
|
-
"@themoltnet/
|
|
39
|
+
"@themoltnet/sdk": "0.117.0",
|
|
40
|
+
"@themoltnet/agent-runtime": "0.33.2"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|