@qduc/term2 0.32.2 → 0.33.0
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/build-info.json +2 -2
- package/dist/components/menu/ModelSelectionMenu.d.ts.map +1 -1
- package/dist/components/menu/ModelSelectionMenu.js +12 -14
- package/dist/components/menu/ModelSelectionMenu.js.map +1 -1
- package/dist/lib/agent-chat-service.d.ts.map +1 -1
- package/dist/lib/agent-chat-service.js +2 -6
- package/dist/lib/agent-chat-service.js.map +1 -1
- package/dist/lib/agent-client.d.ts +2 -1
- package/dist/lib/agent-client.d.ts.map +1 -1
- package/dist/lib/agent-client.js +71 -84
- package/dist/lib/agent-client.js.map +1 -1
- package/dist/lib/agent-factory.d.ts.map +1 -1
- package/dist/lib/agent-factory.js +17 -19
- package/dist/lib/agent-factory.js.map +1 -1
- package/dist/prompts/tool-surface-guidance.js +1 -1
- package/dist/prompts/tool-surface-guidance.js.map +1 -1
- package/dist/prompts/tool-surface-guidance.ts +1 -1
- package/dist/providers/ai-sdk-anthropic.provider.d.ts.map +1 -1
- package/dist/providers/ai-sdk-anthropic.provider.js +16 -2
- package/dist/providers/ai-sdk-anthropic.provider.js.map +1 -1
- package/dist/providers/ai-sdk-google.provider.d.ts.map +1 -1
- package/dist/providers/ai-sdk-google.provider.js +16 -2
- package/dist/providers/ai-sdk-google.provider.js.map +1 -1
- package/dist/providers/ai-sdk-openrouter.provider.d.ts.map +1 -1
- package/dist/providers/ai-sdk-openrouter.provider.js +16 -1
- package/dist/providers/ai-sdk-openrouter.provider.js.map +1 -1
- package/dist/providers/ai-sdk-provider-settings.d.ts +2 -0
- package/dist/providers/ai-sdk-provider-settings.d.ts.map +1 -1
- package/dist/providers/ai-sdk-provider-settings.js +5 -0
- package/dist/providers/ai-sdk-provider-settings.js.map +1 -1
- package/dist/providers/ai-sdk-streamed-model.d.ts +3 -3
- package/dist/providers/ai-sdk-streamed-model.d.ts.map +1 -1
- package/dist/providers/ai-sdk-streamed-model.js +5 -69
- package/dist/providers/ai-sdk-streamed-model.js.map +1 -1
- package/dist/providers/anthropic-chat-caching.d.ts +5 -0
- package/dist/providers/anthropic-chat-caching.d.ts.map +1 -0
- package/dist/providers/{common/openai-compatible-messages.js → anthropic-chat-caching.js} +19 -1
- package/dist/providers/anthropic-chat-caching.js.map +1 -0
- package/dist/providers/codex.provider.js +2 -0
- package/dist/providers/codex.provider.js.map +1 -1
- package/dist/providers/custom-provider-runtime.d.ts +18 -0
- package/dist/providers/custom-provider-runtime.d.ts.map +1 -0
- package/dist/providers/custom-provider-runtime.js +26 -0
- package/dist/providers/custom-provider-runtime.js.map +1 -0
- package/dist/providers/llama-cpp.provider.d.ts +3 -0
- package/dist/providers/llama-cpp.provider.d.ts.map +1 -1
- package/dist/providers/llama-cpp.provider.js +17 -0
- package/dist/providers/llama-cpp.provider.js.map +1 -1
- package/dist/providers/neuralwatt.provider.d.ts +6 -0
- package/dist/providers/neuralwatt.provider.d.ts.map +1 -0
- package/dist/providers/neuralwatt.provider.js +8 -0
- package/dist/providers/neuralwatt.provider.js.map +1 -0
- package/dist/providers/openai-compatible-lazy.d.ts.map +1 -1
- package/dist/providers/openai-compatible-lazy.js +2 -0
- package/dist/providers/openai-compatible-lazy.js.map +1 -1
- package/dist/providers/openai-compatible-middleware.d.ts +4 -9
- package/dist/providers/openai-compatible-middleware.d.ts.map +1 -1
- package/dist/providers/openai-compatible-middleware.js +25 -48
- package/dist/providers/openai-compatible-middleware.js.map +1 -1
- package/dist/providers/openai-compatible-response-normalizer.d.ts +1 -1
- package/dist/providers/openai-compatible-response-normalizer.d.ts.map +1 -1
- package/dist/providers/openai-compatible-response-normalizer.js +6 -6
- package/dist/providers/openai-compatible-response-normalizer.js.map +1 -1
- package/dist/providers/openai-compatible.provider.d.ts +2 -32
- package/dist/providers/openai-compatible.provider.d.ts.map +1 -1
- package/dist/providers/openai-compatible.provider.js +18 -144
- package/dist/providers/openai-compatible.provider.js.map +1 -1
- package/dist/providers/openai.provider.d.ts.map +1 -1
- package/dist/providers/openai.provider.js +1 -0
- package/dist/providers/openai.provider.js.map +1 -1
- package/dist/providers/opencode-middleware.d.ts +9 -0
- package/dist/providers/opencode-middleware.d.ts.map +1 -0
- package/dist/providers/opencode-middleware.js +15 -0
- package/dist/providers/opencode-middleware.js.map +1 -0
- package/dist/providers/opencode-model-provider.d.ts +19 -0
- package/dist/providers/opencode-model-provider.d.ts.map +1 -0
- package/dist/providers/opencode-model-provider.js +128 -0
- package/dist/providers/opencode-model-provider.js.map +1 -0
- package/dist/providers/opencode.provider.d.ts +2 -0
- package/dist/providers/opencode.provider.d.ts.map +1 -1
- package/dist/providers/opencode.provider.js +4 -0
- package/dist/providers/opencode.provider.js.map +1 -1
- package/dist/providers/openrouter-decision-models.d.ts +7 -0
- package/dist/providers/openrouter-decision-models.d.ts.map +1 -0
- package/dist/providers/openrouter-decision-models.js +31 -0
- package/dist/providers/openrouter-decision-models.js.map +1 -0
- package/dist/providers/openrouter.provider.js +2 -1
- package/dist/providers/openrouter.provider.js.map +1 -1
- package/dist/providers/orcarouter-models.d.ts +12 -0
- package/dist/providers/orcarouter-models.d.ts.map +1 -0
- package/dist/providers/orcarouter-models.js +34 -0
- package/dist/providers/orcarouter-models.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/services/agent-runtime/application-run-loop.d.ts +20 -1
- package/dist/services/agent-runtime/application-run-loop.d.ts.map +1 -1
- package/dist/services/agent-runtime/application-run-loop.js +871 -759
- package/dist/services/agent-runtime/application-run-loop.js.map +1 -1
- package/dist/services/agent-runtime/tool-call-execution.d.ts +1 -1
- package/dist/services/agent-runtime/tool-call-execution.d.ts.map +1 -1
- package/dist/services/agent-runtime/tool-call-execution.js +6 -3
- package/dist/services/agent-runtime/tool-call-execution.js.map +1 -1
- package/dist/services/approval/approval-flow-coordinator.d.ts +4 -2
- package/dist/services/approval/approval-flow-coordinator.d.ts.map +1 -1
- package/dist/services/approval/approval-flow-coordinator.js +2 -1
- package/dist/services/approval/approval-flow-coordinator.js.map +1 -1
- package/dist/services/approval/approval-state.d.ts +0 -13
- package/dist/services/approval/approval-state.d.ts.map +1 -1
- package/dist/services/approval/approval-state.js +0 -5
- package/dist/services/approval/approval-state.js.map +1 -1
- package/dist/services/approval/background-subagent-approval-controller.d.ts.map +1 -1
- package/dist/services/approval/background-subagent-approval-controller.js +0 -2
- package/dist/services/approval/background-subagent-approval-controller.js.map +1 -1
- package/dist/services/approval/nested-approval-owner.d.ts.map +1 -1
- package/dist/services/approval/nested-approval-owner.js +5 -1
- package/dist/services/approval/nested-approval-owner.js.map +1 -1
- package/dist/services/approval/shell-auto-approval-resolver.d.ts +2 -2
- package/dist/services/approval/shell-auto-approval-resolver.d.ts.map +1 -1
- package/dist/services/approval/shell-auto-approval-resolver.js +2 -2
- package/dist/services/approval/shell-auto-approval-resolver.js.map +1 -1
- package/dist/services/approval/tool-approval-batch-coordinator.d.ts +2 -2
- package/dist/services/approval/tool-approval-batch-coordinator.d.ts.map +1 -1
- package/dist/services/approval/tool-approval-batch-coordinator.js +4 -4
- package/dist/services/approval/tool-approval-batch-coordinator.js.map +1 -1
- package/dist/services/conversation/conversation-adapter.d.ts +17 -47
- package/dist/services/conversation/conversation-adapter.d.ts.map +1 -1
- package/dist/services/conversation/conversation-adapter.js +114 -88
- package/dist/services/conversation/conversation-adapter.js.map +1 -1
- package/dist/services/conversation/conversation-orchestrator.d.ts.map +1 -1
- package/dist/services/conversation/conversation-orchestrator.js +48 -121
- package/dist/services/conversation/conversation-orchestrator.js.map +1 -1
- package/dist/services/conversation/conversation-record-stream.d.ts +21 -0
- package/dist/services/conversation/conversation-record-stream.d.ts.map +1 -0
- package/dist/services/conversation/conversation-record-stream.js +186 -0
- package/dist/services/conversation/conversation-record-stream.js.map +1 -0
- package/dist/services/conversation/conversation-record.d.ts +160 -0
- package/dist/services/conversation/conversation-record.d.ts.map +1 -0
- package/dist/services/conversation/conversation-record.js +371 -0
- package/dist/services/conversation/conversation-record.js.map +1 -0
- package/dist/services/conversation/conversation-result-builder.d.ts +0 -4
- package/dist/services/conversation/conversation-result-builder.d.ts.map +1 -1
- package/dist/services/conversation/conversation-result-builder.js +0 -10
- package/dist/services/conversation/conversation-result-builder.js.map +1 -1
- package/dist/services/conversation/conversation-service.d.ts +2 -1
- package/dist/services/conversation/conversation-service.d.ts.map +1 -1
- package/dist/services/conversation/conversation-service.js +3 -0
- package/dist/services/conversation/conversation-service.js.map +1 -1
- package/dist/services/conversation-agent-client.d.ts +17 -7
- package/dist/services/conversation-agent-client.d.ts.map +1 -1
- package/dist/services/logging/conversation-logger.d.ts +1 -7
- package/dist/services/logging/conversation-logger.d.ts.map +1 -1
- package/dist/services/logging/conversation-logger.js +0 -18
- package/dist/services/logging/conversation-logger.js.map +1 -1
- package/dist/services/model-service.d.ts +2 -6
- package/dist/services/model-service.d.ts.map +1 -1
- package/dist/services/model-service.js +8 -21
- package/dist/services/model-service.js.map +1 -1
- package/dist/services/models/decision-model-listing.d.ts +2 -12
- package/dist/services/models/decision-model-listing.d.ts.map +1 -1
- package/dist/services/models/decision-model-listing.js +2 -29
- package/dist/services/models/decision-model-listing.js.map +1 -1
- package/dist/services/provider-continuity.d.ts +26 -0
- package/dist/services/provider-continuity.d.ts.map +1 -1
- package/dist/services/provider-continuity.js +47 -0
- package/dist/services/provider-continuity.js.map +1 -1
- package/dist/services/retry/recovery-executor.d.ts +3 -4
- package/dist/services/retry/recovery-executor.d.ts.map +1 -1
- package/dist/services/retry/recovery-executor.js +13 -75
- package/dist/services/retry/recovery-executor.js.map +1 -1
- package/dist/services/retry/retry-contracts.d.ts +2 -10
- package/dist/services/retry/retry-contracts.d.ts.map +1 -1
- package/dist/services/session/continuation-plan-applier.d.ts +5 -9
- package/dist/services/session/continuation-plan-applier.d.ts.map +1 -1
- package/dist/services/session/continuation-plan-applier.js +9 -43
- package/dist/services/session/continuation-plan-applier.js.map +1 -1
- package/dist/services/session/continuation-recovery-handler.d.ts +4 -6
- package/dist/services/session/continuation-recovery-handler.d.ts.map +1 -1
- package/dist/services/session/continuation-recovery-handler.js +3 -15
- package/dist/services/session/continuation-recovery-handler.js.map +1 -1
- package/dist/services/session/initial-input-preparer.d.ts +2 -2
- package/dist/services/session/initial-input-preparer.d.ts.map +1 -1
- package/dist/services/session/initial-input-preparer.js +3 -3
- package/dist/services/session/initial-input-preparer.js.map +1 -1
- package/dist/services/session/initial-turn-recovery-handler.d.ts +2 -4
- package/dist/services/session/initial-turn-recovery-handler.d.ts.map +1 -1
- package/dist/services/session/initial-turn-recovery-handler.js +4 -5
- package/dist/services/session/initial-turn-recovery-handler.js.map +1 -1
- package/dist/services/session/live-run.d.ts +9 -3
- package/dist/services/session/live-run.d.ts.map +1 -1
- package/dist/services/session/live-run.js +19 -4
- package/dist/services/session/live-run.js.map +1 -1
- package/dist/services/session/session-composition.d.ts +8 -12
- package/dist/services/session/session-composition.d.ts.map +1 -1
- package/dist/services/session/session-composition.js +41 -87
- package/dist/services/session/session-composition.js.map +1 -1
- package/dist/services/session/session-input-planner.d.ts +2 -2
- package/dist/services/session/session-input-planner.d.ts.map +1 -1
- package/dist/services/session/session-input-planner.js +6 -13
- package/dist/services/session/session-input-planner.js.map +1 -1
- package/dist/services/session/session-lifecycle.d.ts +73 -23
- package/dist/services/session/session-lifecycle.d.ts.map +1 -1
- package/dist/services/session/session-lifecycle.js +127 -45
- package/dist/services/session/session-lifecycle.js.map +1 -1
- package/dist/services/session/session-stream-processor.d.ts +4 -30
- package/dist/services/session/session-stream-processor.d.ts.map +1 -1
- package/dist/services/session/session-stream-processor.js +11 -300
- package/dist/services/session/session-stream-processor.js.map +1 -1
- package/dist/services/session/session-tool-tracker.d.ts +9 -144
- package/dist/services/session/session-tool-tracker.d.ts.map +1 -1
- package/dist/services/session/session-tool-tracker.js +17 -251
- package/dist/services/session/session-tool-tracker.js.map +1 -1
- package/dist/services/session/{turn-workflow.d.ts → session-turn-adapter.d.ts} +10 -43
- package/dist/services/session/session-turn-adapter.d.ts.map +1 -0
- package/dist/services/session/session-turn-adapter.js +734 -0
- package/dist/services/session/session-turn-adapter.js.map +1 -0
- package/dist/services/session/turn-attempt-factory.d.ts +2 -11
- package/dist/services/session/turn-attempt-factory.d.ts.map +1 -1
- package/dist/services/session/turn-attempt-factory.js +4 -12
- package/dist/services/session/turn-attempt-factory.js.map +1 -1
- package/dist/services/session/turn-attempt.d.ts +0 -3
- package/dist/services/session/turn-attempt.d.ts.map +1 -1
- package/dist/services/session/turn-attempt.js +0 -5
- package/dist/services/session/turn-attempt.js.map +1 -1
- package/dist/services/session/turn-coordinator.d.ts +3 -3
- package/dist/services/session/turn-coordinator.d.ts.map +1 -1
- package/dist/services/session/turn-coordinator.js +31 -26
- package/dist/services/session/turn-coordinator.js.map +1 -1
- package/dist/services/session/turn-projection-state.d.ts +45 -0
- package/dist/services/session/turn-projection-state.d.ts.map +1 -0
- package/dist/services/session/turn-projection-state.js +61 -0
- package/dist/services/session/turn-projection-state.js.map +1 -0
- package/dist/services/subagents/tool-policy.d.ts +5 -5
- package/dist/services/subagents/tool-policy.d.ts.map +1 -1
- package/dist/services/subagents/tool-policy.js.map +1 -1
- package/dist/tools/system/run-code/run-code-runtime.d.ts.map +1 -1
- package/dist/tools/system/run-code/run-code-runtime.js +90 -19
- package/dist/tools/system/run-code/run-code-runtime.js.map +1 -1
- package/dist/tools/system/shell.d.ts +2 -2
- package/dist/tools/system/shell.d.ts.map +1 -1
- package/dist/tools/system/shell.js +1 -0
- package/dist/tools/system/shell.js.map +1 -1
- package/dist/tools/tool-names.d.ts +1 -1
- package/dist/tools/tool-names.d.ts.map +1 -1
- package/dist/utils/ai/token-usage.js +9 -8
- package/dist/utils/ai/token-usage.js.map +1 -1
- package/package.json +1 -1
- package/dist/providers/anthropic-middleware.d.ts +0 -25
- package/dist/providers/anthropic-middleware.d.ts.map +0 -1
- package/dist/providers/anthropic-middleware.js +0 -32
- package/dist/providers/anthropic-middleware.js.map +0 -1
- package/dist/providers/common/openai-compatible-messages.d.ts +0 -2
- package/dist/providers/common/openai-compatible-messages.d.ts.map +0 -1
- package/dist/providers/common/openai-compatible-messages.js.map +0 -1
- package/dist/services/session/continuation-call-id-resolver.d.ts +0 -29
- package/dist/services/session/continuation-call-id-resolver.d.ts.map +0 -1
- package/dist/services/session/continuation-call-id-resolver.js +0 -69
- package/dist/services/session/continuation-call-id-resolver.js.map +0 -1
- package/dist/services/session/continuation-state.d.ts +0 -69
- package/dist/services/session/continuation-state.d.ts.map +0 -1
- package/dist/services/session/continuation-state.js +0 -78
- package/dist/services/session/continuation-state.js.map +0 -1
- package/dist/services/session/session-manager.d.ts +0 -87
- package/dist/services/session/session-manager.d.ts.map +0 -1
- package/dist/services/session/session-manager.js +0 -146
- package/dist/services/session/session-manager.js.map +0 -1
- package/dist/services/session/turn-workflow.d.ts.map +0 -1
- package/dist/services/session/turn-workflow.js +0 -1055
- package/dist/services/session/turn-workflow.js.map +0 -1
|
@@ -46,11 +46,16 @@ export class MaxTurnsExceededError extends Error {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
class EventQueue {
|
|
49
|
+
acceptBeforePublish;
|
|
50
|
+
constructor(acceptBeforePublish) {
|
|
51
|
+
this.acceptBeforePublish = acceptBeforePublish;
|
|
52
|
+
}
|
|
49
53
|
#items = [];
|
|
50
54
|
#waiters = [];
|
|
51
55
|
#closed = false;
|
|
52
56
|
#failure;
|
|
53
57
|
push(item) {
|
|
58
|
+
this.acceptBeforePublish?.(item);
|
|
54
59
|
const waiter = this.#waiters.shift();
|
|
55
60
|
if (waiter)
|
|
56
61
|
waiter.resolve({ done: false, value: item });
|
|
@@ -98,6 +103,7 @@ export class ApplicationRunLoop {
|
|
|
98
103
|
#contextCompactionSessionState;
|
|
99
104
|
#turnInputMailbox;
|
|
100
105
|
#activeAbortController = null;
|
|
106
|
+
#activeTurnController = null;
|
|
101
107
|
#segmentGeneration = 0;
|
|
102
108
|
/** The run whose budget an out-of-band grant applies to, held across a pause. */
|
|
103
109
|
#activeRunBudgetState;
|
|
@@ -143,6 +149,7 @@ export class ApplicationRunLoop {
|
|
|
143
149
|
this.#turnInputMailbox.closeTurn();
|
|
144
150
|
}
|
|
145
151
|
abort() {
|
|
152
|
+
this.#activeTurnController?.abort();
|
|
146
153
|
this.abortSegment();
|
|
147
154
|
// An aborted turn will not resume, so nothing may keep waiting on it.
|
|
148
155
|
this.#turnInputMailbox.abortTurn();
|
|
@@ -192,7 +199,6 @@ export class ApplicationRunLoop {
|
|
|
192
199
|
#createToolCallExecution() {
|
|
193
200
|
return new ToolCallExecution({
|
|
194
201
|
toolLifecycle: this.#deps.toolLifecycle,
|
|
195
|
-
getOnToolDispatch: this.#deps.getOnToolDispatch,
|
|
196
202
|
resolveMaxParallelToolCalls: this.#deps.resolveMaxParallelToolCalls,
|
|
197
203
|
isMaxTurnsExceeded: (error) => error instanceof MaxTurnsExceededError,
|
|
198
204
|
logDiagnostic: this.#deps.logDiagnostic,
|
|
@@ -256,12 +262,9 @@ export class ApplicationRunLoop {
|
|
|
256
262
|
* the tool results of the round that was in flight when they typed.
|
|
257
263
|
*/
|
|
258
264
|
startStream(agent, input, options = {}) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// With one, this call may equally be the same turn restarting after a
|
|
263
|
-
// retry, which the loop cannot tell apart — so `openTurn`/`closeTurn` own
|
|
264
|
-
// the decision instead.
|
|
265
|
+
return this.#run(this.#createState(agent, input, options), options);
|
|
266
|
+
}
|
|
267
|
+
#createState(agent, input, options) {
|
|
265
268
|
this.#turnInputMailbox.startStream();
|
|
266
269
|
const state = {
|
|
267
270
|
agent,
|
|
@@ -329,24 +332,31 @@ export class ApplicationRunLoop {
|
|
|
329
332
|
state.approvalDecisionInterruption = interruption;
|
|
330
333
|
state.approvalMessage = approvalOptions?.message;
|
|
331
334
|
};
|
|
332
|
-
return
|
|
335
|
+
return state;
|
|
336
|
+
}
|
|
337
|
+
/** Root turns use the same loop; session operations never dispatch a run. */
|
|
338
|
+
async runTurn(session) {
|
|
339
|
+
const controller = new AbortController();
|
|
340
|
+
this.#activeTurnController = controller;
|
|
341
|
+
try {
|
|
342
|
+
await this.#runTurn(undefined, session, controller.signal);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
if (this.#activeTurnController === controller)
|
|
346
|
+
this.#activeTurnController = null;
|
|
347
|
+
}
|
|
333
348
|
}
|
|
334
349
|
continueRunStream(handle, options = {}) {
|
|
335
350
|
const state = unwrapContinuationHandle(handle);
|
|
336
351
|
if (!state || typeof state !== 'object' || !('agent' in state) || !('input' in state)) {
|
|
337
352
|
throw new Error('Invalid application continuation state');
|
|
338
353
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (!state.toolExecution)
|
|
346
|
-
state.toolExecution = this.#createToolCallExecution();
|
|
347
|
-
// Handles created before cost accounting existed have no record list.
|
|
348
|
-
if (!state.costRecords)
|
|
349
|
-
state.costRecords = [];
|
|
354
|
+
if (!state.live?.resume)
|
|
355
|
+
throw new Error('Application turn is not waiting for continuation');
|
|
356
|
+
this.#refreshContinuation(state, options);
|
|
357
|
+
return this.#run(state, options);
|
|
358
|
+
}
|
|
359
|
+
#refreshContinuation(state, options) {
|
|
350
360
|
if (!state.runBudget && options.runBudget) {
|
|
351
361
|
state.runBudget = new RunBudget(options.runBudget);
|
|
352
362
|
}
|
|
@@ -382,13 +392,7 @@ export class ApplicationRunLoop {
|
|
|
382
392
|
state.turnCount = 0;
|
|
383
393
|
if (state.maxTurns === undefined)
|
|
384
394
|
state.maxTurns = options.maxTurns;
|
|
385
|
-
|
|
386
|
-
state.supportsConversationChaining = options.supportsConversationChaining === true;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
// AgentClient supplies the current provider capability on every resume.
|
|
390
|
-
state.supportsConversationChaining = options.supportsConversationChaining === true;
|
|
391
|
-
}
|
|
395
|
+
state.supportsConversationChaining = options.supportsConversationChaining === true;
|
|
392
396
|
// A continuation receives a fresh immutable snapshot from the session;
|
|
393
397
|
// refresh the preparation closure when supplied, while preserving the
|
|
394
398
|
// root closure for callers that do not provide one again.
|
|
@@ -409,12 +413,12 @@ export class ApplicationRunLoop {
|
|
|
409
413
|
state.turnId = options.turnId;
|
|
410
414
|
if (options.hookScope !== undefined)
|
|
411
415
|
state.hookScope = options.hookScope;
|
|
412
|
-
return this.#run(state, options);
|
|
413
416
|
}
|
|
414
|
-
#
|
|
415
|
-
const queue = new EventQueue();
|
|
417
|
+
#createView(state, options, acceptEvent) {
|
|
418
|
+
const queue = new EventQueue(acceptEvent);
|
|
416
419
|
const segmentGeneration = ++this.#segmentGeneration;
|
|
417
|
-
|
|
420
|
+
if (!state.live)
|
|
421
|
+
this.abortSegment();
|
|
418
422
|
const controller = new AbortController();
|
|
419
423
|
this.#activeAbortController = controller;
|
|
420
424
|
this.#turnInputMailbox.startSegment();
|
|
@@ -465,46 +469,137 @@ export class ApplicationRunLoop {
|
|
|
465
469
|
return state.costRecords ?? [];
|
|
466
470
|
},
|
|
467
471
|
});
|
|
468
|
-
let
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
472
|
+
let resolveSegment;
|
|
473
|
+
let rejectSegment;
|
|
474
|
+
stream.completed = new Promise((resolve, reject) => {
|
|
475
|
+
resolveSegment = resolve;
|
|
476
|
+
rejectSegment = reject;
|
|
477
|
+
});
|
|
478
|
+
// The iterator may report a failure before the caller observes completed.
|
|
479
|
+
void stream.completed.catch(() => undefined);
|
|
480
|
+
const current = {
|
|
481
|
+
state,
|
|
482
|
+
stream,
|
|
483
|
+
queue,
|
|
484
|
+
options: effectiveOptions,
|
|
485
|
+
toolContext,
|
|
486
|
+
requestAbortController: controller,
|
|
487
|
+
generation: segmentGeneration,
|
|
488
|
+
resolveSegment,
|
|
489
|
+
rejectSegment,
|
|
490
|
+
};
|
|
491
|
+
this.#activeRunBudgetState = state.runBudget ? state : undefined;
|
|
492
|
+
return current;
|
|
493
|
+
}
|
|
494
|
+
#run(state, options) {
|
|
495
|
+
const current = this.#createView(state, options);
|
|
496
|
+
if (state.live) {
|
|
497
|
+
const resume = state.live.resume;
|
|
498
|
+
Object.assign(state.live, current, { resume: undefined });
|
|
499
|
+
resume();
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
state.live = current;
|
|
503
|
+
void this.#runTurn(current).then((value) => this.#settleSegment(current, value), (error) => this.#settleSegment(current, undefined, error));
|
|
504
|
+
}
|
|
505
|
+
return current.stream;
|
|
506
|
+
}
|
|
507
|
+
#observe(context) {
|
|
508
|
+
context.session.beginSegment(context.stream);
|
|
509
|
+
context.observation = context.session.consume(context.stream);
|
|
510
|
+
void context.observation.catch(() => undefined);
|
|
511
|
+
}
|
|
512
|
+
async #completeTurn(context) {
|
|
513
|
+
const value = finish(context.stream, context.state, context.queue);
|
|
514
|
+
if (context.session) {
|
|
515
|
+
this.#settleSegment(context, value);
|
|
516
|
+
await context.observation;
|
|
517
|
+
await context.session.completed(context.stream);
|
|
518
|
+
}
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
/** Complete an event view; a pause leaves the owning async task alive. */
|
|
522
|
+
#settleSegment(context, value, error, waiting = false) {
|
|
523
|
+
const { state, stream, queue, generation } = context;
|
|
524
|
+
if (error === undefined)
|
|
525
|
+
context.session?.acceptSegment(stream);
|
|
526
|
+
state.runBudget?.pause();
|
|
527
|
+
const cancelled = error instanceof Error && error.name === 'AbortError';
|
|
528
|
+
const paused = (waiting || context.resume !== undefined) && !cancelled && error === undefined;
|
|
529
|
+
if (error !== undefined) {
|
|
530
|
+
stream.cancelled = cancelled;
|
|
473
531
|
queue.close(error);
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
532
|
+
context.rejectSegment(error);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
context.resolveSegment(value);
|
|
536
|
+
}
|
|
537
|
+
if (generation !== this.#segmentGeneration)
|
|
538
|
+
return;
|
|
539
|
+
if (!paused && this.#activeAbortController === context.requestAbortController)
|
|
540
|
+
this.#activeAbortController = null;
|
|
541
|
+
this.#turnInputMailbox.settleSegment({
|
|
542
|
+
paused,
|
|
543
|
+
reason: {
|
|
544
|
+
pendingApprovals: state.toolExecution.pendingApprovals.length,
|
|
545
|
+
pendingBudgetInteraction: state.pendingRunBudgetInteraction !== undefined,
|
|
546
|
+
cancelled,
|
|
547
|
+
error: error instanceof Error ? error.name : error ? String(error) : undefined,
|
|
548
|
+
turnCount: state.turnCount,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
if (!paused && this.#activeRunBudgetState === state)
|
|
552
|
+
this.#activeRunBudgetState = undefined;
|
|
553
|
+
}
|
|
554
|
+
/** Publish a paused view and suspend here until its owner reconnects or cancels. */
|
|
555
|
+
async #waitForDecision(context) {
|
|
556
|
+
if (context.session) {
|
|
557
|
+
this.#settleSegment(context, finish(context.stream, context.state, context.queue), undefined, true);
|
|
558
|
+
await context.observation;
|
|
559
|
+
const options = await context.session.interrupted(context.stream);
|
|
560
|
+
this.#assertLive(context);
|
|
561
|
+
this.#refreshContinuation(context.state, options);
|
|
562
|
+
Object.assign(context, this.#createView(context.state, { ...options, signal: context.turnSignal }, (event) => context.session.acceptEvent(event)));
|
|
563
|
+
this.#observe(context);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const { state, stream, queue, options } = context;
|
|
567
|
+
await new Promise((resolve, reject) => {
|
|
568
|
+
const signal = options.signal;
|
|
569
|
+
const abort = () => {
|
|
570
|
+
context.resume = undefined;
|
|
571
|
+
reject(Object.assign(new Error('Operation aborted'), { name: 'AbortError' }));
|
|
572
|
+
};
|
|
573
|
+
context.resume = () => {
|
|
574
|
+
signal?.removeEventListener('abort', abort);
|
|
575
|
+
resolve();
|
|
576
|
+
};
|
|
577
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
578
|
+
if (signal?.aborted) {
|
|
579
|
+
abort();
|
|
502
580
|
return;
|
|
503
|
-
|
|
504
|
-
|
|
581
|
+
}
|
|
582
|
+
this.#settleSegment(context, finish(stream, state, queue));
|
|
505
583
|
});
|
|
506
|
-
|
|
507
|
-
|
|
584
|
+
}
|
|
585
|
+
/** A budget decision resumes this exact boundary without recounting it. */
|
|
586
|
+
async #waitForBudget(context) {
|
|
587
|
+
const { state } = context;
|
|
588
|
+
while (state.pendingRunBudgetInteraction) {
|
|
589
|
+
if (state.runBudgetInteractionDecision === 'stop') {
|
|
590
|
+
state.pendingRunBudgetInteraction = undefined;
|
|
591
|
+
state.runBudgetInteractionDecision = undefined;
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (state.runBudgetInteractionDecision === 'continue') {
|
|
595
|
+
state.pendingRunBudgetInteraction = undefined;
|
|
596
|
+
state.runBudgetInteractionDecision = undefined;
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
context.stream.interruptions = [state.pendingRunBudgetInteraction];
|
|
600
|
+
await this.#waitForDecision(context);
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
508
603
|
}
|
|
509
604
|
/** Route boundary advice through the same admission lane as mode notices. */
|
|
510
605
|
#queuePendingSystemNotice(text) {
|
|
@@ -517,741 +612,764 @@ export class ApplicationRunLoop {
|
|
|
517
612
|
outputPush(stream, queue, { type: 'item', item });
|
|
518
613
|
}, state.turnCount);
|
|
519
614
|
}
|
|
520
|
-
|
|
615
|
+
/** The single model → tools → model cycle, shared by root and nested turns. */
|
|
616
|
+
async #runTurn(context, session, turnSignal) {
|
|
521
617
|
while (true) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
state
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
if (state.toolExecution.pendingApprovals.length > 0 && state.approvalDecision) {
|
|
539
|
-
state.toolExecution.resolveApproval({ callId: state.approvalDecisionCallId, interruption: state.approvalDecisionInterruption }, state.approvalDecision, state.approvalMessage, state.approvals);
|
|
540
|
-
state.approvalDecision = undefined;
|
|
541
|
-
state.approvalDecisionCallId = undefined;
|
|
542
|
-
state.approvalDecisionInterruption = undefined;
|
|
543
|
-
state.approvalMessage = undefined;
|
|
544
|
-
await state.toolExecution.settle(this.#toolCallContext(state, stream, queue, toolContext));
|
|
545
|
-
stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
|
|
546
|
-
if (state.toolExecution.pendingApprovals.length > 0) {
|
|
547
|
-
return finish(stream, state, queue);
|
|
618
|
+
try {
|
|
619
|
+
turnSignal?.throwIfAborted();
|
|
620
|
+
if (!context) {
|
|
621
|
+
const start = await session.prepare(turnSignal);
|
|
622
|
+
turnSignal?.throwIfAborted();
|
|
623
|
+
if (!start)
|
|
624
|
+
return;
|
|
625
|
+
const options = { ...start.options, signal: turnSignal };
|
|
626
|
+
const state = this.#createState(start.agent, start.input, options);
|
|
627
|
+
this.#refreshContinuation(state, options);
|
|
628
|
+
context = this.#createView(state, options, (event) => session.acceptEvent(event));
|
|
629
|
+
context.session = session;
|
|
630
|
+
context.turnSignal = turnSignal;
|
|
631
|
+
state.live = context;
|
|
632
|
+
this.#observe(context);
|
|
548
633
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
634
|
+
this.#assertLive(context);
|
|
635
|
+
if (await this.#prepareRequest(context))
|
|
636
|
+
return await this.#completeTurn(context);
|
|
637
|
+
const response = await this.#askModel(context);
|
|
638
|
+
this.#assertLive(context);
|
|
639
|
+
const calls = this.#recordResponse(context, response);
|
|
640
|
+
if (await this.#executeTools(context, response, calls))
|
|
641
|
+
return await this.#completeTurn(context);
|
|
642
|
+
if (calls.length === 0 && this.#finishTurn(context))
|
|
643
|
+
return await this.#completeTurn(context);
|
|
555
644
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
|
|
563
|
-
return finish(stream, state, queue);
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (!session)
|
|
647
|
+
throw error;
|
|
648
|
+
if (context) {
|
|
649
|
+
this.#settleSegment(context, undefined, error);
|
|
650
|
+
await context.observation?.catch(() => undefined);
|
|
564
651
|
}
|
|
652
|
+
turnSignal?.throwIfAborted();
|
|
653
|
+
if (!(await session.recover(error)))
|
|
654
|
+
return;
|
|
655
|
+
context = undefined;
|
|
565
656
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
},
|
|
606
|
-
automaticCompactionsThisRun: state.automaticCompactionsThisRun ?? 0,
|
|
607
|
-
lastCompletedInputTokens: state.lastCompletedInputTokens,
|
|
608
|
-
signal: options.signal,
|
|
609
|
-
onStarted: (provider) => outputPush(stream, queue, { type: 'context_compaction_started', provider, strategy: 'local' }),
|
|
610
|
-
});
|
|
611
|
-
if (compaction.kind !== 'unchanged' && !costsRecordedLive && compaction.costRecords?.length) {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
#assertLive(context) {
|
|
660
|
+
if (context.options.signal?.aborted)
|
|
661
|
+
throw Object.assign(new Error('Operation aborted'), { name: 'AbortError' });
|
|
662
|
+
}
|
|
663
|
+
/** Admit input and prepare context once for the next logical model round. */
|
|
664
|
+
async #prepareRequest(context) {
|
|
665
|
+
let { state, stream, queue, options } = context;
|
|
666
|
+
// The request boundary: every tool result of the previous round is in
|
|
667
|
+
// history, and the next request has not been built. A user message
|
|
668
|
+
// admitted here reaches the model in sequence, mid-turn.
|
|
669
|
+
this.#admitPendingSteers(state, stream, queue);
|
|
670
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
671
|
+
if (await this.#waitForBudget(context))
|
|
672
|
+
return true;
|
|
673
|
+
({ state, stream, queue, options } = context);
|
|
674
|
+
const compactionHistoryResult = state.usesDeltaHistory
|
|
675
|
+
? state.compactionHistory
|
|
676
|
+
? mergeChainedCompactionHistory(state.compactionHistory, state.history)
|
|
677
|
+
: { kind: 'skipped', reason: 'missing_snapshot' }
|
|
678
|
+
: { kind: 'ready', history: state.history };
|
|
679
|
+
const boundaryDecision = options.onRequestBoundary?.(compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : state.history, (text) => this.#queuePendingSystemNotice(text), {
|
|
680
|
+
...(state.lastCompletedInputTokens !== undefined
|
|
681
|
+
? { lastCompletedInputTokens: state.lastCompletedInputTokens }
|
|
682
|
+
: {}),
|
|
683
|
+
...(compactionHistoryResult.kind === 'skipped' ? { compactionSkipReason: compactionHistoryResult.reason } : {}),
|
|
684
|
+
});
|
|
685
|
+
const compactionHistory = compactionHistoryResult.kind === 'ready' ? compactionHistoryResult.history : undefined;
|
|
686
|
+
if (options.boundaryCompaction && compactionHistory && !boundaryDecision?.deferCompaction) {
|
|
687
|
+
const compactionStartedAt = Date.now();
|
|
688
|
+
let costsRecordedLive = false;
|
|
689
|
+
const compaction = await options.boundaryCompaction.compact({
|
|
690
|
+
history: compactionHistory,
|
|
691
|
+
instructions: state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions,
|
|
692
|
+
tools: toModelTools(state.agent.tools),
|
|
693
|
+
recordCostRecords: (records) => {
|
|
694
|
+
if (records.length) {
|
|
695
|
+
costsRecordedLive = true;
|
|
612
696
|
state.costRecords ??= [];
|
|
613
|
-
state.costRecords.push(...
|
|
697
|
+
state.costRecords.push(...records);
|
|
698
|
+
for (const record of records)
|
|
699
|
+
queue.push({ type: 'cost_update', record });
|
|
700
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
614
701
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
outputPush(stream, queue, {
|
|
626
|
-
type: 'context_compaction_completed',
|
|
627
|
-
provider: state.currentProviderId ?? 'unknown',
|
|
628
|
-
strategy: 'local',
|
|
629
|
-
durationMs: Math.max(0, Date.now() - compactionStartedAt),
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
else if (compaction.kind === 'failed') {
|
|
633
|
-
outputPush(stream, queue, {
|
|
634
|
-
type: 'context_compaction_failed',
|
|
635
|
-
provider: compaction.provider,
|
|
636
|
-
strategy: 'local',
|
|
637
|
-
durationMs: Math.max(0, Date.now() - compactionStartedAt),
|
|
638
|
-
});
|
|
639
|
-
if (compaction.error)
|
|
640
|
-
throw compaction.error;
|
|
641
|
-
}
|
|
642
|
-
else if (compaction.notice && compaction.notice !== state.compactionNotice) {
|
|
643
|
-
state.compactionNotice = compaction.notice;
|
|
644
|
-
this.#queuePendingSystemNotice(compaction.notice);
|
|
645
|
-
}
|
|
646
|
-
// Summarization is an asynchronous part of this same request
|
|
647
|
-
// boundary. Admit anything that arrived while it was in flight after
|
|
648
|
-
// applying a replacement, so the steer cannot be overwritten by the
|
|
649
|
-
// checkpoint or miss a terminal model request.
|
|
650
|
-
this.#admitPendingSteers(state, stream, queue);
|
|
651
|
-
this.#evaluateRunBudget(state, stream, queue);
|
|
652
|
-
if (state.pendingRunBudgetInteraction)
|
|
653
|
-
return this.#pauseForRunBudgetInteraction(state, stream, queue);
|
|
654
|
-
}
|
|
655
|
-
this.#admitPendingSteers(state, stream, queue);
|
|
656
|
-
state.turnCount += 1;
|
|
657
|
-
this.#evaluateRunBudget(state, stream, queue);
|
|
658
|
-
if (state.pendingRunBudgetInteraction) {
|
|
659
|
-
state.requestBoundaryReady = true;
|
|
660
|
-
return this.#pauseForRunBudgetInteraction(state, stream, queue);
|
|
661
|
-
}
|
|
662
|
-
// Retain the legacy error only for callers that have not adopted staged
|
|
663
|
-
// budgets. A configured run budget turns the same count into critical
|
|
664
|
-
// evidence and lets the decision surface choose what happens next.
|
|
665
|
-
if (!state.runBudget && state.maxTurns !== undefined && state.turnCount > state.maxTurns) {
|
|
666
|
-
throw new MaxTurnsExceededError(state.maxTurns);
|
|
667
|
-
}
|
|
702
|
+
return !state.pendingRunBudgetInteraction && !state.criticalWrapUpPending;
|
|
703
|
+
},
|
|
704
|
+
automaticCompactionsThisRun: state.automaticCompactionsThisRun ?? 0,
|
|
705
|
+
lastCompletedInputTokens: state.lastCompletedInputTokens,
|
|
706
|
+
signal: options.signal,
|
|
707
|
+
onStarted: (provider) => outputPush(stream, queue, { type: 'context_compaction_started', provider, strategy: 'local' }),
|
|
708
|
+
});
|
|
709
|
+
if (compaction.kind !== 'unchanged' && !costsRecordedLive && compaction.costRecords?.length) {
|
|
710
|
+
state.costRecords ??= [];
|
|
711
|
+
state.costRecords.push(...compaction.costRecords);
|
|
668
712
|
}
|
|
669
|
-
|
|
670
|
-
state.
|
|
713
|
+
if (compaction.kind === 'compacted') {
|
|
714
|
+
state.inputUsageInvalidatedByCompaction = true;
|
|
715
|
+
state.lastCompletedInputTokens = undefined;
|
|
716
|
+
state.compactedDuringRun = true;
|
|
717
|
+
state.history.splice(0, state.history.length, ...compaction.history);
|
|
718
|
+
state.input.splice(0, state.input.length, ...normalizeApplicationInput(compaction.modelInput));
|
|
719
|
+
state.responseId = undefined;
|
|
720
|
+
state.responseProviderId = undefined;
|
|
721
|
+
state.usesDeltaHistory = false;
|
|
722
|
+
state.automaticCompactionsThisRun = (state.automaticCompactionsThisRun ?? 0) + 1;
|
|
723
|
+
outputPush(stream, queue, {
|
|
724
|
+
type: 'context_compaction_completed',
|
|
725
|
+
provider: state.currentProviderId ?? 'unknown',
|
|
726
|
+
strategy: 'local',
|
|
727
|
+
durationMs: Math.max(0, Date.now() - compactionStartedAt),
|
|
728
|
+
});
|
|
671
729
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
let activeRequestId = '';
|
|
679
|
-
let activeRequest;
|
|
680
|
-
let pendingNativeReasoning;
|
|
681
|
-
let deferredNativeReasoning = [];
|
|
682
|
-
let criticalWrapUp = false;
|
|
683
|
-
while (true) {
|
|
684
|
-
streamedToolCalls = [];
|
|
685
|
-
completion = undefined;
|
|
686
|
-
sawToolCall = false;
|
|
687
|
-
pendingNativeReasoning = undefined;
|
|
688
|
-
deferredNativeReasoning = [];
|
|
689
|
-
// Stable process-unique request id, allocated immediately before dispatch
|
|
690
|
-
// so a cost record can be settled exactly once (success or failure).
|
|
691
|
-
activeRequestId = this.#nextRequestId();
|
|
692
|
-
const requestId = activeRequestId;
|
|
693
|
-
const outputLengthBefore = stream.output.length;
|
|
694
|
-
const newItemsLengthBefore = stream.newItems.length;
|
|
695
|
-
const inputLengthBefore = state.input.length;
|
|
696
|
-
const historyLengthBefore = state.history.length;
|
|
697
|
-
const criticalWrapUpDispatchedBefore = state.criticalWrapUpDispatched;
|
|
698
|
-
let provisionalTextCharacters = 0;
|
|
699
|
-
let provisionalReasoningCharacters = 0;
|
|
700
|
-
let provisionalTextDeltas = 0;
|
|
701
|
-
let provisionalReasoningDeltas = 0;
|
|
702
|
-
let lastTextPartId;
|
|
703
|
-
criticalWrapUp = state.criticalWrapUpPending === true && state.criticalWrapUpDispatched !== true;
|
|
704
|
-
if (criticalWrapUp)
|
|
705
|
-
state.criticalWrapUpDispatched = true;
|
|
706
|
-
const disableChaining = state.disableChainingForAttempt === true;
|
|
707
|
-
if (disableChaining) {
|
|
708
|
-
state.disableChainingForAttempt = false;
|
|
709
|
-
state.responseId = undefined;
|
|
710
|
-
state.responseProviderId = undefined;
|
|
711
|
-
}
|
|
712
|
-
// A per-request signal lets a recoverable guard trip cancel only this
|
|
713
|
-
// provider request; the segment signal still cancels everything.
|
|
714
|
-
const requestSignal = new AbortController();
|
|
715
|
-
const abortRequest = () => requestSignal.abort();
|
|
716
|
-
if (options.signal?.aborted)
|
|
717
|
-
requestSignal.abort();
|
|
718
|
-
else
|
|
719
|
-
options.signal?.addEventListener('abort', abortRequest, { once: true });
|
|
720
|
-
const requestInstructions = state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions;
|
|
721
|
-
// Same synchronous step as the instructions above, so the snapshot is
|
|
722
|
-
// exactly the state the model is shown in this request.
|
|
723
|
-
state.requestSnapshot = state.agent.resolveRequestSnapshot?.();
|
|
724
|
-
const requestCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
|
|
725
|
-
const effectiveOutput = resolveRequestOutput(requestCatalog, state.agent.modelSettings?.maxTokens);
|
|
726
|
-
const request = {
|
|
727
|
-
instructions: criticalWrapUp
|
|
728
|
-
? `${requestInstructions}\n\nBudget containment is terminal. Do not call tools. In this one final response, summarize what you completed, the evidence you have, and what remains.`
|
|
729
|
-
: requestInstructions,
|
|
730
|
-
...(state.supportsConversationChaining &&
|
|
731
|
-
!disableChaining &&
|
|
732
|
-
state.responseId &&
|
|
733
|
-
state.responseProviderId !== undefined &&
|
|
734
|
-
state.responseProviderId === state.currentProviderId
|
|
735
|
-
? { previousResponseId: state.responseId }
|
|
736
|
-
: {}),
|
|
737
|
-
...(disableChaining ? { disableChaining: true } : {}),
|
|
738
|
-
input: projectModelRequestHistory(state.input),
|
|
739
|
-
tools: toModelTools(criticalWrapUp ? [] : state.agent.tools),
|
|
740
|
-
applicationTools: criticalWrapUp ? [] : state.agent.tools,
|
|
741
|
-
...(state.agent.modelSettings?.temperature !== undefined
|
|
742
|
-
? { temperature: state.agent.modelSettings.temperature }
|
|
743
|
-
: {}),
|
|
744
|
-
...(state.agent.modelSettings?.reasoning ? { reasoning: state.agent.modelSettings.reasoning } : {}),
|
|
745
|
-
...(effectiveOutput !== undefined ? { maxTokens: effectiveOutput } : {}),
|
|
746
|
-
...(state.agent.outputType !== undefined ? { outputType: state.agent.outputType } : {}),
|
|
747
|
-
...(state.agent.modelSettings?.codex ? { codex: state.agent.modelSettings.codex } : {}),
|
|
748
|
-
...(state.agent.modelSettings?.providerData
|
|
749
|
-
? {
|
|
750
|
-
providerOptions: this.#contextCompactionSessionState.disabled
|
|
751
|
-
? Object.fromEntries(Object.entries(state.agent.modelSettings.providerData).filter(([key]) => key !== 'contextCompaction'))
|
|
752
|
-
: state.agent.modelSettings.providerData,
|
|
753
|
-
}
|
|
754
|
-
: {}),
|
|
755
|
-
signal: requestSignal.signal,
|
|
756
|
-
...(state.recoveryBudget ? { recoveryBudget: state.recoveryBudget } : {}),
|
|
757
|
-
};
|
|
758
|
-
activeRequest = request;
|
|
759
|
-
const generationGuard = new GenerationGuard({
|
|
760
|
-
maxOutputCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
761
|
-
maxTextCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
762
|
-
maxReasoningCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
763
|
-
maxToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
764
|
-
maxCumulativeToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
765
|
-
requestDeadlineMs: state.agent.modelSettings?.maxModelRequestDurationMs,
|
|
766
|
-
maxStreamIdleMs: state.agent.modelSettings?.maxModelStreamIdleMs,
|
|
767
|
-
...options.generationGuard,
|
|
730
|
+
else if (compaction.kind === 'failed') {
|
|
731
|
+
outputPush(stream, queue, {
|
|
732
|
+
type: 'context_compaction_failed',
|
|
733
|
+
provider: compaction.provider,
|
|
734
|
+
strategy: 'local',
|
|
735
|
+
durationMs: Math.max(0, Date.now() - compactionStartedAt),
|
|
768
736
|
});
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
737
|
+
if (compaction.error)
|
|
738
|
+
throw compaction.error;
|
|
739
|
+
}
|
|
740
|
+
else if (compaction.notice && compaction.notice !== state.compactionNotice) {
|
|
741
|
+
state.compactionNotice = compaction.notice;
|
|
742
|
+
this.#queuePendingSystemNotice(compaction.notice);
|
|
743
|
+
}
|
|
744
|
+
// Summarization is an asynchronous part of this same request
|
|
745
|
+
// boundary. Admit anything that arrived while it was in flight after
|
|
746
|
+
// applying a replacement, so the steer cannot be overwritten by the
|
|
747
|
+
// checkpoint or miss a terminal model request.
|
|
748
|
+
this.#admitPendingSteers(state, stream, queue);
|
|
749
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
750
|
+
if (await this.#waitForBudget(context))
|
|
751
|
+
return true;
|
|
752
|
+
({ state, stream, queue, options } = context);
|
|
753
|
+
}
|
|
754
|
+
this.#admitPendingSteers(state, stream, queue);
|
|
755
|
+
state.turnCount += 1;
|
|
756
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
757
|
+
if (await this.#waitForBudget(context))
|
|
758
|
+
return true;
|
|
759
|
+
({ state, stream, queue, options } = context);
|
|
760
|
+
// Retain the legacy error only for callers that have not adopted staged
|
|
761
|
+
// budgets. A configured run budget turns the same count into critical
|
|
762
|
+
// evidence and lets the decision surface choose what happens next.
|
|
763
|
+
if (!state.runBudget && state.maxTurns !== undefined && state.turnCount > state.maxTurns) {
|
|
764
|
+
throw new MaxTurnsExceededError(state.maxTurns);
|
|
765
|
+
}
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
/** Own streaming and safe request attempts; never dispatch tools here. */
|
|
769
|
+
async #askModel(context) {
|
|
770
|
+
const { state, stream, queue, options, requestAbortController } = context;
|
|
771
|
+
const model = await this.#deps.resolveModel(state.agent.model);
|
|
772
|
+
const maxRetries = state.agent.modelSettings?.retry?.maxRetries ?? 0;
|
|
773
|
+
let attempt = 0;
|
|
774
|
+
let streamedToolCalls = [];
|
|
775
|
+
let completion;
|
|
776
|
+
let sawToolCall = false;
|
|
777
|
+
let activeRequestId = '';
|
|
778
|
+
let activeRequest;
|
|
779
|
+
let pendingNativeReasoning;
|
|
780
|
+
let deferredNativeReasoning = [];
|
|
781
|
+
let criticalWrapUp = false;
|
|
782
|
+
while (true) {
|
|
783
|
+
streamedToolCalls = [];
|
|
784
|
+
completion = undefined;
|
|
785
|
+
sawToolCall = false;
|
|
786
|
+
pendingNativeReasoning = undefined;
|
|
787
|
+
deferredNativeReasoning = [];
|
|
788
|
+
// Stable process-unique request id, allocated immediately before dispatch
|
|
789
|
+
// so a cost record can be settled exactly once (success or failure).
|
|
790
|
+
activeRequestId = this.#nextRequestId();
|
|
791
|
+
const requestId = activeRequestId;
|
|
792
|
+
const outputLengthBefore = stream.output.length;
|
|
793
|
+
const newItemsLengthBefore = stream.newItems.length;
|
|
794
|
+
const inputLengthBefore = state.input.length;
|
|
795
|
+
const historyLengthBefore = state.history.length;
|
|
796
|
+
const criticalWrapUpDispatchedBefore = state.criticalWrapUpDispatched;
|
|
797
|
+
let provisionalTextCharacters = 0;
|
|
798
|
+
let provisionalReasoningCharacters = 0;
|
|
799
|
+
let provisionalTextDeltas = 0;
|
|
800
|
+
let provisionalReasoningDeltas = 0;
|
|
801
|
+
let lastTextPartId;
|
|
802
|
+
criticalWrapUp = state.criticalWrapUpPending === true && state.criticalWrapUpDispatched !== true;
|
|
803
|
+
if (criticalWrapUp)
|
|
804
|
+
state.criticalWrapUpDispatched = true;
|
|
805
|
+
const disableChaining = state.disableChainingForAttempt === true;
|
|
806
|
+
if (disableChaining) {
|
|
807
|
+
state.disableChainingForAttempt = false;
|
|
808
|
+
state.responseId = undefined;
|
|
809
|
+
state.responseProviderId = undefined;
|
|
810
|
+
}
|
|
811
|
+
// A per-request signal lets a recoverable guard trip cancel only this
|
|
812
|
+
// provider request; the segment signal still cancels everything.
|
|
813
|
+
const requestSignal = new AbortController();
|
|
814
|
+
const abortRequest = () => requestSignal.abort();
|
|
815
|
+
if (options.signal?.aborted)
|
|
816
|
+
requestSignal.abort();
|
|
817
|
+
else
|
|
818
|
+
options.signal?.addEventListener('abort', abortRequest, { once: true });
|
|
819
|
+
const requestInstructions = state.agent.resolveInstructionsForRequest?.() ?? state.agent.instructions;
|
|
820
|
+
// Same synchronous step as the instructions above, so the snapshot is
|
|
821
|
+
// exactly the state the model is shown in this request.
|
|
822
|
+
state.requestSnapshot = state.agent.resolveRequestSnapshot?.();
|
|
823
|
+
const requestCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
|
|
824
|
+
const effectiveOutput = resolveRequestOutput(requestCatalog, state.agent.modelSettings?.maxTokens);
|
|
825
|
+
const request = {
|
|
826
|
+
instructions: criticalWrapUp
|
|
827
|
+
? `${requestInstructions}\n\nBudget containment is terminal. Do not call tools. In this one final response, summarize what you completed, the evidence you have, and what remains.`
|
|
828
|
+
: requestInstructions,
|
|
829
|
+
...(state.supportsConversationChaining &&
|
|
830
|
+
!disableChaining &&
|
|
831
|
+
state.responseId &&
|
|
832
|
+
state.responseProviderId !== undefined &&
|
|
833
|
+
state.responseProviderId === state.currentProviderId
|
|
834
|
+
? { previousResponseId: state.responseId }
|
|
835
|
+
: {}),
|
|
836
|
+
...(disableChaining ? { disableChaining: true } : {}),
|
|
837
|
+
input: projectModelRequestHistory(state.input),
|
|
838
|
+
tools: toModelTools(criticalWrapUp ? [] : state.agent.tools),
|
|
839
|
+
applicationTools: criticalWrapUp ? [] : state.agent.tools,
|
|
840
|
+
...(state.agent.modelSettings?.temperature !== undefined
|
|
841
|
+
? { temperature: state.agent.modelSettings.temperature }
|
|
842
|
+
: {}),
|
|
843
|
+
...(state.agent.modelSettings?.reasoning ? { reasoning: state.agent.modelSettings.reasoning } : {}),
|
|
844
|
+
...(effectiveOutput !== undefined ? { maxTokens: effectiveOutput } : {}),
|
|
845
|
+
...(state.agent.outputType !== undefined ? { outputType: state.agent.outputType } : {}),
|
|
846
|
+
...(state.agent.modelSettings?.codex ? { codex: state.agent.modelSettings.codex } : {}),
|
|
847
|
+
...(state.agent.modelSettings?.providerData
|
|
848
|
+
? {
|
|
849
|
+
providerOptions: this.#contextCompactionSessionState.disabled
|
|
850
|
+
? Object.fromEntries(Object.entries(state.agent.modelSettings.providerData).filter(([key]) => key !== 'contextCompaction'))
|
|
851
|
+
: state.agent.modelSettings.providerData,
|
|
856
852
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
: state.lastCompletedInputTokens,
|
|
883
|
-
chainedContextKnown: !needsChainedSnapshot || fullContext?.kind === 'ready',
|
|
884
|
-
});
|
|
885
|
-
await consume();
|
|
886
|
-
};
|
|
853
|
+
: {}),
|
|
854
|
+
signal: requestSignal.signal,
|
|
855
|
+
...(state.recoveryBudget ? { recoveryBudget: state.recoveryBudget } : {}),
|
|
856
|
+
};
|
|
857
|
+
activeRequest = request;
|
|
858
|
+
const generationGuard = new GenerationGuard({
|
|
859
|
+
maxOutputCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
860
|
+
maxTextCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
861
|
+
maxReasoningCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
862
|
+
maxToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
863
|
+
maxCumulativeToolArgumentCharacters: state.agent.modelSettings?.maxStreamOutputChars,
|
|
864
|
+
requestDeadlineMs: state.agent.modelSettings?.maxModelRequestDurationMs,
|
|
865
|
+
maxStreamIdleMs: state.agent.modelSettings?.maxModelStreamIdleMs,
|
|
866
|
+
...options.generationGuard,
|
|
867
|
+
});
|
|
868
|
+
const consume = async () => {
|
|
869
|
+
const deadline = new GenerationStreamDeadlines({ totalMs: generationGuard.requestDeadlineMs, idleMs: generationGuard.maxStreamIdleMs }, () => requestAbortController.abort(), () => generationGuard.progress);
|
|
870
|
+
const toolArgumentRunaway = isLunaChainedRequest(state, request)
|
|
871
|
+
? new ToolArgumentRunawayGuard({
|
|
872
|
+
...DEFAULT_TOOL_ARGUMENT_RUNAWAY_SIGNATURE,
|
|
873
|
+
timeoutMs: generationGuard.toolArgumentRunawayMs,
|
|
874
|
+
}, () => requestAbortController.abort())
|
|
875
|
+
: undefined;
|
|
876
|
+
let iterator;
|
|
877
|
+
let iteratorFinished = false;
|
|
887
878
|
try {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
await
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (error instanceof RequestInputLimitError) {
|
|
896
|
-
this.#deps.logDiagnostic?.('Request input admission refused', {
|
|
897
|
-
code: error.code,
|
|
898
|
-
action: error.action,
|
|
899
|
-
reason: error.reason,
|
|
900
|
-
guardClass: 'admission',
|
|
901
|
-
setting: 'agent.maxRequestInputTokens',
|
|
902
|
-
executionScope: state.hookScope ?? 'root',
|
|
903
|
-
recovery: 'retained_work',
|
|
904
|
-
limit: error.limit,
|
|
905
|
-
estimatedTokens: error.estimatedTokens,
|
|
906
|
-
observedTokens: error.observedTokens,
|
|
907
|
-
capacity: error.capacity,
|
|
908
|
-
outputReserve: error.outputReserve,
|
|
909
|
-
estimationReserve: error.estimationReserve,
|
|
910
|
-
});
|
|
911
|
-
throw error;
|
|
912
|
-
}
|
|
913
|
-
if (isProviderContextOverflow(error)) {
|
|
914
|
-
this.#observeTerminalFailure(error, {
|
|
915
|
-
requestId: activeRequestId,
|
|
916
|
-
provider: state.currentProviderId,
|
|
917
|
-
model: state.agent.model,
|
|
918
|
-
tier: resolveServiceTier(request),
|
|
919
|
-
});
|
|
920
|
-
throw new ProviderContextOverflowError(error);
|
|
921
|
-
}
|
|
922
|
-
const recoverOversizedToolArgument = isOversizedToolArgumentTrip(error) &&
|
|
923
|
-
(state.oversizedToolArgumentRecoveries ?? 0) < MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES &&
|
|
924
|
-
!options.signal?.aborted;
|
|
925
|
-
if (recoverOversizedToolArgument)
|
|
926
|
-
requestSignal.abort();
|
|
927
|
-
else if (error instanceof GenerationGuardError)
|
|
928
|
-
requestAbortController.abort();
|
|
929
|
-
const decision = classifyInLoopModelRetry(error, attempt, maxRetries, Math.random, {
|
|
930
|
-
previousResponseId: activeRequest?.previousResponseId,
|
|
931
|
-
});
|
|
932
|
-
const rollbackProvisionalAttempt = () => {
|
|
933
|
-
stream.output.splice(outputLengthBefore);
|
|
934
|
-
if (stream.newItems !== stream.output) {
|
|
935
|
-
stream.newItems.splice(newItemsLengthBefore);
|
|
879
|
+
iterator = model.stream(request)[Symbol.asyncIterator]();
|
|
880
|
+
while (true) {
|
|
881
|
+
const nextOperation = iterator.next();
|
|
882
|
+
const next = await deadline.wait(toolArgumentRunaway ? toolArgumentRunaway.wait(nextOperation) : nextOperation);
|
|
883
|
+
if (next.done) {
|
|
884
|
+
iteratorFinished = true;
|
|
885
|
+
return;
|
|
936
886
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
887
|
+
// Any streamed event is transport activity: re-arm the inactivity
|
|
888
|
+
// window so long legitimate reasoning is never mistaken for a stall.
|
|
889
|
+
deadline.recordActivity();
|
|
890
|
+
const event = next.value;
|
|
891
|
+
if (event.type === 'completion') {
|
|
892
|
+
toolArgumentRunaway?.observeToolCallCompleted();
|
|
893
|
+
generationGuard.observeCompletion(event.output);
|
|
894
|
+
completion = event;
|
|
895
|
+
continue;
|
|
945
896
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
897
|
+
if (event.type === 'text_delta') {
|
|
898
|
+
const startsNewPart = event.partId !== undefined &&
|
|
899
|
+
lastTextPartId !== undefined &&
|
|
900
|
+
event.partId !== lastTextPartId &&
|
|
901
|
+
event.text.length > 0;
|
|
902
|
+
if (event.partId !== undefined && event.text.length > 0)
|
|
903
|
+
lastTextPartId = event.partId;
|
|
904
|
+
const text = startsNewPart ? `${ASSISTANT_TEXT_PART_SEPARATOR}${event.text}` : event.text;
|
|
905
|
+
toolArgumentRunaway?.observeText();
|
|
906
|
+
generationGuard.observeText(text);
|
|
907
|
+
provisionalTextCharacters += text.length;
|
|
908
|
+
provisionalTextDeltas++;
|
|
909
|
+
outputPush(stream, queue, { type: 'text_delta', text });
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (event.type === 'codex_rate_limits') {
|
|
913
|
+
outputPush(stream, queue, { type: 'codex_rate_limits', rateLimits: event.rateLimits });
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (event.type === 'reasoning_delta') {
|
|
917
|
+
const accepted = generationGuard.observeReasoning(event.text);
|
|
918
|
+
if (!accepted)
|
|
919
|
+
continue;
|
|
920
|
+
const forwarded = accepted === event.text ? event : { ...event, text: accepted };
|
|
921
|
+
pendingNativeReasoning = appendNativeReasoning(pendingNativeReasoning, forwarded);
|
|
922
|
+
provisionalReasoningCharacters += accepted.length;
|
|
923
|
+
provisionalReasoningDeltas++;
|
|
924
|
+
outputPush(stream, queue, { type: 'reasoning_delta', text: accepted });
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (event.type === 'tool_call_streaming_delta') {
|
|
928
|
+
toolArgumentRunaway?.observeToolArgumentProgress(event.argumentCharCount);
|
|
929
|
+
generationGuard.observeToolArgumentProgress(event.argumentCharCount);
|
|
930
|
+
outputPush(stream, queue, event);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (event.type === 'context_compaction_started' || event.type === 'context_compaction_completed') {
|
|
934
|
+
outputPush(stream, queue, event);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (event.type === 'tool_call') {
|
|
938
|
+
toolArgumentRunaway?.observeToolCallCompleted();
|
|
939
|
+
generationGuard.observeToolCall(event.arguments);
|
|
940
|
+
// The authoritative completion may introduce a native
|
|
941
|
+
// replacement boundary before this call's reasoning.
|
|
942
|
+
if (pendingNativeReasoning)
|
|
943
|
+
deferredNativeReasoning.push(pendingNativeReasoning);
|
|
944
|
+
pendingNativeReasoning = undefined;
|
|
945
|
+
sawToolCall = true;
|
|
946
|
+
streamedToolCalls.push(event);
|
|
974
947
|
}
|
|
975
|
-
this.#deps.logDiagnostic?.('Retrying model request in run loop', {
|
|
976
|
-
attempt,
|
|
977
|
-
maxRetries,
|
|
978
|
-
kind: decision.kind,
|
|
979
|
-
delayMs: decision.delayMs,
|
|
980
|
-
error: describeError(error),
|
|
981
|
-
});
|
|
982
|
-
await (this.#deps.waitBeforeModelRetry ?? sleepWithAbort)(decision.delayMs, options.signal);
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
// Session recovery owns full-history rebuild for a chained delta.
|
|
986
|
-
// Retract this cycle's provisional tokens so a later chain_recovery
|
|
987
|
-
// does not append a second answer after a half-sentence.
|
|
988
|
-
if (!decision.retryable && decision.reason === 'chained_delta_not_self_contained') {
|
|
989
|
-
rollbackProvisionalAttempt();
|
|
990
948
|
}
|
|
991
|
-
// Dispatch began, but no terminal completion was accepted. Record an
|
|
992
|
-
// unpriced marker so the summary stays honest (partial) rather than
|
|
993
|
-
// appearing exact. Observational only: the error still propagates.
|
|
994
|
-
const cancelled = error instanceof Error && error.name === 'AbortError';
|
|
995
|
-
const record = this.#appendCostRecord(state, {
|
|
996
|
-
requestId,
|
|
997
|
-
provider: state.currentProviderId,
|
|
998
|
-
model: state.agent.model,
|
|
999
|
-
tier: resolveServiceTier(request),
|
|
1000
|
-
outcome: cancelled ? 'cancelled' : 'failed',
|
|
1001
|
-
});
|
|
1002
|
-
// Only the live queue sees cost records: `output`/`newItems` feed
|
|
1003
|
-
// persistence and replay, and a cost event must never become a
|
|
1004
|
-
// restored history item.
|
|
1005
|
-
queue.push({ type: 'cost_update', record });
|
|
1006
|
-
this.#evaluateRunBudget(state, stream, queue);
|
|
1007
|
-
this.#observeTerminalFailure(error, {
|
|
1008
|
-
requestId,
|
|
1009
|
-
provider: state.currentProviderId,
|
|
1010
|
-
model: state.agent.model,
|
|
1011
|
-
tier: resolveServiceTier(request),
|
|
1012
|
-
});
|
|
1013
|
-
throw error;
|
|
1014
949
|
}
|
|
1015
950
|
finally {
|
|
1016
|
-
|
|
951
|
+
deadline.dispose();
|
|
952
|
+
toolArgumentRunaway?.dispose();
|
|
953
|
+
if (iterator && !iteratorFinished)
|
|
954
|
+
void iterator.return?.().catch(() => undefined);
|
|
1017
955
|
}
|
|
956
|
+
};
|
|
957
|
+
const dispatch = async () => {
|
|
958
|
+
state.requestPreparation?.prepare(request);
|
|
959
|
+
// A response ID can be established after a self-contained request.
|
|
960
|
+
// It does not make this run's full history a delta needing a snapshot.
|
|
961
|
+
const needsChainedSnapshot = Boolean(request.previousResponseId && state.usesDeltaHistory);
|
|
962
|
+
const fullContext = needsChainedSnapshot && state.compactionHistory
|
|
963
|
+
? mergeChainedCompactionHistory(state.compactionHistory, state.history)
|
|
964
|
+
: undefined;
|
|
965
|
+
const contextCatalog = getCatalogModel(state.currentProviderId ?? 'openai', state.agent.model);
|
|
966
|
+
enforceRequestInputLimit({
|
|
967
|
+
contextWindow: contextCatalog?.contextWindow,
|
|
968
|
+
maxOutputTokens: request.maxTokens,
|
|
969
|
+
limit: state.requestInputLimit ? state.requestInputLimit() : state.agent.modelSettings?.maxRequestInputTokens,
|
|
970
|
+
history: fullContext?.kind === 'ready'
|
|
971
|
+
? fullContext.history
|
|
972
|
+
: needsChainedSnapshot
|
|
973
|
+
? [...(state.compactionHistory ?? []), ...request.input]
|
|
974
|
+
: request.input,
|
|
975
|
+
instructions: request.instructions ?? '',
|
|
976
|
+
tools: request.tools,
|
|
977
|
+
lastCompletedInputTokens: state.inputUsageInvalidatedByCompaction
|
|
978
|
+
? undefined
|
|
979
|
+
: state.lastCompletedInputTokens,
|
|
980
|
+
chainedContextKnown: !needsChainedSnapshot || fullContext?.kind === 'ready',
|
|
981
|
+
});
|
|
982
|
+
await consume();
|
|
983
|
+
};
|
|
984
|
+
try {
|
|
985
|
+
if (state.requestPreparation)
|
|
986
|
+
await state.requestPreparation.run(dispatch);
|
|
987
|
+
else
|
|
988
|
+
await dispatch();
|
|
989
|
+
break;
|
|
1018
990
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
991
|
+
catch (error) {
|
|
992
|
+
if (error instanceof RequestInputLimitError) {
|
|
993
|
+
this.#deps.logDiagnostic?.('Request input admission refused', {
|
|
994
|
+
code: error.code,
|
|
995
|
+
action: error.action,
|
|
996
|
+
reason: error.reason,
|
|
997
|
+
guardClass: 'admission',
|
|
998
|
+
setting: 'agent.maxRequestInputTokens',
|
|
999
|
+
executionScope: state.hookScope ?? 'root',
|
|
1000
|
+
recovery: 'retained_work',
|
|
1001
|
+
limit: error.limit,
|
|
1002
|
+
estimatedTokens: error.estimatedTokens,
|
|
1003
|
+
observedTokens: error.observedTokens,
|
|
1004
|
+
capacity: error.capacity,
|
|
1005
|
+
outputReserve: error.outputReserve,
|
|
1006
|
+
estimationReserve: error.estimationReserve,
|
|
1007
|
+
});
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
if (isProviderContextOverflow(error)) {
|
|
1027
1011
|
this.#observeTerminalFailure(error, {
|
|
1028
1012
|
requestId: activeRequestId,
|
|
1029
1013
|
provider: state.currentProviderId,
|
|
1030
1014
|
model: state.agent.model,
|
|
1031
|
-
tier:
|
|
1015
|
+
tier: resolveServiceTier(request),
|
|
1032
1016
|
});
|
|
1033
|
-
throw error;
|
|
1017
|
+
throw new ProviderContextOverflowError(error);
|
|
1034
1018
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
const finalCompletion = completion;
|
|
1051
|
-
// A terminal response ends recovery, unlike partial tokens or tool
|
|
1052
|
-
// activity. Later failures must not inherit an old episode's deadline.
|
|
1053
|
-
state.recoveryBudget?.noteModelResponseCompleted();
|
|
1054
|
-
// Commit the authoritative terminal state before handling terminal-only
|
|
1055
|
-
// tool calls. Approval may pause immediately after this point, and the
|
|
1056
|
-
// continuation must still carry the response that produced the calls.
|
|
1057
|
-
// Response IDs are provider-chain state, not generic run metadata. Keep
|
|
1058
|
-
// them out of continuation handles for providers that do not support
|
|
1059
|
-
// chaining, while still exposing the completed provider response on the
|
|
1060
|
-
// current stream for diagnostics.
|
|
1061
|
-
if (state.supportsConversationChaining && state.currentProviderId !== undefined) {
|
|
1062
|
-
state.responseId = finalCompletion.responseId;
|
|
1063
|
-
state.responseProviderId = state.currentProviderId;
|
|
1064
|
-
}
|
|
1065
|
-
else {
|
|
1066
|
-
state.responseId = undefined;
|
|
1067
|
-
state.responseProviderId = undefined;
|
|
1068
|
-
}
|
|
1069
|
-
let normalizedCompletionUsage;
|
|
1070
|
-
const upstreamProvider = extractAiSdkUpstreamProvider(finalCompletion.providerMetadata);
|
|
1071
|
-
if (finalCompletion.usage !== undefined) {
|
|
1072
|
-
normalizedCompletionUsage = normalizeModelUsage(finalCompletion.usage);
|
|
1073
|
-
if (upstreamProvider) {
|
|
1074
|
-
if (normalizedCompletionUsage) {
|
|
1075
|
-
normalizedCompletionUsage.upstream_provider = upstreamProvider;
|
|
1019
|
+
const recoverOversizedToolArgument = isOversizedToolArgumentTrip(error) &&
|
|
1020
|
+
(state.oversizedToolArgumentRecoveries ?? 0) < MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES &&
|
|
1021
|
+
!options.signal?.aborted;
|
|
1022
|
+
if (recoverOversizedToolArgument)
|
|
1023
|
+
requestSignal.abort();
|
|
1024
|
+
else if (error instanceof GenerationGuardError)
|
|
1025
|
+
requestAbortController.abort();
|
|
1026
|
+
const decision = classifyInLoopModelRetry(error, attempt, maxRetries, Math.random, {
|
|
1027
|
+
previousResponseId: activeRequest?.previousResponseId,
|
|
1028
|
+
});
|
|
1029
|
+
const rollbackProvisionalAttempt = () => {
|
|
1030
|
+
stream.output.splice(outputLengthBefore);
|
|
1031
|
+
if (stream.newItems !== stream.output) {
|
|
1032
|
+
stream.newItems.splice(newItemsLengthBefore);
|
|
1076
1033
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1034
|
+
if (provisionalTextDeltas > 0 || provisionalReasoningDeltas > 0) {
|
|
1035
|
+
queue.push({
|
|
1036
|
+
type: 'model_attempt_rollback',
|
|
1037
|
+
textCharacters: provisionalTextCharacters,
|
|
1038
|
+
reasoningCharacters: provisionalReasoningCharacters,
|
|
1039
|
+
textDeltas: provisionalTextDeltas,
|
|
1040
|
+
reasoningDeltas: provisionalReasoningDeltas,
|
|
1041
|
+
});
|
|
1079
1042
|
}
|
|
1043
|
+
};
|
|
1044
|
+
if (recoverOversizedToolArgument) {
|
|
1045
|
+
// No tool ran: a call is dispatched only after its arguments
|
|
1046
|
+
// complete. Discard the partial response and ask for smaller calls.
|
|
1047
|
+
state.oversizedToolArgumentRecoveries = (state.oversizedToolArgumentRecoveries ?? 0) + 1;
|
|
1048
|
+
state.input.splice(inputLengthBefore);
|
|
1049
|
+
state.history.splice(historyLengthBefore);
|
|
1050
|
+
state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
|
|
1051
|
+
rollbackProvisionalAttempt();
|
|
1052
|
+
this.#deps.logDiagnostic?.('Recovering oversized tool argument in run loop', {
|
|
1053
|
+
code: error.code,
|
|
1054
|
+
recovery: state.oversizedToolArgumentRecoveries,
|
|
1055
|
+
maxRecoveries: MAX_OVERSIZED_TOOL_ARGUMENT_RECOVERIES,
|
|
1056
|
+
});
|
|
1057
|
+
this.#queuePendingSystemNotice(oversizedToolArgumentNotice(error.message));
|
|
1058
|
+
this.#admitPendingSteers(state, stream, queue);
|
|
1059
|
+
continue;
|
|
1080
1060
|
}
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1061
|
+
if (decision.retryable && !options.signal?.aborted) {
|
|
1062
|
+
attempt++;
|
|
1063
|
+
state.input.splice(inputLengthBefore);
|
|
1064
|
+
state.history.splice(historyLengthBefore);
|
|
1065
|
+
state.criticalWrapUpDispatched = criticalWrapUpDispatchedBefore;
|
|
1066
|
+
rollbackProvisionalAttempt();
|
|
1067
|
+
if (decision.kind === 'chain_recovery') {
|
|
1068
|
+
state.disableChainingForAttempt = true;
|
|
1069
|
+
state.responseId = undefined;
|
|
1070
|
+
state.responseProviderId = undefined;
|
|
1085
1071
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
? { cachedInputTokens: accumulated.cache_read_tokens }
|
|
1096
|
-
: {}),
|
|
1097
|
-
...(accumulated.cache_creation_tokens !== undefined
|
|
1098
|
-
? { cacheWriteTokens: accumulated.cache_creation_tokens }
|
|
1099
|
-
: {}),
|
|
1100
|
-
};
|
|
1072
|
+
this.#deps.logDiagnostic?.('Retrying model request in run loop', {
|
|
1073
|
+
attempt,
|
|
1074
|
+
maxRetries,
|
|
1075
|
+
kind: decision.kind,
|
|
1076
|
+
delayMs: decision.delayMs,
|
|
1077
|
+
error: describeError(error),
|
|
1078
|
+
});
|
|
1079
|
+
await (this.#deps.waitBeforeModelRetry ?? sleepWithAbort)(decision.delayMs, options.signal);
|
|
1080
|
+
continue;
|
|
1101
1081
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1082
|
+
// Session recovery owns full-history rebuild for a chained delta.
|
|
1083
|
+
// Retract this cycle's provisional tokens so a later chain_recovery
|
|
1084
|
+
// does not append a second answer after a half-sentence.
|
|
1085
|
+
if (!decision.retryable && decision.reason === 'chained_delta_not_self_contained') {
|
|
1086
|
+
rollbackProvisionalAttempt();
|
|
1106
1087
|
}
|
|
1088
|
+
// Dispatch began, but no terminal completion was accepted. Record an
|
|
1089
|
+
// unpriced marker so the summary stays honest (partial) rather than
|
|
1090
|
+
// appearing exact. Observational only: the error still propagates.
|
|
1091
|
+
const cancelled = error instanceof Error && error.name === 'AbortError';
|
|
1092
|
+
const record = this.#appendCostRecord(state, {
|
|
1093
|
+
requestId,
|
|
1094
|
+
provider: state.currentProviderId,
|
|
1095
|
+
model: state.agent.model,
|
|
1096
|
+
tier: resolveServiceTier(request),
|
|
1097
|
+
outcome: cancelled ? 'cancelled' : 'failed',
|
|
1098
|
+
});
|
|
1099
|
+
// Only the live queue sees cost records: `output`/`newItems` feed
|
|
1100
|
+
// persistence and replay, and a cost event must never become a
|
|
1101
|
+
// restored history item.
|
|
1102
|
+
queue.push({ type: 'cost_update', record });
|
|
1103
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
1104
|
+
this.#observeTerminalFailure(error, {
|
|
1105
|
+
requestId,
|
|
1106
|
+
provider: state.currentProviderId,
|
|
1107
|
+
model: state.agent.model,
|
|
1108
|
+
tier: resolveServiceTier(request),
|
|
1109
|
+
});
|
|
1110
|
+
throw error;
|
|
1107
1111
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
else {
|
|
1112
|
-
normalizedCompletionUsage = undefined;
|
|
1112
|
+
finally {
|
|
1113
|
+
options.signal?.removeEventListener('abort', abortRequest);
|
|
1113
1114
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
// likewise be terminal-only, so associate it before replaying calls.
|
|
1142
|
-
const toolCalls = [...streamedToolCalls];
|
|
1143
|
-
const hasTerminalReasoning = finalCompletion.output.some((item) => item.type === 'reasoning' && appendNativeReasoning(undefined, item) !== undefined);
|
|
1144
|
-
if (hasTerminalReasoning) {
|
|
1145
|
-
// Terminal output supplies native ordering and authoritative metadata.
|
|
1146
|
-
// Streamed fragments have already been displayed, but are not history.
|
|
1147
|
-
pendingNativeReasoning = undefined;
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
streamedToolCalls,
|
|
1118
|
+
completion,
|
|
1119
|
+
sawToolCall,
|
|
1120
|
+
activeRequestId,
|
|
1121
|
+
activeRequest,
|
|
1122
|
+
pendingNativeReasoning,
|
|
1123
|
+
deferredNativeReasoning,
|
|
1124
|
+
criticalWrapUp,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/** Accept response facts before any tool from that response can execute. */
|
|
1128
|
+
#recordResponse(context, response) {
|
|
1129
|
+
const { state, stream, queue } = context;
|
|
1130
|
+
const { streamedToolCalls, completion, sawToolCall, activeRequestId, activeRequest, deferredNativeReasoning, criticalWrapUp, } = response;
|
|
1131
|
+
let { pendingNativeReasoning } = response;
|
|
1132
|
+
if (!completion) {
|
|
1133
|
+
if (streamedToolCalls.length === 0) {
|
|
1134
|
+
const error = new Error('Application model turn ended without completion');
|
|
1135
|
+
this.#observeTerminalFailure(error, {
|
|
1136
|
+
requestId: activeRequestId,
|
|
1137
|
+
provider: state.currentProviderId,
|
|
1138
|
+
model: state.agent.model,
|
|
1139
|
+
tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
|
|
1140
|
+
});
|
|
1141
|
+
throw error;
|
|
1148
1142
|
}
|
|
1149
|
-
|
|
1143
|
+
if (!criticalWrapUp) {
|
|
1150
1144
|
for (const reasoning of deferredNativeReasoning)
|
|
1151
1145
|
commitPendingNativeReasoning(state, stream, queue, reasoning);
|
|
1146
|
+
commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1152
1147
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1148
|
+
return criticalWrapUp ? [] : streamedToolCalls;
|
|
1149
|
+
}
|
|
1150
|
+
const finalCompletion = completion;
|
|
1151
|
+
// A terminal response ends recovery, unlike partial tokens or tool
|
|
1152
|
+
// activity. Later failures must not inherit an old episode's deadline.
|
|
1153
|
+
state.recoveryBudget?.noteModelResponseCompleted();
|
|
1154
|
+
// Commit the authoritative terminal state before handling terminal-only
|
|
1155
|
+
// tool calls. Approval may pause immediately after this point, and the
|
|
1156
|
+
// continuation must still carry the response that produced the calls.
|
|
1157
|
+
// Response IDs are provider-chain state, not generic run metadata. Keep
|
|
1158
|
+
// them out of continuation handles for providers that do not support
|
|
1159
|
+
// chaining, while still exposing the completed provider response on the
|
|
1160
|
+
// current stream for diagnostics.
|
|
1161
|
+
if (state.supportsConversationChaining && state.currentProviderId !== undefined) {
|
|
1162
|
+
state.responseId = finalCompletion.responseId;
|
|
1163
|
+
state.responseProviderId = state.currentProviderId;
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
state.responseId = undefined;
|
|
1167
|
+
state.responseProviderId = undefined;
|
|
1168
|
+
}
|
|
1169
|
+
let normalizedCompletionUsage;
|
|
1170
|
+
const upstreamProvider = extractAiSdkUpstreamProvider(finalCompletion.providerMetadata);
|
|
1171
|
+
if (finalCompletion.usage !== undefined) {
|
|
1172
|
+
normalizedCompletionUsage = normalizeModelUsage(finalCompletion.usage);
|
|
1173
|
+
if (upstreamProvider) {
|
|
1174
|
+
if (normalizedCompletionUsage) {
|
|
1175
|
+
normalizedCompletionUsage.upstream_provider = upstreamProvider;
|
|
1166
1176
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1170
|
-
const historyItem = {
|
|
1171
|
-
...item.item,
|
|
1172
|
-
providerOpaque: {
|
|
1173
|
-
provider: item.provider,
|
|
1174
|
-
...(item.sourceProvider ? { sourceProvider: item.sourceProvider } : {}),
|
|
1175
|
-
},
|
|
1176
|
-
};
|
|
1177
|
-
state.history.push(historyItem);
|
|
1178
|
-
state.input.push(item);
|
|
1179
|
-
if (isOpenAICompaction(item)) {
|
|
1180
|
-
// Completion usage describes the request before native replacement.
|
|
1181
|
-
// Retain billed usage/costs; invalidate only the next admission hint.
|
|
1182
|
-
state.lastCompletedInputTokens = undefined;
|
|
1183
|
-
state.inputUsageInvalidatedByCompaction = true;
|
|
1177
|
+
else {
|
|
1178
|
+
normalizedCompletionUsage = { upstream_provider: upstreamProvider };
|
|
1184
1179
|
}
|
|
1185
|
-
outputPush(stream, queue, { type: 'item', item });
|
|
1186
1180
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
content: [{ type: 'output_text', text: assistantText }],
|
|
1181
|
+
if (normalizedCompletionUsage) {
|
|
1182
|
+
if (normalizedCompletionUsage.prompt_tokens !== undefined) {
|
|
1183
|
+
state.lastCompletedInputTokens = normalizedCompletionUsage.prompt_tokens;
|
|
1184
|
+
state.inputUsageInvalidatedByCompaction = false;
|
|
1185
|
+
}
|
|
1186
|
+
const accumulated = addTokenUsage(normalizeModelUsage(state.usage), normalizedCompletionUsage);
|
|
1187
|
+
state.usage = {
|
|
1188
|
+
...(accumulated.prompt_tokens !== undefined ? { inputTokens: accumulated.prompt_tokens } : {}),
|
|
1189
|
+
...(accumulated.completion_tokens !== undefined ? { outputTokens: accumulated.completion_tokens } : {}),
|
|
1190
|
+
// `total_tokens` is the billable normalized total, including cache
|
|
1191
|
+
// creation tokens. Keep it alongside the application aliases so the
|
|
1192
|
+
// authoritative run accumulator never reconstructs a smaller total.
|
|
1193
|
+
...(accumulated.total_tokens !== undefined ? { totalTokens: accumulated.total_tokens } : {}),
|
|
1194
|
+
...(accumulated.cache_read_tokens !== undefined ? { cachedInputTokens: accumulated.cache_read_tokens } : {}),
|
|
1195
|
+
...(accumulated.cache_creation_tokens !== undefined
|
|
1196
|
+
? { cacheWriteTokens: accumulated.cache_creation_tokens }
|
|
1197
|
+
: {}),
|
|
1205
1198
|
};
|
|
1206
|
-
state.history.push(item);
|
|
1207
|
-
outputPush(stream, queue, { type: 'item', item });
|
|
1208
|
-
state.input.push({ type: 'message', role: 'assistant', content: [{ type: 'text', text: assistantText }] });
|
|
1209
1199
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
// the text stranded after its own tool result, where the assistant
|
|
1215
|
-
// message merger glued it onto the *next* turn's tool call and every
|
|
1216
|
-
// request ended with a bare assistant message. Models read that as a
|
|
1217
|
-
// turn truncated mid-sentence and restart instead of progressing.
|
|
1218
|
-
if (!state.criticalWrapUpPending && toolCalls.length > 0) {
|
|
1219
|
-
await this.#dispatchToolCalls(state, stream, queue, toolCalls, toolContext);
|
|
1200
|
+
else {
|
|
1201
|
+
// Providers may carry a future/opaque usage shape. Preserve the old
|
|
1202
|
+
// pass-through behavior when normalization cannot recognize it.
|
|
1203
|
+
state.usage = finalCompletion.usage;
|
|
1220
1204
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1205
|
+
}
|
|
1206
|
+
else if (upstreamProvider) {
|
|
1207
|
+
normalizedCompletionUsage = { upstream_provider: upstreamProvider };
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
normalizedCompletionUsage = undefined;
|
|
1211
|
+
}
|
|
1212
|
+
// Attribute cost while the dispatched request's identity is still known.
|
|
1213
|
+
// Emit it immediately so the UI can show this request's cost before the
|
|
1214
|
+
// run ends; the terminal result re-delivers the cumulative record list
|
|
1215
|
+
// and the session accumulator ignores the duplicates by request id.
|
|
1216
|
+
// Only the live queue sees the event: `output`/`newItems` feed
|
|
1217
|
+
// persistence and replay, and a cost event must never become a restored
|
|
1218
|
+
// history item.
|
|
1219
|
+
const record = this.#appendCostRecord(state, {
|
|
1220
|
+
requestId: activeRequestId,
|
|
1221
|
+
provider: state.currentProviderId,
|
|
1222
|
+
model: state.agent.model,
|
|
1223
|
+
tier: activeRequest ? resolveServiceTier(activeRequest) : 'standard',
|
|
1224
|
+
outcome: 'completed',
|
|
1225
|
+
usage: normalizedCompletionUsage,
|
|
1226
|
+
providerUsd: finalCompletion.costUsd,
|
|
1227
|
+
});
|
|
1228
|
+
queue.push({ type: 'cost_update', record });
|
|
1229
|
+
// Cost and usage arrive in the same completion metadata, so surface both
|
|
1230
|
+
// live. The footer is a per-request indicator: emit this request's own
|
|
1231
|
+
// usage, never the run accumulator in `state.usage`.
|
|
1232
|
+
if (normalizedCompletionUsage)
|
|
1233
|
+
queue.push({ type: 'usage_update', usage: normalizedCompletionUsage });
|
|
1234
|
+
this.#evaluateRunBudget(state, stream, queue);
|
|
1235
|
+
stream.lastResponseId = finalCompletion.responseId;
|
|
1236
|
+
stream.rawResponses?.push(finalCompletion);
|
|
1237
|
+
// Some provider adapters report function calls only in the terminal
|
|
1238
|
+
// completion rather than as separate stream events. Their reasoning may
|
|
1239
|
+
// likewise be terminal-only, so associate it before replaying calls.
|
|
1240
|
+
const toolCalls = [...streamedToolCalls];
|
|
1241
|
+
const hasTerminalReasoning = finalCompletion.output.some((item) => item.type === 'reasoning' && appendNativeReasoning(undefined, item) !== undefined);
|
|
1242
|
+
if (hasTerminalReasoning) {
|
|
1243
|
+
// Terminal output supplies native ordering and authoritative metadata.
|
|
1244
|
+
// Streamed fragments have already been displayed, but are not history.
|
|
1245
|
+
pendingNativeReasoning = undefined;
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
for (const reasoning of deferredNativeReasoning)
|
|
1249
|
+
commitPendingNativeReasoning(state, stream, queue, reasoning);
|
|
1250
|
+
}
|
|
1251
|
+
for (const item of finalCompletion.output) {
|
|
1252
|
+
if (item.type === 'reasoning') {
|
|
1253
|
+
// Each terminal native item is independently replayable; encrypted
|
|
1254
|
+
// reasoning cannot be concatenated or overwritten by its sibling.
|
|
1255
|
+
pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1256
|
+
pendingNativeReasoning = appendNativeReasoning(undefined, item);
|
|
1257
|
+
continue;
|
|
1228
1258
|
}
|
|
1259
|
+
if (item.type === 'tool_call') {
|
|
1260
|
+
pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1261
|
+
if (!sawToolCall)
|
|
1262
|
+
toolCalls.push(item);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (item.type !== 'provider_opaque')
|
|
1266
|
+
continue;
|
|
1267
|
+
pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1268
|
+
const historyItem = {
|
|
1269
|
+
...item.item,
|
|
1270
|
+
providerOpaque: {
|
|
1271
|
+
provider: item.provider,
|
|
1272
|
+
...(item.sourceProvider ? { sourceProvider: item.sourceProvider } : {}),
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
state.history.push(historyItem);
|
|
1276
|
+
state.input.push(item);
|
|
1277
|
+
if (isOpenAICompaction(item)) {
|
|
1278
|
+
// Completion usage describes the request before native replacement.
|
|
1279
|
+
// Retain billed usage/costs; invalidate only the next admission hint.
|
|
1280
|
+
state.lastCompletedInputTokens = undefined;
|
|
1281
|
+
state.inputUsageInvalidatedByCompaction = true;
|
|
1282
|
+
}
|
|
1283
|
+
outputPush(stream, queue, { type: 'item', item });
|
|
1284
|
+
}
|
|
1285
|
+
pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
|
|
1286
|
+
const assistantText = finalCompletion.output
|
|
1287
|
+
.filter((item) => item.type === 'message')
|
|
1288
|
+
.flatMap((item) => item.content)
|
|
1289
|
+
// Whitespace-only output (e.g. a background-notification acknowledgment)
|
|
1290
|
+
// must not enter the canonical history: the truthy check used to let
|
|
1291
|
+
// "\n\n" through, accumulating junk in model context.
|
|
1292
|
+
.map((part) => part.text)
|
|
1293
|
+
.filter((text) => text.length > 0)
|
|
1294
|
+
.join(ASSISTANT_TEXT_PART_SEPARATOR)
|
|
1295
|
+
.trim();
|
|
1296
|
+
if (assistantText) {
|
|
1297
|
+
stream.finalOutput = assistantText;
|
|
1298
|
+
const item = {
|
|
1299
|
+
type: 'message',
|
|
1300
|
+
role: 'assistant',
|
|
1301
|
+
content: [{ type: 'output_text', text: assistantText }],
|
|
1302
|
+
};
|
|
1303
|
+
state.history.push(item);
|
|
1304
|
+
outputPush(stream, queue, { type: 'item', item });
|
|
1305
|
+
state.input.push({ type: 'message', role: 'assistant', content: [{ type: 'text', text: assistantText }] });
|
|
1306
|
+
}
|
|
1307
|
+
return toolCalls;
|
|
1308
|
+
}
|
|
1309
|
+
/** Execute and settle one tool plan; approvals suspend this operation in place. */
|
|
1310
|
+
async #executeTools(context, response, calls) {
|
|
1311
|
+
let { state, stream, queue, options, toolContext } = context;
|
|
1312
|
+
if (response.criticalWrapUp)
|
|
1313
|
+
return true;
|
|
1314
|
+
if (!state.criticalWrapUpPending && calls.length > 0) {
|
|
1315
|
+
await this.#dispatchToolCalls(state, stream, queue, calls, toolContext);
|
|
1316
|
+
}
|
|
1317
|
+
if (state.toolExecution.terminateAfterExecution)
|
|
1318
|
+
return true;
|
|
1319
|
+
if (calls.length === 0)
|
|
1320
|
+
return false;
|
|
1321
|
+
while (state.pendingRunBudgetInteraction || state.toolExecution.pendingApprovals.length > 0) {
|
|
1322
|
+
if (await this.#waitForBudget(context))
|
|
1323
|
+
return true;
|
|
1324
|
+
({ state, stream, queue, options, toolContext } = context);
|
|
1325
|
+
let answeredApproval = false;
|
|
1229
1326
|
if (state.toolExecution.pendingApprovals.length > 0) {
|
|
1230
1327
|
stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
if (stopDecision) {
|
|
1241
|
-
this.#deps.logDiagnostic?.('Normal stop policy decided', { action: stopDecision.action, turnCount: state.turnCount, ...stopDecision.diagnostics }, { severity: 'debug', eventType: 'run_loop.normal_stop_policy' });
|
|
1328
|
+
await this.#waitForDecision(context);
|
|
1329
|
+
({ state, stream, queue, options, toolContext } = context);
|
|
1330
|
+
if (state.approvalDecision) {
|
|
1331
|
+
state.toolExecution.resolveApproval({ callId: state.approvalDecisionCallId, interruption: state.approvalDecisionInterruption }, state.approvalDecision, state.approvalMessage, state.approvals);
|
|
1332
|
+
state.approvalDecision = undefined;
|
|
1333
|
+
state.approvalDecisionCallId = undefined;
|
|
1334
|
+
state.approvalDecisionInterruption = undefined;
|
|
1335
|
+
state.approvalMessage = undefined;
|
|
1336
|
+
answeredApproval = true;
|
|
1242
1337
|
}
|
|
1243
|
-
if (stopDecision?.action === 'continue') {
|
|
1244
|
-
this.#queuePendingSystemNotice(stopDecision.reminder);
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
if (stopDecision?.terminalCause)
|
|
1248
|
-
state.terminalCause = stopDecision.terminalCause;
|
|
1249
|
-
// Evidence discovered when a response has already completed needs no
|
|
1250
|
-
// human boundary: there is no later request or tool to block.
|
|
1251
|
-
state.pendingRunBudgetInteraction = undefined;
|
|
1252
|
-
return finish(stream, state, queue);
|
|
1253
1338
|
}
|
|
1339
|
+
this.#assertLive(context);
|
|
1340
|
+
if (state.toolExecution.hasPlan) {
|
|
1341
|
+
await state.toolExecution.settle(this.#toolCallContext(state, stream, queue, toolContext));
|
|
1342
|
+
}
|
|
1343
|
+
stream.interruptions = state.toolExecution.pendingApprovals.map((item) => item.interruption);
|
|
1344
|
+
if (answeredApproval && options.stopAfterApprovalResolution && stream.interruptions.length === 0)
|
|
1345
|
+
return true;
|
|
1346
|
+
if (state.toolExecution.terminateAfterExecution)
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
/** Normal no-tool completion; only an explicit stop-policy continuation repeats. */
|
|
1352
|
+
#finishTurn(context) {
|
|
1353
|
+
const { state, options } = context;
|
|
1354
|
+
// The normal stop seam. A stop policy may keep the same run going; the
|
|
1355
|
+
// reminder is admitted at the next request boundary, where pending
|
|
1356
|
+
// budget evidence pauses first and turn accounting applies as usual.
|
|
1357
|
+
const stopDecision = options.onNormalStop && !options.signal?.aborted && !state.criticalWrapUpPending
|
|
1358
|
+
? options.onNormalStop(state.history, state.agent)
|
|
1359
|
+
: undefined;
|
|
1360
|
+
if (stopDecision) {
|
|
1361
|
+
this.#deps.logDiagnostic?.('Normal stop policy decided', { action: stopDecision.action, turnCount: state.turnCount, ...stopDecision.diagnostics }, { severity: 'debug', eventType: 'run_loop.normal_stop_policy' });
|
|
1254
1362
|
}
|
|
1363
|
+
if (stopDecision?.action === 'continue') {
|
|
1364
|
+
this.#queuePendingSystemNotice(stopDecision.reminder);
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
if (stopDecision?.terminalCause)
|
|
1368
|
+
state.terminalCause = stopDecision.terminalCause;
|
|
1369
|
+
// Evidence discovered when a response has already completed needs no
|
|
1370
|
+
// human boundary: there is no later request or tool to block.
|
|
1371
|
+
state.pendingRunBudgetInteraction = undefined;
|
|
1372
|
+
return true;
|
|
1255
1373
|
}
|
|
1256
1374
|
async #dispatchToolCalls(state, stream, queue, events, toolContext) {
|
|
1257
1375
|
const context = this.#toolCallContext(state, stream, queue, toolContext);
|
|
@@ -1288,6 +1406,7 @@ export class ApplicationRunLoop {
|
|
|
1288
1406
|
});
|
|
1289
1407
|
outputPush(stream, queue, { type: 'item', item: callItem });
|
|
1290
1408
|
},
|
|
1409
|
+
onExecutionStarted: (callId) => state.live?.session?.acceptToolDispatch(callId),
|
|
1291
1410
|
onDispatch: (entry) => {
|
|
1292
1411
|
outputPush(stream, queue, {
|
|
1293
1412
|
type: 'tool_call_dispatched',
|
|
@@ -1388,13 +1507,6 @@ export class ApplicationRunLoop {
|
|
|
1388
1507
|
#requiresHumanBudgetDecision(event) {
|
|
1389
1508
|
return event.type === 'tool_stall' || (event.type === 'budget_stage' && event.stage !== 'soft');
|
|
1390
1509
|
}
|
|
1391
|
-
#pauseForRunBudgetInteraction(state, stream, queue) {
|
|
1392
|
-
const interaction = state.pendingRunBudgetInteraction;
|
|
1393
|
-
if (!interaction)
|
|
1394
|
-
return finish(stream, state, queue);
|
|
1395
|
-
stream.interruptions = [interaction];
|
|
1396
|
-
return finish(stream, state, queue);
|
|
1397
|
-
}
|
|
1398
1510
|
}
|
|
1399
1511
|
function isLunaChainedRequest(state, request) {
|
|
1400
1512
|
return (state.currentProviderId === 'codex' &&
|