@xopcai/xopc 0.0.384 → 0.0.385

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.
Files changed (150) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BSel9yf8.js → agent-avatar-dicebear-Cb9eaXxR.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-DgHMyhuY.js → agent-defaults-settings-panel-DEV-xtgm.js} +1 -1
  5. package/dist/gateway/static/root/assets/{agent-plugin-dialog-BosuN4dO.js → agent-plugin-dialog-DXKc3Luj.js} +1 -1
  6. package/dist/gateway/static/root/assets/{agents-B-pw5RKI.js → agents-DcNxwpJQ.js} +3 -3
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-DuOqKkfH.js → app-management-settings-panel-X5i0Tz7_.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-8JF9rXFr.js → appearance-settings-CmHHzquT.js} +1 -1
  9. package/dist/gateway/static/root/assets/{apps-page-Cuclpk0A.js → apps-page-ChFA4Zp0.js} +1 -1
  10. package/dist/gateway/static/root/assets/{automations-page-D7NwNy2I.js → automations-page-BFqsIqOI.js} +1 -1
  11. package/dist/gateway/static/root/assets/{automations-workspace-CSxW4wX0.js → automations-workspace-Blfak-fT.js} +1 -1
  12. package/dist/gateway/static/root/assets/{block-editor-Dnb7CH_l.js → block-editor-CpOLG3Co.js} +1 -1
  13. package/dist/gateway/static/root/assets/{browser-automations-page-GBgDe21o.js → browser-automations-page-BSbTJ9j2.js} +1 -1
  14. package/dist/gateway/static/root/assets/{browser-settings-page-B2FXaO3n.js → browser-settings-page-BUlILJrB.js} +1 -1
  15. package/dist/gateway/static/root/assets/{capabilities-page-f2-MquJn.js → capabilities-page-CeRdkwRf.js} +2 -2
  16. package/dist/gateway/static/root/assets/{capabilities-settings-panel-D9F0mw16.js → capabilities-settings-panel-S-cKsGja.js} +1 -1
  17. package/dist/gateway/static/root/assets/{capability-header-actions-DVRxJPQP.js → capability-header-actions-6w8pQhnn.js} +1 -1
  18. package/dist/gateway/static/root/assets/{channels-CnDm5T1t.js → channels-DfNTGBmp.js} +1 -1
  19. package/dist/gateway/static/root/assets/{chat-terminal-dock-BxSCDbK7.js → chat-terminal-dock-DnoQHpDc.js} +1 -1
  20. package/dist/gateway/static/root/assets/{computer-settings-page-DVAB-kQw.js → computer-settings-page-7yliE7sG.js} +1 -1
  21. package/dist/gateway/static/root/assets/{connector-service-page-D2EeLXei.js → connector-service-page-DOLAUFSW.js} +1 -1
  22. package/dist/gateway/static/root/assets/{connectors-page-DK9n4hgZ.js → connectors-page-DAsn97XG.js} +1 -1
  23. package/dist/gateway/static/root/assets/{date-picker-Cl9KfFj8.js → date-picker-DNDqr4Fk.js} +1 -1
  24. package/dist/gateway/static/root/assets/{dependency-picker-Ei6aX5WM.js → dependency-picker-B5oG7RgE.js} +1 -1
  25. package/dist/gateway/static/root/assets/{desktop-pet-4Evl4RKX.js → desktop-pet-K7kUCX1O.js} +1 -1
  26. package/dist/gateway/static/root/assets/{desktop-pet-settings-DAD_OWcX.js → desktop-pet-settings-KgOAuqSh.js} +1 -1
  27. package/dist/gateway/static/root/assets/{device-pairing-wizard-BA21epMn.js → device-pairing-wizard-DvqniZDm.js} +1 -1
  28. package/dist/gateway/static/root/assets/{directory-picker-path-field-B_Zb5nNR.js → directory-picker-path-field-BmOguLDl.js} +1 -1
  29. package/dist/gateway/static/root/assets/{extension-page-8gWSThWP.js → extension-page-DSSuMQxp.js} +1 -1
  30. package/dist/gateway/static/root/assets/{extension-settings-page-CunUbB09.js → extension-settings-page-DSd__mNd.js} +1 -1
  31. package/dist/gateway/static/root/assets/{fallback-models-BSAfzXby.js → fallback-models-D7bjbiwZ.js} +1 -1
  32. package/dist/gateway/static/root/assets/{gateway-settings-CN-2229W.js → gateway-settings-hSXGWumL.js} +1 -1
  33. package/dist/gateway/static/root/assets/{highlighted-body-KQOG7T2V-DktBCtHF.js → highlighted-body-KQOG7T2V-C7xJwE61.js} +1 -1
  34. package/dist/gateway/static/root/assets/{home-page-CUJcNW3H.js → home-page-DJNmlMfH.js} +1 -1
  35. package/dist/gateway/static/root/assets/{imports-page-DQm1MNNC.js → imports-page-BufZPzGE.js} +1 -1
  36. package/dist/gateway/static/root/assets/index-DBRwvWzm.css +1 -0
  37. package/dist/gateway/static/root/assets/{index-BPNL54bv.js → index-K8SXDtxr.js} +9 -9
  38. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-DaC5YjuJ.js → keyboard-shortcuts-settings-B8pPP5RF.js} +1 -1
  39. package/dist/gateway/static/root/assets/{local-app-workbench-page-DwQyyo7a.js → local-app-workbench-page-zEtvE7pJ.js} +1 -1
  40. package/dist/gateway/static/root/assets/{local-apps-page-CpZhivpf.js → local-apps-page-Bli7e_eg.js} +1 -1
  41. package/dist/gateway/static/root/assets/{logs-page-Db43nh1M.js → logs-page-BimZk3R4.js} +1 -1
  42. package/dist/gateway/static/root/assets/{management-settings-DrKejBEZ.js → management-settings-5Y4COyU9.js} +1 -1
  43. package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-DBRj4-1-.js +1 -0
  44. package/dist/gateway/static/root/assets/{note-detail-panel-a8NSwmmW.js → note-detail-panel-KLDPLVg7.js} +3 -3
  45. package/dist/gateway/static/root/assets/{notes-page-BKRke43F.js → notes-page-DST0oPXD.js} +1 -1
  46. package/dist/gateway/static/root/assets/{page-context-capture-button-DUaMkmxv.js → page-context-capture-button-Bmed83-6.js} +1 -1
  47. package/dist/gateway/static/root/assets/{personal-page-BqF-HHw1.js → personal-page-BG4Ystrm.js} +1 -1
  48. package/dist/gateway/static/root/assets/{product-open-page-GrhtTQIz.js → product-open-page-CXkYP9xE.js} +1 -1
  49. package/dist/gateway/static/root/assets/{product-preview-content-zdP7gN0y.js → product-preview-content-DjmupQSq.js} +1 -1
  50. package/dist/gateway/static/root/assets/{project-detail-page-9Vd_AqDW.js → project-detail-page-CzBmV0P6.js} +1 -1
  51. package/dist/gateway/static/root/assets/{projects-page-DGunhjoM.js → projects-page-DByqe5Em.js} +1 -1
  52. package/dist/gateway/static/root/assets/{remote-access-hub-BoRsF3Lx.js → remote-access-hub-DTjj-odF.js} +1 -1
  53. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-W0HDm--J.js → runtime-tools-settings-panel-B_ptn3T8.js} +1 -1
  54. package/dist/gateway/static/root/assets/{scenes-page-CYsFOfVJ.js → scenes-page-wkua4l5y.js} +1 -1
  55. package/dist/gateway/static/root/assets/{schema-form-CBxEbBQq.js → schema-form-VvOSM84Y.js} +1 -1
  56. package/dist/gateway/static/root/assets/{sessions-page-Dthfx_vc.js → sessions-page-D06XwYAT.js} +1 -1
  57. package/dist/gateway/static/root/assets/settings-advanced-gate-DoAHy1wj.js +1 -0
  58. package/dist/gateway/static/root/assets/{settings-page-CFs-WP_2.js → settings-page-C-6BJdb1.js} +1 -1
  59. package/dist/gateway/static/root/assets/{setup-status-panel-D2-9fXYL.js → setup-status-panel-L0JSG0K-.js} +1 -1
  60. package/dist/gateway/static/root/assets/{shares-settings-DTb88RHD.js → shares-settings-BbgAUM3v.js} +1 -1
  61. package/dist/gateway/static/root/assets/{skill-api-DJ8UBsxq.js → skill-api-BFU1tPyw.js} +1 -1
  62. package/dist/gateway/static/root/assets/{skills-page-DjbBOikT.js → skills-page-DZw6el4o.js} +1 -1
  63. package/dist/gateway/static/root/assets/{system-settings-panel-Dt5GkLhM.js → system-settings-panel-DshSGWNg.js} +1 -1
  64. package/dist/gateway/static/root/assets/{task-detail-page-88EShcbs.js → task-detail-page-CfZhq4XZ.js} +1 -1
  65. package/dist/gateway/static/root/assets/{tunnel-api-JHttXi63.js → tunnel-api-DFDXwWnU.js} +1 -1
  66. package/dist/gateway/static/root/assets/{usage-page-B3rUa0fW.js → usage-page-2RJLg54s.js} +1 -1
  67. package/dist/gateway/static/root/assets/{user-model-page-Cq1VSsZw.js → user-model-page-y1gjGYPI.js} +1 -1
  68. package/dist/gateway/static/root/assets/{work-discovery-overlay-BAtG_JHv.js → work-discovery-overlay-Ch-KfpCF.js} +1 -1
  69. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-Cw12NFgx.js → workflow-run-setup-panel-CTu297Hc.js} +1 -1
  70. package/dist/gateway/static/root/assets/{workflows-page-kR5ouOuM.js → workflows-page-CRZXG_rm.js} +1 -1
  71. package/dist/gateway/static/root/assets/{xopc-cloud-account-card-BTBVKpSg.js → xopc-cloud-account-card-hEfjVjnc.js} +1 -1
  72. package/dist/gateway/static/root/index.html +2 -2
  73. package/dist/package.js +1 -1
  74. package/dist/src/agent/child-agent-factory.js +7 -1
  75. package/dist/src/agent/embedded/execution-tool-policy.d.ts +1 -1
  76. package/dist/src/agent/embedded/execution-tool-policy.js +3 -2
  77. package/dist/src/agent/embedded/run-for-session.js +32 -12
  78. package/dist/src/agent/embedded/run-turn.js +108 -24
  79. package/dist/src/agent/embedded/session-runner.js +9 -1
  80. package/dist/src/agent/embedded/types.d.ts +2 -1
  81. package/dist/src/agent/external-tools/cliProvider.d.ts +1 -0
  82. package/dist/src/agent/external-tools/cliProvider.js +1 -0
  83. package/dist/src/agent/external-tools/composio-provider.js +2 -0
  84. package/dist/src/agent/external-tools/gateway-tools.js +8 -2
  85. package/dist/src/agent/external-tools/service.d.ts +1 -0
  86. package/dist/src/agent/external-tools/service.js +3 -1
  87. package/dist/src/agent/external-tools/types.d.ts +4 -0
  88. package/dist/src/agent/orchestration/agent-turn-policy.d.ts +1 -0
  89. package/dist/src/agent/orchestration/delegation-budget.js +2 -1
  90. package/dist/src/agent/orchestration/llm-turn-retry.d.ts +5 -0
  91. package/dist/src/agent/orchestration/llm-turn-retry.js +15 -5
  92. package/dist/src/agent/orchestration/turn-recovery.d.ts +6 -0
  93. package/dist/src/agent/orchestration/turn-recovery.js +29 -0
  94. package/dist/src/agent/service/direct-turn-helpers.js +6 -5
  95. package/dist/src/agent/service/process-direct-streaming.js +7 -1
  96. package/dist/src/agent/tasks/task-judge-service.js +28 -10
  97. package/dist/src/agent/tools/factory.js +10 -1
  98. package/dist/src/agent/tools/index.d.ts +1 -0
  99. package/dist/src/agent/tools/index.js +2 -1
  100. package/dist/src/agent/tools/personal-request-result-tool.js +20 -3
  101. package/dist/src/agent/tools/personal-request-tool.js +162 -15
  102. package/dist/src/agent/tools/personal-task-tool.d.ts +5 -1
  103. package/dist/src/agent/tools/personal-task-tool.js +137 -15
  104. package/dist/src/agent/tools/task-collaboration-tool.d.ts +16 -0
  105. package/dist/src/agent/tools/task-collaboration-tool.js +153 -0
  106. package/dist/src/agent/tools/xopc-use-tool.js +1 -1
  107. package/dist/src/cli/commands/channels.js +1 -1
  108. package/dist/src/cli/commands/connectors.js +1 -1
  109. package/dist/src/cli/commands/extension-marketplace.js +1 -1
  110. package/dist/src/cli/commands/models.js +1 -1
  111. package/dist/src/cli/commands/onboard.js +1 -1
  112. package/dist/src/connectors/connection-recovery-service.d.ts +6 -0
  113. package/dist/src/connectors/connection-recovery-service.js +151 -32
  114. package/dist/src/extensions/sdk/index.js +1 -1
  115. package/dist/src/gateway/hono/routes/connection-waits.js +3 -1
  116. package/dist/src/gateway/service/run-gateway-agent.js +9 -5
  117. package/dist/src/gateway/service/session-input-coordinator.js +4 -2
  118. package/dist/src/gateway/service.js +9 -9
  119. package/dist/src/personal-agent/prompts.js +6 -2
  120. package/dist/src/personal-agent/reply-composer.js +23 -9
  121. package/dist/src/personal-agent/request-access.d.ts +3 -0
  122. package/dist/src/personal-agent/request-access.js +31 -0
  123. package/dist/src/personal-agent/request-delivery.js +9 -4
  124. package/dist/src/personal-agent/request-repository.d.ts +1 -0
  125. package/dist/src/personal-agent/request-service.d.ts +2 -0
  126. package/dist/src/personal-agent/request-service.js +32 -7
  127. package/dist/src/providers/model-response.js +13 -2
  128. package/dist/src/session/store.js +19 -6
  129. package/dist/src/storage/sqlite/connection-wait-repository.d.ts +2 -0
  130. package/dist/src/storage/sqlite/connection-wait-repository.js +83 -4
  131. package/dist/src/storage/sqlite/transcript-repository.d.ts +2 -0
  132. package/dist/src/storage/sqlite/transcript-repository.js +25 -1
  133. package/dist/src/tasks/task-application-service.js +1 -1
  134. package/dist/src/tasks/task-collaboration-delivery.js +4 -1
  135. package/dist/src/tasks/task-collaboration-repository.d.ts +3 -0
  136. package/dist/src/tasks/task-collaboration-repository.js +11 -1
  137. package/dist/src/tasks/task-context-assembler.js +13 -5
  138. package/dist/src/tasks/task-continuation.d.ts +5 -0
  139. package/dist/src/tasks/task-continuation.js +32 -0
  140. package/dist/src/tasks/task-main-update-delivery.js +13 -8
  141. package/dist/src/tasks/task-read-model-projector.js +1 -1
  142. package/dist/src/tasks/task-run-coordinator.d.ts +1 -0
  143. package/dist/src/tasks/task-run-coordinator.js +21 -6
  144. package/dist/src/tasks/task-run-dispatcher.js +11 -0
  145. package/dist/src/tasks/task-run-repository.d.ts +8 -0
  146. package/dist/src/tasks/task-run-repository.js +32 -1
  147. package/package.json +5 -5
  148. package/dist/gateway/static/root/assets/index-yseLAt9D.css +0 -1
  149. package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-C4VzqE9z.js +0 -1
  150. package/dist/gateway/static/root/assets/settings-advanced-gate-O_0Lk38S.js +0 -1
