@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.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BSel9yf8.js → agent-avatar-dicebear-Cb9eaXxR.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-DgHMyhuY.js → agent-defaults-settings-panel-DEV-xtgm.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-plugin-dialog-BosuN4dO.js → agent-plugin-dialog-DXKc3Luj.js} +1 -1
- package/dist/gateway/static/root/assets/{agents-B-pw5RKI.js → agents-DcNxwpJQ.js} +3 -3
- package/dist/gateway/static/root/assets/{app-management-settings-panel-DuOqKkfH.js → app-management-settings-panel-X5i0Tz7_.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-8JF9rXFr.js → appearance-settings-CmHHzquT.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-Cuclpk0A.js → apps-page-ChFA4Zp0.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-D7NwNy2I.js → automations-page-BFqsIqOI.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-workspace-CSxW4wX0.js → automations-workspace-Blfak-fT.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Dnb7CH_l.js → block-editor-CpOLG3Co.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automations-page-GBgDe21o.js → browser-automations-page-BSbTJ9j2.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-settings-page-B2FXaO3n.js → browser-settings-page-BUlILJrB.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-page-f2-MquJn.js → capabilities-page-CeRdkwRf.js} +2 -2
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-D9F0mw16.js → capabilities-settings-panel-S-cKsGja.js} +1 -1
- package/dist/gateway/static/root/assets/{capability-header-actions-DVRxJPQP.js → capability-header-actions-6w8pQhnn.js} +1 -1
- package/dist/gateway/static/root/assets/{channels-CnDm5T1t.js → channels-DfNTGBmp.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-BxSCDbK7.js → chat-terminal-dock-DnoQHpDc.js} +1 -1
- package/dist/gateway/static/root/assets/{computer-settings-page-DVAB-kQw.js → computer-settings-page-7yliE7sG.js} +1 -1
- package/dist/gateway/static/root/assets/{connector-service-page-D2EeLXei.js → connector-service-page-DOLAUFSW.js} +1 -1
- package/dist/gateway/static/root/assets/{connectors-page-DK9n4hgZ.js → connectors-page-DAsn97XG.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-Cl9KfFj8.js → date-picker-DNDqr4Fk.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-Ei6aX5WM.js → dependency-picker-B5oG7RgE.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-4Evl4RKX.js → desktop-pet-K7kUCX1O.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-DAD_OWcX.js → desktop-pet-settings-KgOAuqSh.js} +1 -1
- package/dist/gateway/static/root/assets/{device-pairing-wizard-BA21epMn.js → device-pairing-wizard-DvqniZDm.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-B_Zb5nNR.js → directory-picker-path-field-BmOguLDl.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-8gWSThWP.js → extension-page-DSSuMQxp.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-CunUbB09.js → extension-settings-page-DSd__mNd.js} +1 -1
- package/dist/gateway/static/root/assets/{fallback-models-BSAfzXby.js → fallback-models-D7bjbiwZ.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-CN-2229W.js → gateway-settings-hSXGWumL.js} +1 -1
- package/dist/gateway/static/root/assets/{highlighted-body-KQOG7T2V-DktBCtHF.js → highlighted-body-KQOG7T2V-C7xJwE61.js} +1 -1
- package/dist/gateway/static/root/assets/{home-page-CUJcNW3H.js → home-page-DJNmlMfH.js} +1 -1
- package/dist/gateway/static/root/assets/{imports-page-DQm1MNNC.js → imports-page-BufZPzGE.js} +1 -1
- package/dist/gateway/static/root/assets/index-DBRwvWzm.css +1 -0
- package/dist/gateway/static/root/assets/{index-BPNL54bv.js → index-K8SXDtxr.js} +9 -9
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-DaC5YjuJ.js → keyboard-shortcuts-settings-B8pPP5RF.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-DwQyyo7a.js → local-app-workbench-page-zEtvE7pJ.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-CpZhivpf.js → local-apps-page-Bli7e_eg.js} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-Db43nh1M.js → logs-page-BimZk3R4.js} +1 -1
- package/dist/gateway/static/root/assets/{management-settings-DrKejBEZ.js → management-settings-5Y4COyU9.js} +1 -1
- package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-DBRj4-1-.js +1 -0
- package/dist/gateway/static/root/assets/{note-detail-panel-a8NSwmmW.js → note-detail-panel-KLDPLVg7.js} +3 -3
- package/dist/gateway/static/root/assets/{notes-page-BKRke43F.js → notes-page-DST0oPXD.js} +1 -1
- package/dist/gateway/static/root/assets/{page-context-capture-button-DUaMkmxv.js → page-context-capture-button-Bmed83-6.js} +1 -1
- package/dist/gateway/static/root/assets/{personal-page-BqF-HHw1.js → personal-page-BG4Ystrm.js} +1 -1
- package/dist/gateway/static/root/assets/{product-open-page-GrhtTQIz.js → product-open-page-CXkYP9xE.js} +1 -1
- package/dist/gateway/static/root/assets/{product-preview-content-zdP7gN0y.js → product-preview-content-DjmupQSq.js} +1 -1
- package/dist/gateway/static/root/assets/{project-detail-page-9Vd_AqDW.js → project-detail-page-CzBmV0P6.js} +1 -1
- package/dist/gateway/static/root/assets/{projects-page-DGunhjoM.js → projects-page-DByqe5Em.js} +1 -1
- package/dist/gateway/static/root/assets/{remote-access-hub-BoRsF3Lx.js → remote-access-hub-DTjj-odF.js} +1 -1
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-W0HDm--J.js → runtime-tools-settings-panel-B_ptn3T8.js} +1 -1
- package/dist/gateway/static/root/assets/{scenes-page-CYsFOfVJ.js → scenes-page-wkua4l5y.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CBxEbBQq.js → schema-form-VvOSM84Y.js} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-Dthfx_vc.js → sessions-page-D06XwYAT.js} +1 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-DoAHy1wj.js +1 -0
- package/dist/gateway/static/root/assets/{settings-page-CFs-WP_2.js → settings-page-C-6BJdb1.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-D2-9fXYL.js → setup-status-panel-L0JSG0K-.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-DTb88RHD.js → shares-settings-BbgAUM3v.js} +1 -1
- package/dist/gateway/static/root/assets/{skill-api-DJ8UBsxq.js → skill-api-BFU1tPyw.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page-DjbBOikT.js → skills-page-DZw6el4o.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-Dt5GkLhM.js → system-settings-panel-DshSGWNg.js} +1 -1
- package/dist/gateway/static/root/assets/{task-detail-page-88EShcbs.js → task-detail-page-CfZhq4XZ.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-JHttXi63.js → tunnel-api-DFDXwWnU.js} +1 -1
- package/dist/gateway/static/root/assets/{usage-page-B3rUa0fW.js → usage-page-2RJLg54s.js} +1 -1
- package/dist/gateway/static/root/assets/{user-model-page-Cq1VSsZw.js → user-model-page-y1gjGYPI.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-BAtG_JHv.js → work-discovery-overlay-Ch-KfpCF.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-Cw12NFgx.js → workflow-run-setup-panel-CTu297Hc.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-kR5ouOuM.js → workflows-page-CRZXG_rm.js} +1 -1
- package/dist/gateway/static/root/assets/{xopc-cloud-account-card-BTBVKpSg.js → xopc-cloud-account-card-hEfjVjnc.js} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/agent/child-agent-factory.js +7 -1
- package/dist/src/agent/embedded/execution-tool-policy.d.ts +1 -1
- package/dist/src/agent/embedded/execution-tool-policy.js +3 -2
- package/dist/src/agent/embedded/run-for-session.js +32 -12
- package/dist/src/agent/embedded/run-turn.js +108 -24
- package/dist/src/agent/embedded/session-runner.js +9 -1
- package/dist/src/agent/embedded/types.d.ts +2 -1
- package/dist/src/agent/external-tools/cliProvider.d.ts +1 -0
- package/dist/src/agent/external-tools/cliProvider.js +1 -0
- package/dist/src/agent/external-tools/composio-provider.js +2 -0
- package/dist/src/agent/external-tools/gateway-tools.js +8 -2
- package/dist/src/agent/external-tools/service.d.ts +1 -0
- package/dist/src/agent/external-tools/service.js +3 -1
- package/dist/src/agent/external-tools/types.d.ts +4 -0
- package/dist/src/agent/orchestration/agent-turn-policy.d.ts +1 -0
- package/dist/src/agent/orchestration/delegation-budget.js +2 -1
- package/dist/src/agent/orchestration/llm-turn-retry.d.ts +5 -0
- package/dist/src/agent/orchestration/llm-turn-retry.js +15 -5
- package/dist/src/agent/orchestration/turn-recovery.d.ts +6 -0
- package/dist/src/agent/orchestration/turn-recovery.js +29 -0
- package/dist/src/agent/service/direct-turn-helpers.js +6 -5
- package/dist/src/agent/service/process-direct-streaming.js +7 -1
- package/dist/src/agent/tasks/task-judge-service.js +28 -10
- package/dist/src/agent/tools/factory.js +10 -1
- package/dist/src/agent/tools/index.d.ts +1 -0
- package/dist/src/agent/tools/index.js +2 -1
- package/dist/src/agent/tools/personal-request-result-tool.js +20 -3
- package/dist/src/agent/tools/personal-request-tool.js +162 -15
- package/dist/src/agent/tools/personal-task-tool.d.ts +5 -1
- package/dist/src/agent/tools/personal-task-tool.js +137 -15
- package/dist/src/agent/tools/task-collaboration-tool.d.ts +16 -0
- package/dist/src/agent/tools/task-collaboration-tool.js +153 -0
- package/dist/src/agent/tools/xopc-use-tool.js +1 -1
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/cli/commands/connectors.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/connectors/connection-recovery-service.d.ts +6 -0
- package/dist/src/connectors/connection-recovery-service.js +151 -32
- package/dist/src/extensions/sdk/index.js +1 -1
- package/dist/src/gateway/hono/routes/connection-waits.js +3 -1
- package/dist/src/gateway/service/run-gateway-agent.js +9 -5
- package/dist/src/gateway/service/session-input-coordinator.js +4 -2
- package/dist/src/gateway/service.js +9 -9
- package/dist/src/personal-agent/prompts.js +6 -2
- package/dist/src/personal-agent/reply-composer.js +23 -9
- package/dist/src/personal-agent/request-access.d.ts +3 -0
- package/dist/src/personal-agent/request-access.js +31 -0
- package/dist/src/personal-agent/request-delivery.js +9 -4
- package/dist/src/personal-agent/request-repository.d.ts +1 -0
- package/dist/src/personal-agent/request-service.d.ts +2 -0
- package/dist/src/personal-agent/request-service.js +32 -7
- package/dist/src/providers/model-response.js +13 -2
- package/dist/src/session/store.js +19 -6
- package/dist/src/storage/sqlite/connection-wait-repository.d.ts +2 -0
- package/dist/src/storage/sqlite/connection-wait-repository.js +83 -4
- package/dist/src/storage/sqlite/transcript-repository.d.ts +2 -0
- package/dist/src/storage/sqlite/transcript-repository.js +25 -1
- package/dist/src/tasks/task-application-service.js +1 -1
- package/dist/src/tasks/task-collaboration-delivery.js +4 -1
- package/dist/src/tasks/task-collaboration-repository.d.ts +3 -0
- package/dist/src/tasks/task-collaboration-repository.js +11 -1
- package/dist/src/tasks/task-context-assembler.js +13 -5
- package/dist/src/tasks/task-continuation.d.ts +5 -0
- package/dist/src/tasks/task-continuation.js +32 -0
- package/dist/src/tasks/task-main-update-delivery.js +13 -8
- package/dist/src/tasks/task-read-model-projector.js +1 -1
- package/dist/src/tasks/task-run-coordinator.d.ts +1 -0
- package/dist/src/tasks/task-run-coordinator.js +21 -6
- package/dist/src/tasks/task-run-dispatcher.js +11 -0
- package/dist/src/tasks/task-run-repository.d.ts +8 -0
- package/dist/src/tasks/task-run-repository.js +32 -1
- package/package.json +5 -5
- package/dist/gateway/static/root/assets/index-yseLAt9D.css +0 -1
- package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-C4VzqE9z.js +0 -1
- 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:
|
|
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[
|
|
572
|
-
if (checkpoint?.type === "custom" && (
|
|
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.
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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:
|
|
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: [
|
|
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;
|
|
@@ -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 ? {
|
|
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 ??
|
|
76
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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 };
|