@astralyn/pi 0.86.1 → 0.87.1
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/CHANGELOG.md +45 -0
- package/dist/bundle/chunks/{anthropic-messages-R2LYJUDD.js → anthropic-messages-XWFTZP2N.js} +1 -1
- package/dist/bundle/chunks/{chunk-GTBMYBFG.js → chunk-772GXEFD.js} +50 -44
- package/dist/bundle/chunks/chunk-CDMUKIXY.js +2 -0
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{openai-completions-6SMWQWQN.js → openai-completions-SHS3CAEL.js} +2 -2
- package/dist/bundle/chunks/{virtual-modules-OHUZ2T7X.js → virtual-modules-LQO2MG3T.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/core/agent-session-runtime.js +1 -1
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +47 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +476 -139
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/background/service.d.ts +9 -0
- package/dist/core/background/service.d.ts.map +1 -1
- package/dist/core/background/service.js +13 -1
- package/dist/core/background/service.js.map +1 -1
- package/dist/core/background/session.d.ts +5 -0
- package/dist/core/background/session.d.ts.map +1 -1
- package/dist/core/background/session.js +2 -1
- package/dist/core/background/session.js.map +1 -1
- package/dist/core/cache-warmer.d.ts +1 -0
- package/dist/core/cache-warmer.d.ts.map +1 -1
- package/dist/core/cache-warmer.js +15 -1
- package/dist/core/cache-warmer.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +155 -57
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/crash-log.d.ts +5 -0
- package/dist/core/crash-log.d.ts.map +1 -1
- package/dist/core/crash-log.js +68 -0
- package/dist/core/crash-log.js.map +1 -1
- package/dist/core/export-html/template.js +6 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +16 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +110 -5
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +77 -6
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-config.d.ts +52 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +16 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +6 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +61 -35
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/provider-composer.d.ts +1 -0
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +19 -0
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +6 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +36 -9
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +97 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/read.d.ts +4 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +5 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/btw/agent.d.ts.map +1 -1
- package/dist/extensions/btw/agent.js +8 -5
- package/dist/extensions/btw/agent.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/bug-report.d.ts.map +1 -1
- package/dist/modes/interactive/bug-report.js +4 -0
- package/dist/modes/interactive/bug-report.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +7 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +64 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.js +1 -1
- package/dist/utils/mime.js.map +1 -1
- package/dist/utils/tool-result-images.d.ts +3 -1
- package/dist/utils/tool-result-images.d.ts.map +1 -1
- package/dist/utils/tool-result-images.js +4 -1
- package/dist/utils/tool-result-images.js.map +1 -1
- package/docs/bundled/extensions/background.md +3 -3
- package/docs/bundled/extensions/subagent.md +1 -1
- package/docs/compaction.md +24 -5
- package/docs/extensions.md +72 -8
- package/docs/models.md +30 -1
- package/docs/sdk.md +15 -4
- package/docs/session-format.md +25 -4
- package/docs/usage.md +3 -0
- package/examples/README.md +11 -1
- package/examples/plugins/pi-example-plugin/src/session.ts +3 -2
- package/examples/rpc-client.ts +35 -0
- package/examples/rpc-extension-ui.ts +25 -5
- package/npm-shrinkwrap.json +43 -43
- package/package.json +8 -8
- package/dist/bundle/chunks/chunk-DAEM5JIZ.js +0 -2
|
@@ -18,23 +18,25 @@ import { contentText, getCurrentSystemMessage, getSystemMessageText, retryDelayM
|
|
|
18
18
|
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
19
19
|
import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
|
|
20
20
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
21
|
+
import { processImage } from "../utils/image-process.js";
|
|
21
22
|
import { sleep } from "../utils/sleep.js";
|
|
22
23
|
import { normalizeToolResultImages } from "../utils/tool-result-images.js";
|
|
23
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
24
25
|
import { BackgroundSession } from "./background/session.js";
|
|
25
26
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
26
27
|
import { generateBugReportSummary } from "./bug-report.js";
|
|
27
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
28
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
28
29
|
import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
|
|
29
30
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
30
31
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
31
32
|
import { ExtensionRunner, STALE_EXTENSION_CONTEXT_MESSAGE, wrapRegisteredTools, } from "./extensions/index.js";
|
|
32
33
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
34
|
+
import { convertToLlm } from "./messages.js";
|
|
33
35
|
import { ModelRegistry } from "./model-registry.js";
|
|
34
36
|
import { resolveModelStreamOptions } from "./model-stream-options.js";
|
|
35
37
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
36
38
|
import { exportSessionToJsonl } from "./session-export.js";
|
|
37
|
-
import { getLatestCompactionEntry } from "./session-manager.js";
|
|
39
|
+
import { getLatestCompactionEntry, SessionManager, } from "./session-manager.js";
|
|
38
40
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
39
41
|
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
40
42
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -56,9 +58,6 @@ export function parseSkillBlock(text) {
|
|
|
56
58
|
userMessage: match[4]?.trim() || undefined,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
|
-
// ============================================================================
|
|
60
|
-
// Types
|
|
61
|
-
// ============================================================================
|
|
62
61
|
function withoutDeletedHeaders(headers) {
|
|
63
62
|
return headers
|
|
64
63
|
? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
|
|
@@ -87,6 +86,8 @@ export class AgentSession {
|
|
|
87
86
|
this._pendingNextTurnMessages = [];
|
|
88
87
|
/** Context-only custom messages queued during a run, flushed once the current turn's tool results are in. */
|
|
89
88
|
this._pendingCustomMessages = [];
|
|
89
|
+
/** Background completions queued for steering, awaiting the persistence that confirms delivery. */
|
|
90
|
+
this._queuedBackgroundCompletions = new Map();
|
|
90
91
|
// Compaction state
|
|
91
92
|
this._compactionAbortController = undefined;
|
|
92
93
|
this._autoCompactionAbortController = undefined;
|
|
@@ -100,6 +101,14 @@ export class AgentSession {
|
|
|
100
101
|
this._bashAbortControllers = new Set();
|
|
101
102
|
this._pendingBashMessages = [];
|
|
102
103
|
this._turnIndex = 0;
|
|
104
|
+
this._entryIdsByMessage = new WeakMap();
|
|
105
|
+
this._boundaryDispatchedMessages = new WeakSet();
|
|
106
|
+
this._lastAssistantToolResults = [];
|
|
107
|
+
this._lastActivityOutcome = "completed";
|
|
108
|
+
this._isBeforeSettle = false;
|
|
109
|
+
this._abortDuringBeforeSettle = false;
|
|
110
|
+
this._isEmittingAgentSettled = false;
|
|
111
|
+
this._deferredSettledActions = [];
|
|
103
112
|
this._baseToolDefinitions = new Map();
|
|
104
113
|
this._extensionMode = "print";
|
|
105
114
|
// Tool registry for extension getTools/setTools
|
|
@@ -107,13 +116,17 @@ export class AgentSession {
|
|
|
107
116
|
this._toolDefinitions = new Map();
|
|
108
117
|
this._toolPromptSnippets = new Map();
|
|
109
118
|
this._toolPromptGuidelines = new Map();
|
|
110
|
-
// Track last assistant message for auto-compaction check
|
|
111
|
-
this._lastAssistantMessage = undefined;
|
|
112
119
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
113
120
|
this._handleAgentEvent = async (event) => {
|
|
114
121
|
// Preserve host delivery identity before extension message_end transforms.
|
|
115
122
|
// Content/details may be replaced, but a persisted replacement is still an ack.
|
|
116
123
|
const acknowledgeBackground = event.type === "message_end" ? this._backgroundHost.messageAcknowledgement(event.message) : undefined;
|
|
124
|
+
// A queued completion is tracked by identity too, for the same reason.
|
|
125
|
+
const queuedBackgroundCompletion = event.type === "message_start" || event.type === "message_end"
|
|
126
|
+
? this._queuedBackgroundCompletions.get(event.message)
|
|
127
|
+
: undefined;
|
|
128
|
+
if (event.type === "message_start" && queuedBackgroundCompletion)
|
|
129
|
+
queuedBackgroundCompletion.drained = true;
|
|
117
130
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
118
131
|
// This ensures the UI sees the updated queue state
|
|
119
132
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
@@ -136,18 +149,18 @@ export class AgentSession {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
|
-
// Emit to extensions first
|
|
152
|
+
// Emit to extensions first, then notify public listeners.
|
|
140
153
|
await this._emitExtensionEvent(event);
|
|
141
|
-
// Notify all listeners
|
|
142
154
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
143
155
|
// Handle session persistence
|
|
144
156
|
if (event.type === "message_end") {
|
|
145
|
-
if (this._persistMessage(event.message))
|
|
157
|
+
if (this._persistMessage(event.message)) {
|
|
146
158
|
acknowledgeBackground?.();
|
|
147
|
-
|
|
159
|
+
queuedBackgroundCompletion?.resolve();
|
|
160
|
+
}
|
|
148
161
|
if (event.message.role === "assistant") {
|
|
149
|
-
this._lastAssistantMessage = event.message;
|
|
150
162
|
const assistantMsg = event.message;
|
|
163
|
+
this._lastAssistantMessage = assistantMsg;
|
|
151
164
|
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
|
|
152
165
|
this._overflowRecoveryAttempted = false;
|
|
153
166
|
}
|
|
@@ -169,8 +182,11 @@ export class AgentSession {
|
|
|
169
182
|
// extension and listener dispatch above also picks up messages that turn_end
|
|
170
183
|
// handlers queued.
|
|
171
184
|
if (event.type === "turn_end") {
|
|
185
|
+
this._lastAssistantToolResults = event.toolResults;
|
|
172
186
|
this._flushPendingCustomMessages();
|
|
173
187
|
}
|
|
188
|
+
if (event.type === "agent_end")
|
|
189
|
+
this._failDrainedBackgroundCompletions();
|
|
174
190
|
};
|
|
175
191
|
this.agent = config.agent;
|
|
176
192
|
this.sessionManager = config.sessionManager;
|
|
@@ -178,16 +194,15 @@ export class AgentSession {
|
|
|
178
194
|
this._backgroundHost = new BackgroundSession({
|
|
179
195
|
manager: this.sessionManager,
|
|
180
196
|
role: config.executionRole ?? "main",
|
|
181
|
-
canDeliver: () =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const asides = this._pendingNextTurnMessages.slice();
|
|
189
|
-
return this._runAgentPrompt(asides.length ? [message, ...asides] : message);
|
|
197
|
+
canDeliver: () => {
|
|
198
|
+
if (this.model === undefined || this.isCompacting)
|
|
199
|
+
return false;
|
|
200
|
+
// A completion steered into an active run follows the interactive steering queue, which
|
|
201
|
+
// already preserves the order of queued user input. An idle delivery starts its own
|
|
202
|
+
// turn, so it still waits for the queues to drain.
|
|
203
|
+
return this.isStreaming || (this.pendingMessageCount === 0 && !this.agent.hasQueuedMessages());
|
|
190
204
|
},
|
|
205
|
+
deliver: (message) => this._deliverBackgroundCompletion(message),
|
|
191
206
|
onEntry: (entry) => this._emit({ type: "entry_appended", entry }),
|
|
192
207
|
onError: (event, error) => this._extensionRunner?.emitError({ extensionPath: "<background>", event, error }),
|
|
193
208
|
});
|
|
@@ -212,6 +227,8 @@ export class AgentSession {
|
|
|
212
227
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
213
228
|
this._installAgentToolHooks();
|
|
214
229
|
this._installAgentNextTurnRefresh();
|
|
230
|
+
this._installAgentRequestProjection();
|
|
231
|
+
this._installAgentBoundaryHooks();
|
|
215
232
|
this._installAgentForcedPromptProjection();
|
|
216
233
|
this._buildRuntime({
|
|
217
234
|
activeToolNames: this._initialActiveToolNames,
|
|
@@ -264,7 +281,8 @@ export class AgentSession {
|
|
|
264
281
|
}),
|
|
265
282
|
};
|
|
266
283
|
const currentSystemPrompt = this.systemPrompt;
|
|
267
|
-
|
|
284
|
+
// The canonical projection is what the next request converts; the inspection cache may lag it.
|
|
285
|
+
const messages = structuredClone(this.sessionManager.buildSessionProjection().messages);
|
|
268
286
|
const convert = this.agent.convertToLlm;
|
|
269
287
|
const transform = this.agent.transformContext;
|
|
270
288
|
const prepared = this._captureModelContext
|
|
@@ -375,8 +393,10 @@ export class AgentSession {
|
|
|
375
393
|
: undefined;
|
|
376
394
|
const content = hookResult?.content ?? result.content ?? [];
|
|
377
395
|
// Runs after the extension hook so images injected or replaced by extensions are normalized too.
|
|
396
|
+
const resizeOptions = this.model?.inputLimits?.images?.resize;
|
|
378
397
|
const normalizedContent = await normalizeToolResultImages(content, {
|
|
379
398
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
399
|
+
...(resizeOptions ? { resizeOptions } : {}),
|
|
380
400
|
});
|
|
381
401
|
if (!hookResult && normalizedContent === content) {
|
|
382
402
|
return undefined;
|
|
@@ -392,15 +412,83 @@ export class AgentSession {
|
|
|
392
412
|
async _compactBeforeNextAssistantResponse(context) {
|
|
393
413
|
const model = this.model;
|
|
394
414
|
const settings = this.settingsManager.getCompactionSettings(model);
|
|
415
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
395
416
|
if (!model ||
|
|
396
417
|
model.contextWindow <= 0 ||
|
|
397
|
-
!shouldCompact(
|
|
398
|
-
return context;
|
|
418
|
+
!shouldCompact(estimateProjectedContextTokens(projection, this.sessionManager.getBranch()).tokens, model.contextWindow, settings)) {
|
|
419
|
+
return { ...context, messages: projection.messages };
|
|
399
420
|
}
|
|
400
421
|
await this._runAutoCompaction("threshold", false);
|
|
401
|
-
return {
|
|
402
|
-
|
|
403
|
-
|
|
422
|
+
return { ...context, messages: this.sessionManager.buildSessionProjection().messages };
|
|
423
|
+
}
|
|
424
|
+
_installAgentRequestProjection() {
|
|
425
|
+
const previousPrepareRequest = this.agent.prepareRequest;
|
|
426
|
+
this.agent.prepareRequest = async (request, signal) => {
|
|
427
|
+
const canonicalContext = {
|
|
428
|
+
...request.context,
|
|
429
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
430
|
+
// Messages declare the provider-visible loadout; context.tools keeps executable implementations.
|
|
431
|
+
tools: this.agent.state.tools.slice(),
|
|
432
|
+
};
|
|
433
|
+
const previous = await previousPrepareRequest?.({
|
|
434
|
+
...request,
|
|
435
|
+
context: canonicalContext,
|
|
436
|
+
model: this.agent.state.model,
|
|
437
|
+
thinkingLevel: this.agent.state.thinkingLevel,
|
|
438
|
+
}, signal);
|
|
439
|
+
return {
|
|
440
|
+
...previous,
|
|
441
|
+
context: previous?.context ?? canonicalContext,
|
|
442
|
+
model: previous?.model ?? this.agent.state.model,
|
|
443
|
+
thinkingLevel: previous?.thinkingLevel ?? this.agent.state.thinkingLevel,
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async _dispatchTurnEndBoundary(message, toolResults) {
|
|
448
|
+
this._lastActivityOutcome =
|
|
449
|
+
message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
|
|
450
|
+
const messageEntryId = this._findPersistedMessageEntryId(message);
|
|
451
|
+
if (!this._extensionRunner.hasHandlers("turn_end"))
|
|
452
|
+
return false;
|
|
453
|
+
if (!messageEntryId) {
|
|
454
|
+
this._extensionRunner.emitError({
|
|
455
|
+
extensionPath: "<boundary>",
|
|
456
|
+
event: "turn_end",
|
|
457
|
+
error: "turn_end could not resolve the persisted assistant entry ID",
|
|
458
|
+
});
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const toolResultEntryIds = toolResults.flatMap((result) => {
|
|
462
|
+
const entryId = this._findPersistedMessageEntryId(result);
|
|
463
|
+
return entryId ? [entryId] : [];
|
|
464
|
+
});
|
|
465
|
+
const boundary = await this._extensionRunner.emitBoundary({
|
|
466
|
+
type: "turn_end",
|
|
467
|
+
turnIndex: this._turnIndex,
|
|
468
|
+
message,
|
|
469
|
+
toolResults,
|
|
470
|
+
messageEntryId,
|
|
471
|
+
toolResultEntryIds,
|
|
472
|
+
outcome: this._lastActivityOutcome,
|
|
473
|
+
}, (entries) => this._buildBoundaryContext(entries, "turn_end"));
|
|
474
|
+
this._commitBoundaryDrafts(boundary.entries);
|
|
475
|
+
if (boundary.continue && !this._buildBoundaryContext([], "turn_end").canContinue) {
|
|
476
|
+
this._reportInvalidBoundaryContinuation("turn_end");
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
return boundary.continue;
|
|
480
|
+
}
|
|
481
|
+
_installAgentBoundaryHooks() {
|
|
482
|
+
const previousFinishTurn = this.agent.finishTurn;
|
|
483
|
+
this.agent.finishTurn = async (turn, signal) => {
|
|
484
|
+
this._boundaryDispatchedMessages.add(turn.message);
|
|
485
|
+
const extensionContinue = await this._dispatchTurnEndBoundary(turn.message, turn.toolResults);
|
|
486
|
+
const previousDecision = await previousFinishTurn?.(turn, signal);
|
|
487
|
+
if (previousDecision?.action === "end")
|
|
488
|
+
return previousDecision;
|
|
489
|
+
if (extensionContinue || previousDecision?.action === "continue")
|
|
490
|
+
return { action: "continue" };
|
|
491
|
+
return undefined;
|
|
404
492
|
};
|
|
405
493
|
}
|
|
406
494
|
_installAgentNextTurnRefresh() {
|
|
@@ -409,7 +497,10 @@ export class AgentSession {
|
|
|
409
497
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
410
498
|
: undefined);
|
|
411
499
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
412
|
-
const context = await this._compactBeforeNextAssistantResponse(
|
|
500
|
+
const context = await this._compactBeforeNextAssistantResponse({
|
|
501
|
+
...turn.context,
|
|
502
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
503
|
+
});
|
|
413
504
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
414
505
|
const nextContext = previousSnapshot?.context ?? context;
|
|
415
506
|
const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
|
|
@@ -439,6 +530,84 @@ export class AgentSession {
|
|
|
439
530
|
// =========================================================================
|
|
440
531
|
// Event Subscription
|
|
441
532
|
// =========================================================================
|
|
533
|
+
_refreshFinalizedContext() {
|
|
534
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
535
|
+
for (const entry of projection.entries) {
|
|
536
|
+
for (const message of entry.messages)
|
|
537
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
538
|
+
}
|
|
539
|
+
this.agent.state.messages = projection.messages;
|
|
540
|
+
}
|
|
541
|
+
_applyBoundaryDrafts(manager, drafts) {
|
|
542
|
+
const appended = [];
|
|
543
|
+
for (const draft of drafts) {
|
|
544
|
+
let entryId;
|
|
545
|
+
switch (draft.type) {
|
|
546
|
+
case "custom":
|
|
547
|
+
entryId = manager.appendCustomEntry(draft.customType, draft.data);
|
|
548
|
+
break;
|
|
549
|
+
case "custom_message":
|
|
550
|
+
entryId = manager.appendCustomMessageEntry(draft.customType, draft.content, draft.display, draft.details);
|
|
551
|
+
break;
|
|
552
|
+
case "context_edit":
|
|
553
|
+
entryId = manager.appendContextEdit(draft.targetId, draft.replacement);
|
|
554
|
+
break;
|
|
555
|
+
case "compaction": {
|
|
556
|
+
const tokensBefore = estimateProjectedContextTokens(manager.buildSessionProjection(), manager.getBranch()).tokens;
|
|
557
|
+
entryId = manager.appendCompaction(draft.summary, draft.firstKeptEntryId, tokensBefore, draft.details, true, draft.usage);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const entry = manager.getEntry(entryId);
|
|
562
|
+
if (entry)
|
|
563
|
+
appended.push(entry);
|
|
564
|
+
}
|
|
565
|
+
return appended;
|
|
566
|
+
}
|
|
567
|
+
_createBoundaryPreviewManager(drafts) {
|
|
568
|
+
const header = this.sessionManager.getHeader();
|
|
569
|
+
if (!header)
|
|
570
|
+
throw new Error("Session header is missing");
|
|
571
|
+
const manager = SessionManager.inMemory(this._cwd, undefined, [header, ...this.sessionManager.getBranch()]);
|
|
572
|
+
this._applyBoundaryDrafts(manager, drafts);
|
|
573
|
+
return manager;
|
|
574
|
+
}
|
|
575
|
+
_getPendingBoundaryMessages() {
|
|
576
|
+
return [...this.agent.peekQueuedMessages(), ...this._pendingCustomMessages];
|
|
577
|
+
}
|
|
578
|
+
_buildBoundaryContext(drafts, boundary) {
|
|
579
|
+
const projection = this._createBoundaryPreviewManager(drafts).buildSessionProjection();
|
|
580
|
+
const pendingMessages = this._getPendingBoundaryMessages();
|
|
581
|
+
const llmMessages = convertToLlm(projection.messages);
|
|
582
|
+
const finalRole = llmMessages[llmMessages.length - 1]?.role;
|
|
583
|
+
const hasNonSystemContext = llmMessages.some((message) => message.role !== "system");
|
|
584
|
+
const contextCanContinue = hasNonSystemContext && finalRole !== "assistant";
|
|
585
|
+
const pendingCustomContext = this._pendingCustomMessages.length > 0;
|
|
586
|
+
return {
|
|
587
|
+
contextEntries: projection.entries,
|
|
588
|
+
contextMessages: projection.messages,
|
|
589
|
+
llmMessages,
|
|
590
|
+
pendingMessages,
|
|
591
|
+
canContinue: contextCanContinue ||
|
|
592
|
+
pendingCustomContext ||
|
|
593
|
+
(boundary === "turn_end"
|
|
594
|
+
? this.agent.hasQueuedMessages()
|
|
595
|
+
: finalRole === "assistant" && this.agent.hasQueuedMessages()),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
_commitBoundaryDrafts(drafts) {
|
|
599
|
+
const appended = this._applyBoundaryDrafts(this.sessionManager, drafts);
|
|
600
|
+
this._refreshFinalizedContext();
|
|
601
|
+
for (const entry of appended)
|
|
602
|
+
this._emit({ type: "entry_appended", entry });
|
|
603
|
+
}
|
|
604
|
+
_reportInvalidBoundaryContinuation(event) {
|
|
605
|
+
this._extensionRunner.emitError({
|
|
606
|
+
extensionPath: "<boundary>",
|
|
607
|
+
event,
|
|
608
|
+
error: `${event} requested continuation without runnable model context`,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
442
611
|
/** Emit an event to all listeners */
|
|
443
612
|
_emit(event) {
|
|
444
613
|
for (const l of this._eventListeners) {
|
|
@@ -476,21 +645,35 @@ export class AgentSession {
|
|
|
476
645
|
}
|
|
477
646
|
async _emitAgentSettled() {
|
|
478
647
|
this._cacheWarmer?.onAgentSettled();
|
|
648
|
+
// Background completions stay paused until runs requested by settled handlers have started,
|
|
649
|
+
// so a deferred prompt never races a delivery for the same idle gap.
|
|
479
650
|
const resumeBackground = this.pauseBackgroundNotifications();
|
|
480
651
|
this._isAgentRunActive = false;
|
|
481
652
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
653
|
+
this._isEmittingAgentSettled = true;
|
|
654
|
+
try {
|
|
655
|
+
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
656
|
+
this._emit({ type: "agent_settled" });
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
this._isEmittingAgentSettled = false;
|
|
660
|
+
}
|
|
661
|
+
for (const action of this._deferredSettledActions.splice(0))
|
|
662
|
+
await action();
|
|
484
663
|
}
|
|
485
664
|
finally {
|
|
486
665
|
this._resolveIdleWaitIfIdle();
|
|
487
666
|
resumeBackground();
|
|
488
667
|
}
|
|
489
668
|
}
|
|
490
|
-
/**
|
|
669
|
+
/**
|
|
670
|
+
* Persist model/custom messages and consume queued context only after the append succeeds.
|
|
671
|
+
* Returns the persisted entry ID, which boundary events and recovery omissions resolve later.
|
|
672
|
+
*/
|
|
491
673
|
_persistMessage(message) {
|
|
674
|
+
let entryId;
|
|
492
675
|
if (message.role === "custom") {
|
|
493
|
-
this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
676
|
+
entryId = this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
494
677
|
const pendingIndex = this._pendingNextTurnMessages.indexOf(message);
|
|
495
678
|
if (pendingIndex !== -1)
|
|
496
679
|
this._pendingNextTurnMessages.splice(pendingIndex, 1);
|
|
@@ -499,15 +682,16 @@ export class AgentSession {
|
|
|
499
682
|
message.role === "user" ||
|
|
500
683
|
message.role === "assistant" ||
|
|
501
684
|
message.role === "toolResult") {
|
|
502
|
-
this.sessionManager.appendMessage(message);
|
|
685
|
+
entryId = this.sessionManager.appendMessage(message);
|
|
503
686
|
}
|
|
504
687
|
else {
|
|
505
688
|
// Bash execution, compaction and branch summaries are persisted by their owners.
|
|
506
|
-
return
|
|
689
|
+
return undefined;
|
|
507
690
|
}
|
|
691
|
+
this._entryIdsByMessage.set(message, entryId);
|
|
508
692
|
if (message.role === "toolResult")
|
|
509
693
|
this._backgroundHost.acknowledgeWaitResult(message.details);
|
|
510
|
-
return
|
|
694
|
+
return entryId;
|
|
511
695
|
}
|
|
512
696
|
_willRetryAfterAgentEnd(event) {
|
|
513
697
|
if (this._agentRunAbortRequested)
|
|
@@ -524,6 +708,47 @@ export class AgentSession {
|
|
|
524
708
|
}
|
|
525
709
|
return false;
|
|
526
710
|
}
|
|
711
|
+
_findPersistedMessageEntryId(message) {
|
|
712
|
+
const mapped = this._entryIdsByMessage.get(message);
|
|
713
|
+
if (mapped)
|
|
714
|
+
return mapped;
|
|
715
|
+
for (const entry of [...this.sessionManager.getBranch()].reverse()) {
|
|
716
|
+
if (entry.type === "message" && entry.message === message)
|
|
717
|
+
return entry.id;
|
|
718
|
+
}
|
|
719
|
+
const messageIndex = this.agent.state.messages.indexOf(message);
|
|
720
|
+
if (messageIndex < 0)
|
|
721
|
+
return undefined;
|
|
722
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
723
|
+
let projectedIndex = 0;
|
|
724
|
+
for (const entry of projection.entries) {
|
|
725
|
+
for (let i = 0; i < entry.messages.length; i++) {
|
|
726
|
+
if (projectedIndex === messageIndex) {
|
|
727
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
728
|
+
return entry.sourceEntry.id;
|
|
729
|
+
}
|
|
730
|
+
projectedIndex++;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
_omitRecoveryAttempt(message, toolResults = []) {
|
|
736
|
+
const targets = [message, ...toolResults];
|
|
737
|
+
const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));
|
|
738
|
+
const unresolvedProjectedTarget = targets.some((target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target));
|
|
739
|
+
if (unresolvedProjectedTarget) {
|
|
740
|
+
throw new Error("Cannot persist recovery omission because a projected message has no source entry");
|
|
741
|
+
}
|
|
742
|
+
for (const targetId of targetIds) {
|
|
743
|
+
if (!targetId)
|
|
744
|
+
continue;
|
|
745
|
+
const editId = this.sessionManager.appendContextEdit(targetId, null);
|
|
746
|
+
const entry = this.sessionManager.getEntry(editId);
|
|
747
|
+
if (entry)
|
|
748
|
+
this._emit({ type: "entry_appended", entry });
|
|
749
|
+
}
|
|
750
|
+
this._refreshFinalizedContext();
|
|
751
|
+
}
|
|
527
752
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
528
753
|
_findLastAssistantMessage() {
|
|
529
754
|
const messages = this.agent.state.messages;
|
|
@@ -567,13 +792,9 @@ export class AgentSession {
|
|
|
567
792
|
await this._extensionRunner.emit(extensionEvent);
|
|
568
793
|
}
|
|
569
794
|
else if (event.type === "turn_end") {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
message: event.message,
|
|
574
|
-
toolResults: event.toolResults,
|
|
575
|
-
};
|
|
576
|
-
await this._extensionRunner.emit(extensionEvent);
|
|
795
|
+
if (event.message.role === "assistant" && !this._boundaryDispatchedMessages.delete(event.message)) {
|
|
796
|
+
await this._dispatchTurnEndBoundary(event.message, event.toolResults);
|
|
797
|
+
}
|
|
577
798
|
this._turnIndex++;
|
|
578
799
|
}
|
|
579
800
|
else if (event.type === "message_start") {
|
|
@@ -668,6 +889,9 @@ export class AgentSession {
|
|
|
668
889
|
*/
|
|
669
890
|
dispose() {
|
|
670
891
|
this._backgroundHost.dispose();
|
|
892
|
+
for (const queued of [...this._queuedBackgroundCompletions.values()]) {
|
|
893
|
+
queued.reject(new Error("the session was disposed"));
|
|
894
|
+
}
|
|
671
895
|
try {
|
|
672
896
|
this.abortRetry();
|
|
673
897
|
this.abortCompaction();
|
|
@@ -690,6 +914,10 @@ export class AgentSession {
|
|
|
690
914
|
// =========================================================================
|
|
691
915
|
// Read-only State Access
|
|
692
916
|
// =========================================================================
|
|
917
|
+
/** Refresh the public finalized transcript from the canonical session projection. */
|
|
918
|
+
refreshContext() {
|
|
919
|
+
this._refreshFinalizedContext();
|
|
920
|
+
}
|
|
693
921
|
/** Full agent state */
|
|
694
922
|
get state() {
|
|
695
923
|
return this.agent.state;
|
|
@@ -924,7 +1152,15 @@ export class AgentSession {
|
|
|
924
1152
|
this._isAgentRunActive = true;
|
|
925
1153
|
try {
|
|
926
1154
|
await this.agent.prompt(messages);
|
|
927
|
-
while (
|
|
1155
|
+
while (!this._agentRunAbortRequested) {
|
|
1156
|
+
if (await this._handlePostAgentRun()) {
|
|
1157
|
+
if (this._agentRunAbortRequested)
|
|
1158
|
+
break;
|
|
1159
|
+
await this.agent.continue();
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary()))
|
|
1163
|
+
break;
|
|
928
1164
|
if (this._agentRunAbortRequested)
|
|
929
1165
|
break;
|
|
930
1166
|
await this.agent.continue();
|
|
@@ -939,17 +1175,67 @@ export class AgentSession {
|
|
|
939
1175
|
await this._emitAgentSettled();
|
|
940
1176
|
}
|
|
941
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Deliver a background completion through the same queueing path as interactive input:
|
|
1180
|
+
* steering into an active run lands the result right after the current tool batch, and an
|
|
1181
|
+
* idle session starts its own turn. The promise settles only once the message is persisted,
|
|
1182
|
+
* or once it can no longer be persisted, so the caller never releases its claim for a
|
|
1183
|
+
* message the transcript does not contain.
|
|
1184
|
+
*/
|
|
1185
|
+
_deliverBackgroundCompletion(message) {
|
|
1186
|
+
if (!this.isStreaming) {
|
|
1187
|
+
// Completion turns skip before_agent_start, but consume nextTurn context.
|
|
1188
|
+
const asides = this._pendingNextTurnMessages.slice();
|
|
1189
|
+
return this._runAgentPrompt(asides.length ? [message, ...asides] : message);
|
|
1190
|
+
}
|
|
1191
|
+
return new Promise((resolve, reject) => {
|
|
1192
|
+
const queued = {
|
|
1193
|
+
drained: false,
|
|
1194
|
+
resolve: () => {
|
|
1195
|
+
if (this._queuedBackgroundCompletions.get(message) !== queued)
|
|
1196
|
+
return;
|
|
1197
|
+
this._queuedBackgroundCompletions.delete(message);
|
|
1198
|
+
resolve();
|
|
1199
|
+
},
|
|
1200
|
+
reject: (reason) => {
|
|
1201
|
+
if (this._queuedBackgroundCompletions.get(message) !== queued)
|
|
1202
|
+
return;
|
|
1203
|
+
this._queuedBackgroundCompletions.delete(message);
|
|
1204
|
+
reject(reason);
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
this._queuedBackgroundCompletions.set(message, queued);
|
|
1208
|
+
try {
|
|
1209
|
+
this.agent.steer(message);
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
queued.reject(error);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Fail deliveries the owning run emitted but never persisted. A completion still sitting in
|
|
1218
|
+
* the steering queue when its run ends stays claimed: the next run delivers it, so failing
|
|
1219
|
+
* it here would persist the same message twice.
|
|
1220
|
+
*/
|
|
1221
|
+
_failDrainedBackgroundCompletions() {
|
|
1222
|
+
for (const queued of [...this._queuedBackgroundCompletions.values()]) {
|
|
1223
|
+
if (queued.drained)
|
|
1224
|
+
queued.reject(new Error("the completion message was not persisted"));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
942
1227
|
async _handlePostAgentRun() {
|
|
943
|
-
const
|
|
1228
|
+
const message = this._lastAssistantMessage;
|
|
1229
|
+
const toolResults = this._lastAssistantToolResults;
|
|
944
1230
|
this._lastAssistantMessage = undefined;
|
|
1231
|
+
this._lastAssistantToolResults = [];
|
|
945
1232
|
if (this._agentRunAbortRequested) {
|
|
946
1233
|
this._finishCancelledRetry();
|
|
947
1234
|
return false;
|
|
948
1235
|
}
|
|
949
|
-
if (!
|
|
950
|
-
return
|
|
951
|
-
|
|
952
|
-
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
1236
|
+
if (!message)
|
|
1237
|
+
return this.agent.hasQueuedMessages();
|
|
1238
|
+
if (this._isRetryableError(message) && (await this._prepareRetry(message))) {
|
|
953
1239
|
if (this._agentRunAbortRequested)
|
|
954
1240
|
this._finishCancelledRetry();
|
|
955
1241
|
return !this._agentRunAbortRequested;
|
|
@@ -958,22 +1244,46 @@ export class AgentSession {
|
|
|
958
1244
|
this._finishCancelledRetry();
|
|
959
1245
|
return false;
|
|
960
1246
|
}
|
|
961
|
-
if (
|
|
1247
|
+
if (message.stopReason === "error" && this._retryAttempt > 0) {
|
|
962
1248
|
this._emit({
|
|
963
1249
|
type: "auto_retry_end",
|
|
964
1250
|
success: false,
|
|
965
1251
|
attempt: this._retryAttempt,
|
|
966
|
-
finalError:
|
|
1252
|
+
finalError: message.errorMessage,
|
|
967
1253
|
});
|
|
968
1254
|
this._retryAttempt = 0;
|
|
969
1255
|
}
|
|
970
|
-
if (await this._checkCompaction(
|
|
1256
|
+
if (await this._checkCompaction(message, true, toolResults)) {
|
|
971
1257
|
return !this._agentRunAbortRequested;
|
|
972
1258
|
}
|
|
973
|
-
// The
|
|
974
|
-
//
|
|
1259
|
+
// The low-level loop drains both queues before agent_end. Messages queued by
|
|
1260
|
+
// agent_end handlers require a fresh run before pre-settlement handlers fire.
|
|
975
1261
|
return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
|
|
976
1262
|
}
|
|
1263
|
+
async _runBeforeSettleBoundary() {
|
|
1264
|
+
if (!this._extensionRunner.hasHandlers("agent_before_settle"))
|
|
1265
|
+
return this.agent.hasQueuedMessages();
|
|
1266
|
+
this._isBeforeSettle = true;
|
|
1267
|
+
this._abortDuringBeforeSettle = false;
|
|
1268
|
+
try {
|
|
1269
|
+
const result = await this._extensionRunner.emitBoundary({ type: "agent_before_settle", outcome: this._lastActivityOutcome }, (entries) => this._buildBoundaryContext(entries, "agent_before_settle"));
|
|
1270
|
+
this._commitBoundaryDrafts(result.entries);
|
|
1271
|
+
this._flushPendingCustomMessages();
|
|
1272
|
+
const finalContext = this._buildBoundaryContext([], "agent_before_settle");
|
|
1273
|
+
if (this._abortDuringBeforeSettle)
|
|
1274
|
+
return false;
|
|
1275
|
+
const shouldContinue = result.continue || this.agent.hasQueuedMessages();
|
|
1276
|
+
if (shouldContinue && !finalContext.canContinue) {
|
|
1277
|
+
if (result.continue)
|
|
1278
|
+
this._reportInvalidBoundaryContinuation("agent_before_settle");
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
return shouldContinue;
|
|
1282
|
+
}
|
|
1283
|
+
finally {
|
|
1284
|
+
this._isBeforeSettle = false;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
977
1287
|
async _runInputHandlers(text, images, source, streamingBehavior) {
|
|
978
1288
|
if (!this._extensionRunner.hasHandlers("input")) {
|
|
979
1289
|
return { text, images };
|
|
@@ -987,6 +1297,25 @@ export class AgentSession {
|
|
|
987
1297
|
}
|
|
988
1298
|
return { text, images };
|
|
989
1299
|
}
|
|
1300
|
+
async _normalizePromptImages(images) {
|
|
1301
|
+
if (!images)
|
|
1302
|
+
return { images: [], hints: [] };
|
|
1303
|
+
const normalizedImages = [];
|
|
1304
|
+
const hints = [];
|
|
1305
|
+
for (const image of images) {
|
|
1306
|
+
const processed = await processImage(Buffer.from(image.data, "base64"), image.mimeType, {
|
|
1307
|
+
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
1308
|
+
resizeOptions: this.model?.inputLimits?.images?.resize,
|
|
1309
|
+
});
|
|
1310
|
+
if (!processed.ok) {
|
|
1311
|
+
hints.push(processed.message);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
normalizedImages.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
|
|
1315
|
+
hints.push(...processed.hints);
|
|
1316
|
+
}
|
|
1317
|
+
return { images: normalizedImages, hints };
|
|
1318
|
+
}
|
|
990
1319
|
/**
|
|
991
1320
|
* Send a prompt to the agent.
|
|
992
1321
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -997,6 +1326,10 @@ export class AgentSession {
|
|
|
997
1326
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
998
1327
|
*/
|
|
999
1328
|
async prompt(text, options) {
|
|
1329
|
+
if (this._isEmittingAgentSettled) {
|
|
1330
|
+
this._deferredSettledActions.push(async () => await this.prompt(text, options));
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1000
1333
|
// A user-initiated prompt is the recovery point for failed completion deliveries.
|
|
1001
1334
|
this.retryBackgroundNotifications();
|
|
1002
1335
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
@@ -1073,23 +1406,8 @@ export class AgentSession {
|
|
|
1073
1406
|
if (lastAssistant) {
|
|
1074
1407
|
await this._checkCompaction(lastAssistant, false);
|
|
1075
1408
|
}
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
// Add user message
|
|
1079
|
-
const userContent = [{ type: "text", text: expandedText }];
|
|
1080
|
-
if (currentImages) {
|
|
1081
|
-
userContent.push(...currentImages);
|
|
1082
|
-
}
|
|
1083
|
-
messages.push({
|
|
1084
|
-
role: "user",
|
|
1085
|
-
content: userContent,
|
|
1086
|
-
timestamp: Date.now(),
|
|
1087
|
-
});
|
|
1088
|
-
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
1089
|
-
for (const msg of this._pendingNextTurnMessages) {
|
|
1090
|
-
messages.push(msg);
|
|
1091
|
-
}
|
|
1092
|
-
// Emit before_agent_start extension event
|
|
1409
|
+
// Emit before_agent_start before normalizing images so extension-driven model
|
|
1410
|
+
// selection determines the resize profile used for the request and history.
|
|
1093
1411
|
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1094
1412
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1095
1413
|
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
@@ -1099,6 +1417,22 @@ export class AgentSession {
|
|
|
1099
1417
|
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1100
1418
|
if (!handlerEditedTools)
|
|
1101
1419
|
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1420
|
+
const normalized = await this._normalizePromptImages(currentImages);
|
|
1421
|
+
const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
|
|
1422
|
+
// Build messages only after hooks and image normalization have completed.
|
|
1423
|
+
messages = [];
|
|
1424
|
+
const userContent = [{ type: "text", text: userText }];
|
|
1425
|
+
userContent.push(...normalized.images);
|
|
1426
|
+
messages.push({
|
|
1427
|
+
role: "user",
|
|
1428
|
+
content: userContent,
|
|
1429
|
+
timestamp: Date.now(),
|
|
1430
|
+
});
|
|
1431
|
+
// Inject any pending "nextTurn" messages as context alongside the user message.
|
|
1432
|
+
// They stay queued until _persistMessage() confirms the append.
|
|
1433
|
+
for (const msg of this._pendingNextTurnMessages) {
|
|
1434
|
+
messages.push(msg);
|
|
1435
|
+
}
|
|
1102
1436
|
for (const msg of result.messages) {
|
|
1103
1437
|
messages.push({
|
|
1104
1438
|
role: "custom",
|
|
@@ -1123,6 +1457,10 @@ export class AgentSession {
|
|
|
1123
1457
|
return;
|
|
1124
1458
|
}
|
|
1125
1459
|
preflightResult?.(true);
|
|
1460
|
+
// The user's message is now established, so a completion may join this run through the
|
|
1461
|
+
// steering queue instead of waiting for the turn to end. Everything above still holds
|
|
1462
|
+
// the pause, so no delivery can race input hooks, model validation or compaction.
|
|
1463
|
+
resumeBackground();
|
|
1126
1464
|
await this._runAgentPrompt(messages);
|
|
1127
1465
|
}
|
|
1128
1466
|
finally {
|
|
@@ -1251,11 +1589,7 @@ export class AgentSession {
|
|
|
1251
1589
|
if (images) {
|
|
1252
1590
|
content.push(...images);
|
|
1253
1591
|
}
|
|
1254
|
-
this.agent.followUp({
|
|
1255
|
-
role: "user",
|
|
1256
|
-
content,
|
|
1257
|
-
timestamp: Date.now(),
|
|
1258
|
-
});
|
|
1592
|
+
this.agent.followUp({ role: "user", content, timestamp: Date.now() });
|
|
1259
1593
|
}
|
|
1260
1594
|
/**
|
|
1261
1595
|
* Throw an error if the text is an extension command.
|
|
@@ -1303,6 +1637,10 @@ export class AgentSession {
|
|
|
1303
1637
|
}
|
|
1304
1638
|
}
|
|
1305
1639
|
else if (options?.triggerTurn) {
|
|
1640
|
+
if (this._isEmittingAgentSettled) {
|
|
1641
|
+
this._deferredSettledActions.push(async () => await this._runAgentPrompt(appMessage));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1306
1644
|
await this._runAgentPrompt(appMessage);
|
|
1307
1645
|
}
|
|
1308
1646
|
else if (this.isStreaming) {
|
|
@@ -1317,8 +1655,8 @@ export class AgentSession {
|
|
|
1317
1655
|
}
|
|
1318
1656
|
}
|
|
1319
1657
|
_appendCustomMessage(appMessage) {
|
|
1320
|
-
this.agent.state.messages.push(appMessage);
|
|
1321
1658
|
this._persistMessage(appMessage);
|
|
1659
|
+
this._refreshFinalizedContext();
|
|
1322
1660
|
this._backgroundHost.messageAcknowledgement(appMessage)?.();
|
|
1323
1661
|
this._emit({ type: "message_start", message: appMessage });
|
|
1324
1662
|
this._emit({ type: "message_end", message: appMessage });
|
|
@@ -1384,6 +1722,10 @@ export class AgentSession {
|
|
|
1384
1722
|
this._steeringMessages = [];
|
|
1385
1723
|
this._followUpMessages = [];
|
|
1386
1724
|
this.agent.clearAllQueues();
|
|
1725
|
+
// Clearing the queues drops queued completions; fail them so their claims retry.
|
|
1726
|
+
for (const queued of [...this._queuedBackgroundCompletions.values()]) {
|
|
1727
|
+
queued.reject(new Error("the completion message was dequeued before delivery"));
|
|
1728
|
+
}
|
|
1387
1729
|
this._emitQueueUpdate();
|
|
1388
1730
|
return { steering, followUp };
|
|
1389
1731
|
}
|
|
@@ -1412,6 +1754,8 @@ export class AgentSession {
|
|
|
1412
1754
|
this.abortRetry();
|
|
1413
1755
|
this.abortCompaction();
|
|
1414
1756
|
this.abortBranchSummary();
|
|
1757
|
+
if (this._isBeforeSettle)
|
|
1758
|
+
this._abortDuringBeforeSettle = true;
|
|
1415
1759
|
this.agent.abort();
|
|
1416
1760
|
await this.waitForIdle();
|
|
1417
1761
|
}
|
|
@@ -1732,9 +2076,8 @@ export class AgentSession {
|
|
|
1732
2076
|
}
|
|
1733
2077
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
1734
2078
|
const newEntries = this.sessionManager.getEntries();
|
|
1735
|
-
|
|
1736
|
-
this.
|
|
1737
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2079
|
+
this._refreshFinalizedContext();
|
|
2080
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
1738
2081
|
// Get the saved compaction entry for the extension event
|
|
1739
2082
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1740
2083
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -1829,7 +2172,7 @@ export class AgentSession {
|
|
|
1829
2172
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1830
2173
|
* @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
|
|
1831
2174
|
*/
|
|
1832
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
2175
|
+
async _checkCompaction(assistantMessage, skipAbortedCheck = true, toolResults = []) {
|
|
1833
2176
|
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
1834
2177
|
if (!settings.enabled)
|
|
1835
2178
|
return false;
|
|
@@ -1853,8 +2196,27 @@ export class AgentSession {
|
|
|
1853
2196
|
// Automatic cases 1 and 2: context overflow.
|
|
1854
2197
|
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
1855
2198
|
// independent of the configured context size or any context-clamped provider request limit.
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
2199
|
+
const currentProjection = this.sessionManager.buildSessionProjection();
|
|
2200
|
+
const assistantEntryId = this._findPersistedMessageEntryId(assistantMessage);
|
|
2201
|
+
const assistantIsProjected = assistantEntryId === undefined ||
|
|
2202
|
+
currentProjection.entries.some((entry) => entry.sourceEntry.id === assistantEntryId &&
|
|
2203
|
+
entry.messages.some((message) => message.role === "assistant"));
|
|
2204
|
+
const branch = this.sessionManager.getBranch();
|
|
2205
|
+
const assistantIndex = assistantEntryId ? branch.findIndex((entry) => entry.id === assistantEntryId) : -1;
|
|
2206
|
+
const entriesAfterAssistant = assistantIndex >= 0 ? branch.slice(assistantIndex + 1) : [];
|
|
2207
|
+
const hasPostAssistantContextEdit = entriesAfterAssistant.some((entry) => entry.type === "context_edit");
|
|
2208
|
+
const latestAssistantEdit = entriesAfterAssistant
|
|
2209
|
+
.filter((entry) => entry.type === "context_edit" && entry.targetId === assistantEntryId)
|
|
2210
|
+
.at(-1);
|
|
2211
|
+
const assistantRetainedForExplicitRecovery = assistantEntryId === undefined ||
|
|
2212
|
+
(!entriesAfterAssistant.some((entry) => entry.type === "compaction") &&
|
|
2213
|
+
latestAssistantEdit?.replacement !== null);
|
|
2214
|
+
const assistantUsageMatchesProjection = assistantIsProjected && !hasPostAssistantContextEdit;
|
|
2215
|
+
const explicitOverflow = assistantMessage.stopReason === "error" && isContextOverflow(assistantMessage);
|
|
2216
|
+
const contextOverflow = sameModel &&
|
|
2217
|
+
((explicitOverflow && assistantRetainedForExplicitRecovery) ||
|
|
2218
|
+
(assistantUsageMatchesProjection && isContextOverflow(assistantMessage, contextWindow)));
|
|
2219
|
+
const recoverableLength = sameModel && assistantIsProjected && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
1858
2220
|
if (contextOverflow || recoverableLength) {
|
|
1859
2221
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1860
2222
|
// Case 2: the response completed successfully. Compact, but do not retry because
|
|
@@ -1883,13 +2245,9 @@ export class AgentSession {
|
|
|
1883
2245
|
});
|
|
1884
2246
|
return false;
|
|
1885
2247
|
}
|
|
1886
|
-
//
|
|
1887
|
-
// retry once. The message remains in session history but is excluded from retry context.
|
|
2248
|
+
// Persistently omit the selected final attempt before post-run recovery compaction.
|
|
1888
2249
|
this._overflowRecoveryAttempted = true;
|
|
1889
|
-
|
|
1890
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
1891
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1892
|
-
}
|
|
2250
|
+
this._omitRecoveryAttempt(assistantMessage, toolResults);
|
|
1893
2251
|
return await this._runAutoCompaction("overflow", willRetry);
|
|
1894
2252
|
}
|
|
1895
2253
|
// Case 3: threshold compaction without retry.
|
|
@@ -1897,8 +2255,13 @@ export class AgentSession {
|
|
|
1897
2255
|
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
1898
2256
|
// responses can still compact and do not reset context accounting.
|
|
1899
2257
|
let contextTokens;
|
|
2258
|
+
const projection = currentProjection;
|
|
2259
|
+
const hasContextEdits = projection.entries.some((entry) => entry.sourceEntry.type === "context_edit");
|
|
1900
2260
|
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
1901
|
-
if (
|
|
2261
|
+
if (hasContextEdits) {
|
|
2262
|
+
contextTokens = estimateProjectedContextTokens(projection, branch).tokens;
|
|
2263
|
+
}
|
|
2264
|
+
else if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
1902
2265
|
const messages = this.agent.state.messages;
|
|
1903
2266
|
const estimate = estimateContextTokens(messages);
|
|
1904
2267
|
// Without provider usage, estimate.tokens is the pure message-size estimate.
|
|
@@ -2005,9 +2368,8 @@ export class AgentSession {
|
|
|
2005
2368
|
abortController.signal.throwIfAborted();
|
|
2006
2369
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2007
2370
|
const newEntries = this.sessionManager.getEntries();
|
|
2008
|
-
|
|
2009
|
-
this.
|
|
2010
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2371
|
+
this._refreshFinalizedContext();
|
|
2372
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
2011
2373
|
// Get the saved compaction entry for the extension event
|
|
2012
2374
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
2013
2375
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -2028,19 +2390,8 @@ export class AgentSession {
|
|
|
2028
2390
|
details,
|
|
2029
2391
|
};
|
|
2030
2392
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
|
2031
|
-
if (willRetry)
|
|
2032
|
-
const messages = this.agent.state.messages;
|
|
2033
|
-
const lastMsg = messages[messages.length - 1];
|
|
2034
|
-
// The overflow response was persisted on message_end before _checkCompaction() removed it
|
|
2035
|
-
// from agent state. Rebuilding state from the new compaction can restore that kept entry,
|
|
2036
|
-
// leaving an assistant as the final message. agent.continue() rejects that state, so remove
|
|
2037
|
-
// the retriable error or truncated-length response again before continuing the interrupted turn.
|
|
2038
|
-
if (lastMsg?.role === "assistant" &&
|
|
2039
|
-
(lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
|
|
2040
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2041
|
-
}
|
|
2393
|
+
if (willRetry)
|
|
2042
2394
|
return true;
|
|
2043
|
-
}
|
|
2044
2395
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
2045
2396
|
// Continue once so queued messages are delivered.
|
|
2046
2397
|
return this.agent.hasQueuedMessages();
|
|
@@ -2524,11 +2875,8 @@ export class AgentSession {
|
|
|
2524
2875
|
delayMs,
|
|
2525
2876
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2526
2877
|
});
|
|
2527
|
-
//
|
|
2528
|
-
|
|
2529
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2530
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2531
|
-
}
|
|
2878
|
+
// Keep the failed attempt in raw history while durably omitting it from model projection.
|
|
2879
|
+
this._omitRecoveryAttempt(message);
|
|
2532
2880
|
// Wait with exponential backoff (abortable)
|
|
2533
2881
|
this._retryAbortController = new AbortController();
|
|
2534
2882
|
try {
|
|
@@ -2620,10 +2968,8 @@ export class AgentSession {
|
|
|
2620
2968
|
this._pendingBashMessages.push(bashMessage);
|
|
2621
2969
|
}
|
|
2622
2970
|
else {
|
|
2623
|
-
// Add to agent state immediately
|
|
2624
|
-
this.agent.state.messages.push(bashMessage);
|
|
2625
|
-
// Save to session
|
|
2626
2971
|
this.sessionManager.appendMessage(bashMessage);
|
|
2972
|
+
this._refreshFinalizedContext();
|
|
2627
2973
|
}
|
|
2628
2974
|
}
|
|
2629
2975
|
/**
|
|
@@ -2650,12 +2996,10 @@ export class AgentSession {
|
|
|
2650
2996
|
if (this._pendingBashMessages.length === 0)
|
|
2651
2997
|
return;
|
|
2652
2998
|
for (const bashMessage of this._pendingBashMessages) {
|
|
2653
|
-
// Add to agent state
|
|
2654
|
-
this.agent.state.messages.push(bashMessage);
|
|
2655
|
-
// Save to session
|
|
2656
2999
|
this.sessionManager.appendMessage(bashMessage);
|
|
2657
3000
|
}
|
|
2658
3001
|
this._pendingBashMessages = [];
|
|
3002
|
+
this._refreshFinalizedContext();
|
|
2659
3003
|
}
|
|
2660
3004
|
// =========================================================================
|
|
2661
3005
|
// Session Management
|
|
@@ -2832,9 +3176,8 @@ export class AgentSession {
|
|
|
2832
3176
|
this.sessionManager.appendLabelChange(targetId, label);
|
|
2833
3177
|
}
|
|
2834
3178
|
this._backgroundHost.restoreHistory();
|
|
2835
|
-
// Update
|
|
2836
|
-
|
|
2837
|
-
this.agent.state.messages = sessionContext.messages;
|
|
3179
|
+
// Update finalized context from the canonical session projection.
|
|
3180
|
+
this._refreshFinalizedContext();
|
|
2838
3181
|
this._restoreToolsFromTranscript();
|
|
2839
3182
|
// Emit session_tree event
|
|
2840
3183
|
await this._extensionRunner.emit({
|
|
@@ -2934,30 +3277,24 @@ export class AgentSession {
|
|
|
2934
3277
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
2935
3278
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
2936
3279
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
3280
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
3281
|
+
const branch = this.sessionManager.getBranch();
|
|
3282
|
+
const latestCompaction = getLatestCompactionEntry(branch);
|
|
2939
3283
|
if (latestCompaction) {
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
break;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
if (!hasPostCompactionUsage) {
|
|
3284
|
+
const projectedAssistants = new Set(projection.entries.flatMap((entry) => entry.messages.some((message) => message.role === "assistant" &&
|
|
3285
|
+
message.stopReason !== "aborted" &&
|
|
3286
|
+
message.stopReason !== "error" &&
|
|
3287
|
+
calculateContextTokens(message.usage) > 0)
|
|
3288
|
+
? [entry.sourceEntry.id]
|
|
3289
|
+
: []));
|
|
3290
|
+
const compactionIndex = branch.findIndex((entry) => entry.id === latestCompaction.id);
|
|
3291
|
+
const hasPostCompactionUsage = branch
|
|
3292
|
+
.slice(compactionIndex + 1)
|
|
3293
|
+
.some((entry) => projectedAssistants.has(entry.id));
|
|
3294
|
+
if (!hasPostCompactionUsage)
|
|
2957
3295
|
return { tokens: null, contextWindow, percent: null };
|
|
2958
|
-
}
|
|
2959
3296
|
}
|
|
2960
|
-
const estimate =
|
|
3297
|
+
const estimate = estimateProjectedContextTokens(projection, branch);
|
|
2961
3298
|
const percent = (estimate.tokens / contextWindow) * 100;
|
|
2962
3299
|
return {
|
|
2963
3300
|
tokens: estimate.tokens,
|