@@ -21,6 +21,7 @@ import { voicePresentationPrompt } from "../prompt/voice-presentation.js";
21
21
  import { applyStartupContextToUserMessage } from "../reply/apply-turn-user-enrichment.js";
22
22
  import { AgentRunSupervisor } from "../orchestration/agent-run-supervisor.js";
23
23
  import { materializeNativeExternalTools } from "./external-tool-discovery.js";
24
+ import { getTaskContinuation, getTaskExecutionDeadline } from "../../tasks/task-continuation.js";
24
25
  import crypto from "node:crypto";
25
26
  //#region src/agent/embedded/run-for-session.ts
26
27
  init_src();
@@ -45,14 +46,21 @@ async function runEmbeddedTurnForSessionInner(params) {
45
46
  let idleJob;
46
47
  let turnCompleted = false;
47
48
  const runId = params.runId ?? crypto.randomUUID();
49
+ const taskContinuation = getTaskContinuation(conversationId, runId);
48
50
  const config = params.getConfig?.();
49
51
  if (config) setTraceIdentity({ agentId: resolveEffectiveAgentProfileForSession(conversationId).agentId });
50
52
  const supervisor = new AgentRunSupervisor({
51
53
  timeoutMs: resolveAgentTurnTimeoutMs(config, conversationId),
52
- deadlineAtMs: params.deadlineAtMs,
54
+ deadlineAtMs: params.deadlineAtMs ?? getTaskExecutionDeadline(conversationId, runId),
53
55
  parentSignal: params.abortSignal
54
56
  });
55
57
  const finish = async (result) => {
58
+ if (supervisor.signal.aborted && supervisor.signal.reason instanceof Error && supervisor.signal.reason.message === "Agent run deadline expired") result = {
59
+ ...result,
60
+ ok: false,
61
+ retryable: false,
62
+ errorMessage: supervisor.signal.reason.message
63
+ };
56
64
  turnCompleted = result.ok && result.stopReason !== "connection_required" && result.stopReason !== "clarification_required";
57
65
  if (result.stopReason === "connection_required" || result.stopReason === "clarification_required") return result;
58
66
  try {
@@ -91,15 +99,15 @@ async function runEmbeddedTurnForSessionInner(params) {
91
99
  };
92
100
  try {
93
101
  await params.beforeTurn?.();
94
- if (supervisor.signal.aborted) return {
102
+ if (supervisor.signal.aborted) return await finish({
95
103
  ok: false,
96
104
  errorMessage: "aborted"
97
- };
105
+ });
98
106
  await agentManager.ensureMemoryReadyForSession?.(conversationId);
99
- if (supervisor.signal.aborted) return {
107
+ if (supervisor.signal.aborted) return await finish({
100
108
  ok: false,
101
109
  errorMessage: "aborted"
102
- };
110
+ });
103
111
  const agent = agentManager.getOrCreateAgent(conversationId);
104
112
  const mm = modelManager;
105
113
  const configuredModelRef = String(mm.getModelForSession(conversationId));
@@ -185,10 +193,10 @@ async function runEmbeddedTurnForSessionInner(params) {
185
193
  startupAction: params.startupAction,
186
194
  force: params.forceStartupContext
187
195
  });
188
- if (supervisor.signal.aborted) return {
196
+ if (supervisor.signal.aborted) return await finish({
189
197
  ok: false,
190
198
  errorMessage: "aborted"
191
- };
199
+ });
192
200
  }
193
201
  try {
194
202
  await maybeAutoCompactBeforeTurn({
@@ -206,16 +214,16 @@ async function runEmbeddedTurnForSessionInner(params) {
206
214
  onEvent: params.onEvent
207
215
  });
208
216
  } catch (err) {
209
- if (supervisor.signal.aborted) return {
217
+ if (supervisor.signal.aborted) return await finish({
210
218
  ok: false,
211
219
  errorMessage: "aborted"
212
- };
220
+ });
213
221
  throw err;
214
222
  }
215
- if (supervisor.signal.aborted) return {
223
+ if (supervisor.signal.aborted) return await finish({
216
224
  ok: false,
217
225
  errorMessage: "aborted"
218
- };
226
+ });
219
227
  log.info({
220
228
  conversationId,
221
229
  runId,
@@ -281,9 +289,21 @@ async function runEmbeddedTurnForSessionInner(params) {
281
289
  timeoutMs: attemptPlan.timeoutMs,
282
290
  turnPolicy,
283
291
  abortSignal: supervisor.signal,
284
- onEvent: params.onEvent,
292
+ onEvent: (event) => {
293
+ if (event.type === "error") {
294
+ log.warn({
295
+ conversationId,
296
+ runId,
297
+ modelRef: candidateModelRef,
298
+ errorMessage: event.content
299
+ }, "Model attempt failed");
300
+ return;
301
+ }
302
+ params.onEvent?.(event);
303
+ },
285
304
  onAgentEvent: (event) => agentManager.emitRuntimeEvent(conversationId, event),
286
305
  resumeLastUserMessage,
306
+ recoveryMessage: taskContinuation?.message,
287
307
  requireVisibleReply: params.requireVisibleReply
288
308
  }), runId));
289
309
  attempt.finish(turnResult.ok ? "success" : "error", turnResult.errorMessage);
@@ -12,10 +12,11 @@ import { continuesAfterTool, trackAiUsageStream } from "../../usage/recorder.js"
12
12
  import { cloudCreditBudgetStopMessage, isCloudCreditBudgetExhausted, reportCloudCreditError, withCloudCreditBudget } from "../../providers/xopc-cloud-credit-budget.js";
13
13
  import { isAgentTurnUnsettledError, resolveAgentTurnTimeoutMs, runAgentTurnWithTimeout } from "../orchestration/run-agent-turn-with-timeout.js";
14
14
  import { isContextOverflowError } from "../orchestration/context-overflow.js";
15
+ import { canResumeAfterModelFailure, completedRecoveryToolResult } from "../orchestration/turn-recovery.js";
15
16
  import { getAssistantTurnErrorMessage, isAssistantTurnAborted, isAssistantTurnFailed, maybeRetryTurnAfterTransientLlmFailure, stripTrailingErrorAssistantMessages } from "../orchestration/llm-turn-retry.js";
16
17
  import { allowsExecutionTool, filterExecutionTools, resolveExecutionToolPolicy } from "./execution-tool-policy.js";
17
18
  import { runWithEmbeddedExecutionSession } from "./execution-context.js";
18
- import { externalPolicyContext } from "./tool-metadata.js";
19
+ import { externalPolicyContext, getXopcToolMetadata } from "./tool-metadata.js";
19
20
  import { isComputerControlActive, isComputerLeaseTool, stopComputerControl } from "../../computer/control-guard.js";
20
21
  import { EmbeddedRunConflictError, abortEmbeddedRun, acquireEmbeddedRunLease, queueEmbeddedSteer } from "./runs.js";
21
22
  import { lastAssistantPlainText, subscribeEmbeddedSessionEvents } from "./subscribe-session.js";
@@ -46,6 +47,17 @@ init_sqlite();
46
47
  init_session_repository();
47
48
  const log = createLogger("EmbeddedRun");
48
49
  const LOG_PREVIEW_MAX_CHARS = 300;
50
+ const RECOVERY_READ_TOOLS = new Set([
51
+ "read_file",
52
+ "find",
53
+ "grep",
54
+ "list_dir",
55
+ "tool_manual",
56
+ "tool_search",
57
+ "review_workspace",
58
+ "language_diagnostics",
59
+ "session_status"
60
+ ]);
49
61
  function truncateForLog(value, maxChars = LOG_PREVIEW_MAX_CHARS) {
50
62
  return value.length > maxChars ? `${value.slice(0, maxChars)}…` : value;
51
63
  }
@@ -216,6 +228,11 @@ async function maybeRecoverInterruptedContext(params) {
216
228
  const overflow = !!errorMessage && isContextOverflowError(errorMessage);
217
229
  const recoverableLength = !!assistant && isRecoverableLengthForModel(assistant, params.model);
218
230
  if (!overflow && !recoverableLength) return false;
231
+ const discardedCalls = new Set(recoverableLength && assistant ? discardedAttempt(assistant).toolCallIds : []);
232
+ if (!canResumeAfterModelFailure(params.agent.state.messages.filter((message) => !recoverableLength || message !== assistant && !(message.role === "toolResult" && discardedCalls.has(message.toolCallId))))) {
233
+ log.warn({ conversationId: params.conversationId }, "Context recovery stopped because a tool outcome is unconfirmed");
234
+ return false;
235
+ }
219
236
  if (recoverableLength && (!params.policy.enabled || params.recoveryState.interruptedAttempts >= 1)) {
220
237
  log.warn({
221
238
  conversationId: params.conversationId,
@@ -349,6 +366,7 @@ async function runXopcEmbeddedTurnInner(params) {
349
366
  session.agent.state.messages = stripTrailingErrorAssistantMessages(await transcriptRuntime.loadMessages());
350
367
  runner.piSm.setActiveTurnId?.(runId);
351
368
  const recoveryState = { interruptedAttempts: 0 };
369
+ const requestRetryBudget = { continues: 0 };
352
370
  let useAuthoritativeProjection = false;
353
371
  const basePrepareNextTurn = session.agent.prepareNextTurnWithContext;
354
372
  const inTurnPrepareNextTurn = async (turn, signal) => {
@@ -566,10 +584,11 @@ async function runXopcEmbeddedTurnInner(params) {
566
584
  session.agent.streamFunction = loggingStreamFn;
567
585
  const verification = await RunVerification.open(workspaceDir);
568
586
  const checkpoints = runner.piSm.getBranch?.() ?? [];
587
+ const recoveryTurn = params.resumeLastUserMessage === true || Boolean(params.recoveryMessage);
569
588
  const lastStart = checkpoints.findLastIndex((entry) => entry.type === "custom" && entry.customType === "coding_run_started");
570
589
  const lastSummary = checkpoints.findLastIndex((entry) => entry.type === "custom" && entry.customType === "coding_verification");
571
- const checkpoint = checkpoints[params.resumeLastUserMessage && lastSummary >= lastStart ? lastSummary : lastStart];
572
- if (checkpoint?.type === "custom" && (params.resumeLastUserMessage || lastStart > lastSummary)) verification.restore(checkpoint.data, params.resumeLastUserMessage === true && lastSummary >= lastStart);
590
+ const checkpoint = checkpoints[recoveryTurn && lastSummary >= lastStart ? lastSummary : lastStart];
591
+ if (checkpoint?.type === "custom" && (recoveryTurn || lastStart > lastSummary)) verification.restore(checkpoint.data, recoveryTurn && lastSummary >= lastStart);
573
592
  runner.piSm.appendCustomEntry("coding_run_started", {
574
593
  runId,
575
594
  workspace: workspaceDir,
@@ -577,6 +596,11 @@ async function runXopcEmbeddedTurnInner(params) {
577
596
  ...await verification.summary()
578
597
  });
579
598
  let policyStopped = false;
599
+ let policyCompleted = false;
600
+ let recoveringModel = params.resumeLastUserMessage === true || Boolean(params.recoveryMessage);
601
+ let recoveryMessages = [...session.agent.state.messages];
602
+ let policyStopReason;
603
+ let lengthRecoveryExhausted = false;
580
604
  let connectionStopped = false;
581
605
  let clarificationStopped = false;
582
606
  let sentVisibleMessage = false;
@@ -588,6 +612,7 @@ async function runXopcEmbeddedTurnInner(params) {
588
612
  };
589
613
  if (executionToolPolicy.surface !== "agent" && resolveExecutionToolPolicy(toolPolicyConversationId).surface !== executionToolPolicy.surface) {
590
614
  policyStopped = true;
615
+ policyStopReason = "Execution identity changed during the run";
591
616
  quarantineRunner = true;
592
617
  return {
593
618
  block: true,
@@ -615,8 +640,23 @@ async function runXopcEmbeddedTurnInner(params) {
615
640
  const decision = await params.turnPolicy?.beforeToolCall(externalPolicyContext(context, tools), signal);
616
641
  if (decision?.block) {
617
642
  policyStopped ||= decision.terminate === true;
643
+ if (decision.terminate) policyStopReason = decision.reason;
618
644
  return decision;
619
645
  }
646
+ const tool = tools.find((candidate) => candidate.name === context.toolCall.name);
647
+ const metadata = tool && getXopcToolMetadata(tool);
648
+ const readOnly = RECOVERY_READ_TOOLS.has(context.toolCall.name) || metadata?.external?.readOnly === true || metadata?.annotations?.readOnlyHint === true || context.toolCall.name === "task_collaboration" && context.args.command === "get" || context.toolCall.name === "managed_job" && [
649
+ "wait",
650
+ "status",
651
+ "list"
652
+ ].includes(String(context.args.action));
653
+ if (recoveringModel && !readOnly) {
654
+ const recorded = completedRecoveryToolResult(recoveryMessages, context.toolCall.name, context.args);
655
+ if (recorded) return {
656
+ block: true,
657
+ reason: recorded
658
+ };
659
+ }
620
660
  if (context.toolCall.name !== "read_file") {
621
661
  const scoped = await instructions.forTool(context.toolCall.name, context.args);
622
662
  if (scoped) return {
@@ -661,9 +701,12 @@ async function runXopcEmbeddedTurnInner(params) {
661
701
  conversationId,
662
702
  runId
663
703
  }, "Stopping after the bounded truncated-response recovery attempt");
704
+ lengthRecoveryExhausted = true;
664
705
  return { action: "end" };
665
706
  }
666
707
  policyStopped ||= params.turnPolicy?.shouldStopAfterTurn(context) ?? false;
708
+ policyCompleted ||= policyStopped && params.turnPolicy?.stopReason?.() === "completed";
709
+ if (policyStopped) policyStopReason ??= "Agent execution policy stopped before completion";
667
710
  if (policyStopped || connectionStopped || clarificationStopped) return { action: "end" };
668
711
  const continuation = params.turnPolicy?.takeContinuationMessage?.();
669
712
  if (continuation) {
@@ -721,7 +764,14 @@ async function runXopcEmbeddedTurnInner(params) {
721
764
  content: clarificationResume.content,
722
765
  display: false
723
766
  }, { triggerTurn: true });
724
- else if (params.resumeLastUserMessage) await session.agent.continue();
767
+ else if (params.recoveryMessage && !params.resumeLastUserMessage) {
768
+ if (!canResumeAfterModelFailure(session.agent.state.messages)) throw new Error("Cannot resume because a tool outcome is unconfirmed");
769
+ await session.sendCustomMessage({
770
+ customType: "task_continuation",
771
+ content: params.recoveryMessage,
772
+ display: false
773
+ }, { triggerTurn: true });
774
+ } else if (params.resumeLastUserMessage) await session.agent.continue();
725
775
  else {
726
776
  const text = userMessageToPromptText(userMessage);
727
777
  const images = params.images ?? [];
@@ -730,25 +780,47 @@ async function runXopcEmbeddedTurnInner(params) {
730
780
  await session.agent.waitForIdle();
731
781
  clarificationStopped ||= isClarificationSuspended(conversationId, runId);
732
782
  if (connectionStopped || clarificationStopped) return;
733
- await maybeRetryTurnAfterTransientLlmFailure(session.agent, {
734
- conversationId,
735
- log,
736
- signal: runAbortSignal
737
- });
738
- await maybeRecoverInterruptedContext({
739
- agent: session.agent,
740
- model: resolvedModel,
741
- conversationId,
742
- transcriptRuntime,
743
- policy: compactionPolicy,
744
- systemPrompt: [systemPrompt, rootInstructions].filter(Boolean).join("\n\n"),
745
- abortSignal: runAbortSignal,
746
- onEvent,
747
- recoveryState,
748
- onContextRecovered: () => {
749
- useAuthoritativeProjection = true;
750
- }
751
- });
783
+ const recoverSettledTurn = async () => {
784
+ if (policyStopped || connectionStopped || clarificationStopped) return;
785
+ await maybeRetryTurnAfterTransientLlmFailure(session.agent, {
786
+ conversationId,
787
+ log,
788
+ signal: runAbortSignal,
789
+ budget: requestRetryBudget,
790
+ shouldRetry: () => !policyStopped && !connectionStopped && !clarificationStopped,
791
+ onRetry: () => {
792
+ recoveringModel = true;
793
+ recoveryMessages = [...session.agent.state.messages];
794
+ }
795
+ });
796
+ if (policyStopped || connectionStopped || clarificationStopped) return;
797
+ if (await maybeRecoverInterruptedContext({
798
+ agent: session.agent,
799
+ model: resolvedModel,
800
+ conversationId,
801
+ transcriptRuntime,
802
+ policy: compactionPolicy,
803
+ systemPrompt: [systemPrompt, rootInstructions].filter(Boolean).join("\n\n"),
804
+ abortSignal: runAbortSignal,
805
+ onEvent,
806
+ recoveryState,
807
+ onContextRecovered: () => {
808
+ useAuthoritativeProjection = true;
809
+ recoveringModel = true;
810
+ }
811
+ })) await maybeRetryTurnAfterTransientLlmFailure(session.agent, {
812
+ conversationId,
813
+ log,
814
+ signal: runAbortSignal,
815
+ budget: requestRetryBudget,
816
+ shouldRetry: () => !policyStopped && !connectionStopped && !clarificationStopped,
817
+ onRetry: () => {
818
+ recoveringModel = true;
819
+ recoveryMessages = [...session.agent.state.messages];
820
+ }
821
+ });
822
+ };
823
+ await recoverSettledTurn();
752
824
  if (params.requireVisibleReply && !sentVisibleMessage && !policyStopped && !runAbortSignal.aborted && !isAssistantTurnFailed(session.agent) && !isAssistantTurnAborted(session.agent) && lastAssistantPlainText(session).trim() === "NO_REPLY") {
753
825
  await session.sendCustomMessage({
754
826
  customType: "visible_reply_required",
@@ -756,6 +828,7 @@ async function runXopcEmbeddedTurnInner(params) {
756
828
  display: false
757
829
  }, { triggerTurn: true });
758
830
  await session.agent.waitForIdle();
831
+ await recoverSettledTurn();
759
832
  }
760
833
  const pending = await verification.pendingContext();
761
834
  if ((params.verifyChanges ?? false) && pending && !policyStopped && !runAbortSignal.aborted && !isAssistantTurnFailed(session.agent) && !isAssistantTurnAborted(session.agent)) {
@@ -765,6 +838,7 @@ async function runXopcEmbeddedTurnInner(params) {
765
838
  display: false
766
839
  }, { triggerTurn: true });
767
840
  await session.agent.waitForIdle();
841
+ await recoverSettledTurn();
768
842
  }
769
843
  }), timeoutMs);
770
844
  if (connectionStopped) runner.piSm.appendCustomEntry("connection_required", {
@@ -787,11 +861,21 @@ async function runXopcEmbeddedTurnInner(params) {
787
861
  errorMessage: "aborted"
788
862
  };
789
863
  if (isAssistantTurnAborted(session.agent)) return {
790
- ok: true,
864
+ ok: false,
865
+ retryable: false,
866
+ errorMessage: "aborted",
867
+ lastAssistantText: lastAssistantPlainText(session)
868
+ };
869
+ if (policyStopped && !policyCompleted || lengthRecoveryExhausted || lastAssistantMessage(session.agent.state.messages)?.stopReason === "length") return {
870
+ ok: false,
871
+ retryable: false,
872
+ errorMessage: policyStopReason ?? "Assistant response was truncated before completion",
873
+ stopReason: policyStopped ? "policy_limit" : "response_truncated",
791
874
  lastAssistantText: lastAssistantPlainText(session)
792
875
  };
793
876
  if (isAssistantTurnFailed(session.agent)) return {
794
877
  ok: false,
878
+ retryable: canResumeAfterModelFailure(session.agent.state.messages),
795
879
  errorMessage: getAssistantTurnErrorMessage(session.agent) ?? "Assistant turn failed",
796
880
  lastAssistantText: lastAssistantPlainText(session)
797
881
  };
@@ -236,7 +236,15 @@ var EmbeddedSessionRunnerPool = class {
236
236
  const settingsManager = createEmbeddedSettingsManager(workspaceDir);
237
237
  const piSm = guardSessionManager(params.transcriptRuntime.openSessionManager(workspaceDir), {
238
238
  conversationId: params.transcriptRuntime.persistent ? runtimeId : void 0,
239
- persistCustomTypes: ["codemode-store", TOOL_DISCOVERY_STATE],
239
+ persistCustomTypes: [
240
+ "codemode-store",
241
+ TOOL_DISCOVERY_STATE,
242
+ "coding_run_started",
243
+ "coding_verification",
244
+ "xopc.model-selection",
245
+ "connection_required",
246
+ "clarification_required"
247
+ ],
240
248
  contextWindowTokens: model.contextWindow ?? 128e3,
241
249
  transformMessageForPersistence: params.transcriptRuntime.persistent ? (message) => transformUserMessageForPersistence(runtimeId, message) : void 0
242
250
  });
@@ -95,12 +95,13 @@ export type RunXopcEmbeddedTurnParams = {
95
95
  images?: ImageContent[];
96
96
  /** Continue from the persisted trailing user row instead of appending it again. */
97
97
  resumeLastUserMessage?: boolean;
98
+ recoveryMessage?: string;
98
99
  verifyChanges?: boolean;
99
100
  requireVisibleReply?: boolean;
100
101
  };
101
102
  export type RunXopcEmbeddedTurnResult = {
102
103
  ok: boolean;
103
- stopReason?: 'connection_required' | 'clarification_required';
104
+ stopReason?: 'connection_required' | 'clarification_required' | 'policy_limit' | 'response_truncated';
104
105
  /** False for harness failures that must not trigger another model attempt. */
105
106
  retryable?: boolean;
106
107
  errorMessage?: string;
@@ -23,6 +23,7 @@ export declare class CliToolProvider implements ExternalToolProvider {
23
23
  title: string;
24
24
  summary: string;
25
25
  description: string;
26
+ readOnly: boolean;
26
27
  batchRead: boolean;
27
28
  inputSchema: {
28
29
  properties: {
@@ -120,6 +120,7 @@ var CliToolProvider = class {
120
120
  title: action.id,
121
121
  summary: action.description,
122
122
  description: action.description,
123
+ readOnly: action.scope === "read",
123
124
  batchRead: action.scope === "read",
124
125
  inputSchema: {
125
126
  ...action.inputSchema,
@@ -232,6 +232,7 @@ var ComposioToolProvider = class {
232
232
  summary,
233
233
  description: `${summary} Available accounts: ${JSON.stringify(accounts)}. Use xopcAccountId to select one; never guess when the user's intent is ambiguous.`,
234
234
  inputSchema: actionInputSchema(action),
235
+ readOnly: action.scope === "read" && action.curated,
235
236
  batchRead: accounts.length > 0 && action.scope === "read" && action.curated && BATCH_READ_ACTIONS.has(action.actionId)
236
237
  };
237
238
  }
@@ -313,6 +314,7 @@ var ComposioToolProvider = class {
313
314
  confirmed = true;
314
315
  }
315
316
  const authorizeCurrent = () => {
317
+ context.authorize?.();
316
318
  context.signal?.throwIfAborted();
317
319
  const current = this.availableInstallations();
318
320
  const installation = this.resolve(toolRef, current.installations)?.installation;
@@ -10,6 +10,7 @@ import { connectorPrincipalForSession } from "../../connectors/principal.js";
10
10
  import { parseExternalToolRef } from "./refs.js";
11
11
  import { setXopcToolMetadata } from "../embedded/tool-metadata.js";
12
12
  import { getStoreConnectorInstallPlan, parseStoreConnectorCandidateRef, searchStoreConnectorInstallCandidates } from "../../capabilities/store-connector.js";
13
+ import { assertPersonalRequestAccountAccess } from "../../personal-agent/request-access.js";
13
14
  import { connectionCandidates, resolveConnectionCandidate } from "../../connectors/connection-candidates.js";
14
15
  import { ExternalToolService } from "./service.js";
15
16
  import { EXTERNAL_TOOL_SOURCES } from "./types.js";
@@ -143,6 +144,7 @@ function createExternalToolGatewayTools(providers, getContext, getConfig, getExe
143
144
  const connectorId = composio ? getConnectorInstallation(composio.namespace)?.connectorId : cli && config ? listConnectorInstances(config).find((instance) => instance.instanceId === cli.namespace)?.connectorId : void 0;
144
145
  if (connectorId !== request.connectorId) throw new Error("Use only the selected Personal request connector");
145
146
  if (composio && !listConnectorActionMetadata(connectorId).some((action) => action.actionId === composio.toolName && action.scope === "read" && action.curated)) throw new Error("Personal requests permit only curated read operations");
147
+ assertPersonalRequestAccountAccess(request);
146
148
  const account = getConnectorAccount(request.accountId);
147
149
  const connection = account?.currentConnectionId ? getConnectorConnection(account.currentConnectionId) : void 0;
148
150
  if (!connection || connection.status !== "active" || connection.expiresAt && Date.parse(connection.expiresAt) <= Date.now()) {
@@ -157,11 +159,15 @@ function createExternalToolGatewayTools(providers, getContext, getConfig, getExe
157
159
  }
158
160
  const result = await service.execute({
159
161
  ...params,
160
- ...request ? { readOnly: true } : {},
162
+ ...request ? {
163
+ readOnly: void 0,
164
+ personalReadOnly: true
165
+ } : {},
161
166
  context: {
162
167
  toolCallId,
163
168
  signal,
164
- onUpdate
169
+ onUpdate,
170
+ ...request ? { authorize: () => assertPersonalRequestAccountAccess(request) } : {}
165
171
  }
166
172
  });
167
173
  return {
@@ -28,6 +28,7 @@ export declare class ExternalToolService {
28
28
  arguments?: Record<string, unknown>;
29
29
  approvalId?: string;
30
30
  readOnly?: boolean;
31
+ personalReadOnly?: boolean;
31
32
  context: ExternalToolExecutionContext;
32
33
  }): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>>>;
33
34
  private providerForRef;
@@ -137,10 +137,11 @@ var ExternalToolService = class {
137
137
  const descriptor = await provider.describe(params.toolRef);
138
138
  if (!descriptor) throw new Error(`External tool is unavailable: ${params.toolRef}`);
139
139
  const current = versionDescriptor(descriptor);
140
+ if (params.personalReadOnly && current.readOnly !== true && current.batchRead !== true) throw new Error("This external operation has no host-curated read-only contract");
140
141
  if (params.readOnly && current.batchRead !== true) throw new Error("This external operation is not approved for batch reads");
141
142
  if (current.revision !== params.revision) throw new Error(`Tool contract changed. Describe ${params.toolRef} again before executing it.`);
142
143
  const args = params.arguments ?? {};
143
- if (params.readOnly && (provider.source === "composio" || provider.source === "cli") && (typeof args.xopcAccountId !== "string" || !args.xopcAccountId)) throw new Error("Batch connector reads require an explicit xopcAccountId");
144
+ if ((params.readOnly || params.personalReadOnly) && (provider.source === "composio" || provider.source === "cli") && (typeof args.xopcAccountId !== "string" || !args.xopcAccountId)) throw new Error("Batch connector reads require an explicit xopcAccountId");
144
145
  let validate;
145
146
  try {
146
147
  validate = this.ajv.compile(current.inputSchema);
@@ -151,6 +152,7 @@ var ExternalToolService = class {
151
152
  if (!validate(input)) throw new Error(`Arguments do not match ${params.toolRef}: ${this.ajv.errorsText(validate.errors)}`);
152
153
  };
153
154
  validateArguments(args);
155
+ params.context.authorize?.();
154
156
  return provider.execute(params.toolRef, args, params.approvalId, {
155
157
  ...params.context,
156
158
  contractRevision: current.contractRevision,
@@ -27,6 +27,8 @@ export interface ExternalToolDescriptor extends ExternalToolSearchHit {
27
27
  exposure?: import('@earendil-works/pi-coding-agent').ToolExposure;
28
28
  /** Host-curated contract; never copied from remote readOnlyHint annotations. */
29
29
  batchRead?: boolean;
30
+ /** Host-curated read scope; separate from eligibility for batch execution. */
31
+ readOnly?: boolean;
30
32
  }
31
33
  export interface VersionedExternalToolDescriptor extends ExternalToolDescriptor {
32
34
  revision: string;
@@ -36,6 +38,8 @@ export interface ExternalToolExecutionContext {
36
38
  signal?: AbortSignal;
37
39
  onUpdate?: AgentToolUpdateCallback<Record<string, unknown>>;
38
40
  contractRevision?: string;
41
+ /** Host authorization recheck immediately before remote dispatch. */
42
+ authorize?: () => void;
39
43
  validateArguments?: (args: Record<string, unknown>) => void;
40
44
  }
41
45
  export interface ExternalToolTurnContext {
@@ -8,6 +8,7 @@ export interface AgentTurnPolicy {
8
8
  takeContinuationMessage?(): string | undefined;
9
9
  /** Recheck limits at dispatch, including requests made by recovery paths. */
10
10
  beforeModelRequest?(signal?: AbortSignal): string | undefined;
11
+ stopReason?(): 'completed' | 'limit';
11
12
  }
12
13
  export interface AgentTurnPolicyOptions {
13
14
  maxTurns?: number;
@@ -126,7 +126,8 @@ function createDelegationBudgetPolicy(budget, options, now = Date.now) {
126
126
  const message = continuation;
127
127
  continuation = void 0;
128
128
  return message;
129
- }
129
+ },
130
+ stopReason: () => !stopReason && options.isComplete?.() ? "completed" : "limit"
130
131
  },
131
132
  addTokens(tokens) {
132
133
  usage.totalTokens += Math.max(0, tokens);
@@ -26,6 +26,11 @@ export interface RetryTransientTurnOptions {
26
26
  baseDelayMs?: number;
27
27
  signal?: AbortSignal;
28
28
  conversationId: string;
29
+ onRetry?: () => void;
30
+ shouldRetry?: () => boolean;
31
+ budget?: {
32
+ continues: number;
33
+ };
29
34
  log: {
30
35
  warn: (obj: Record<string, unknown>, msg: string) => void;
31
36
  };
@@ -1,5 +1,6 @@
1
1
  import { isTransientProviderErrorMessage } from "../../providers/model-response.js";
2
2
  import { isContextOverflowError } from "./context-overflow.js";
3
+ import { canResumeAfterModelFailure } from "./turn-recovery.js";
3
4
  //#region src/agent/orchestration/llm-turn-retry.ts
4
5
  function isTransientLlmErrorMessage(message) {
5
6
  return isTransientProviderErrorMessage(message);
@@ -72,9 +73,9 @@ async function waitForRetryDelay(delayMs, signal) {
72
73
  */
73
74
  async function maybeRetryTurnAfterTransientLlmFailure(agent, options) {
74
75
  const maxContinues = options.maxContinues ?? 2;
75
- const baseDelayMs = Math.max(0, options.baseDelayMs ?? 300);
76
- let continues = 0;
77
- while (continues < maxContinues) {
76
+ const baseDelayMs = Math.max(0, options.baseDelayMs ?? 1e3);
77
+ const budget = options.budget ?? { continues: 0 };
78
+ while (budget.continues < maxContinues && options.shouldRetry?.() !== false) {
78
79
  const last = getLastAssistantMessage(agent.state.messages);
79
80
  if (!last) return;
80
81
  if (last.stopReason !== "error") return;
@@ -88,8 +89,15 @@ async function maybeRetryTurnAfterTransientLlmFailure(agent, options) {
88
89
  }, "Assistant turn ended with error (not retrying as transient)");
89
90
  return;
90
91
  }
91
- continues += 1;
92
- const retryDelayMs = baseDelayMs * 2 ** (continues - 1);
92
+ if (!canResumeAfterModelFailure(agent.state.messages)) {
93
+ options.log.warn({ conversationId: options.conversationId }, "Model recovery stopped because a tool outcome is unconfirmed");
94
+ return;
95
+ }
96
+ const continues = ++budget.continues;
97
+ const rateLimited = /429|rate.limit|too many requests/i.test(errMsg);
98
+ const serverDelay = /retry[- ]after\s*[:=]?\s*(\d+(?:\.\d+)?)\s*(ms|s|seconds)?/i.exec(errMsg);
99
+ const requestedDelay = serverDelay ? Number(serverDelay[1]) * (serverDelay[2] === "ms" ? 1 : 1e3) : 0;
100
+ const retryDelayMs = Math.min(6e4, Math.max(requestedDelay, (rateLimited && options.baseDelayMs === void 0 ? 5e3 : baseDelayMs) * 2 ** (continues - 1)));
93
101
  options.log.warn({
94
102
  conversationId: options.conversationId,
95
103
  errorMessage: errMsg,
@@ -99,6 +107,8 @@ async function maybeRetryTurnAfterTransientLlmFailure(agent, options) {
99
107
  retryDelayMs
100
108
  }, "LLM request failed with a transient network error; retrying the same turn. If this persists, check outbound HTTPS to the provider API and HTTP(S)_PROXY.");
101
109
  await waitForRetryDelay(retryDelayMs, options.signal);
110
+ options.signal?.throwIfAborted();
111
+ options.onRetry?.();
102
112
  const trimmed = stripTrailingErrorAssistantMessages(agent.state.messages);
103
113
  agent.state.messages = trimmed;
104
114
  await agent.continue();
@@ -0,0 +1,6 @@
1
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
2
+ /** A model request can be resumed only after every issued tool has a confirmed result. */
3
+ export declare function canResumeAfterModelFailure(rows: readonly unknown[]): boolean;
4
+ export declare function recoveryToolKey(name: string, args: unknown): string;
5
+ /** Prevent a recovering model from replaying an already confirmed operation with a new call ID. */
6
+ export declare function completedRecoveryToolResult(messages: readonly AgentMessage[], name: string, args: unknown): string | undefined;
@@ -0,0 +1,29 @@
1
+ //#region src/agent/orchestration/turn-recovery.ts
2
+ /** A model request can be resumed only after every issued tool has a confirmed result. */
3
+ function canResumeAfterModelFailure(rows) {
4
+ const messages = rows.filter((row) => !!row && typeof row === "object");
5
+ const pending = /* @__PURE__ */ new Set();
6
+ for (const message of messages.slice(Math.max(0, messages.findLastIndex((item) => item.role === "user")))) if (message.role === "assistant") {
7
+ if (message.stopReason === "aborted") return false;
8
+ if (message.stopReason === "error") continue;
9
+ for (const block of Array.isArray(message.content) ? message.content : []) if (block?.type === "toolCall" && typeof block.id === "string") pending.add(block.id);
10
+ } else if (message.role === "toolResult") {
11
+ const recordedReplay = Array.isArray(message.content) && message.content.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith("This operation already completed before recovery."));
12
+ if (message.isError && !recordedReplay) return false;
13
+ if (typeof message.toolCallId === "string") pending.delete(message.toolCallId);
14
+ }
15
+ return pending.size === 0;
16
+ }
17
+ function recoveryToolKey(name, args) {
18
+ return JSON.stringify([name, args], (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
19
+ }
20
+ /** Prevent a recovering model from replaying an already confirmed operation with a new call ID. */
21
+ function completedRecoveryToolResult(messages, name, args) {
22
+ const calls = /* @__PURE__ */ new Map();
23
+ const key = recoveryToolKey(name, args);
24
+ for (const message of messages.slice(Math.max(0, messages.findLastIndex((item) => item.role === "user")))) if (message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted") {
25
+ for (const block of message.content) if (block.type === "toolCall") calls.set(block.id, recoveryToolKey(block.name, block.arguments));
26
+ } else if (message.role === "toolResult" && !message.isError && calls.get(message.toolCallId) === key) return `This operation already completed before recovery. Do not execute it again. Use its recorded result and continue the remaining work:\n${JSON.stringify(message.content).slice(0, 8e3)}`;
27
+ }
28
+ //#endregion
29
+ export { canResumeAfterModelFailure, completedRecoveryToolResult, recoveryToolKey };