@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 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
- runError = (await promptWithProviderErrorRetries({
26387
- session,
26388
- initialPrompt: taskPrompt,
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
- })).runError;
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
- parseError = submitToolHandle.getExhaustedValidationFailure() ?? {
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.1",
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/agent-runtime": "0.33.2",
40
- "@themoltnet/sdk": "0.117.0"
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",