@gajae-code/coding-agent 0.8.2 → 0.9.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 +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
type AgentMessage,
|
|
27
27
|
type AgentState,
|
|
28
28
|
type AgentTool,
|
|
29
|
-
AppendOnlyContextManager,
|
|
30
29
|
resolveTelemetry,
|
|
31
30
|
type StablePrefixSnapshot,
|
|
32
31
|
ThinkingLevel,
|
|
@@ -44,16 +43,20 @@ import {
|
|
|
44
43
|
type EmergencyCompactionSample,
|
|
45
44
|
emergencyCompactionReason,
|
|
46
45
|
estimateMessageTokensHeuristic,
|
|
46
|
+
estimateTextTokensHeuristic,
|
|
47
47
|
generateBranchSummary,
|
|
48
48
|
generateHandoff,
|
|
49
|
+
IMAGE_TOKEN_ESTIMATE,
|
|
49
50
|
prepareCompaction,
|
|
50
51
|
type SummaryOptions,
|
|
51
52
|
shouldCompact,
|
|
52
53
|
} from "@gajae-code/agent-core/compaction";
|
|
53
54
|
import {
|
|
54
55
|
DEFAULT_PRUNE_CONFIG,
|
|
56
|
+
estimateToolOutputPruneSavings,
|
|
55
57
|
pruneAssistantToolArguments,
|
|
56
58
|
pruneToolOutputs,
|
|
59
|
+
shouldRunMaintenancePrune,
|
|
57
60
|
} from "@gajae-code/agent-core/compaction/pruning";
|
|
58
61
|
import type {
|
|
59
62
|
AssistantMessage,
|
|
@@ -128,6 +131,7 @@ import {
|
|
|
128
131
|
prompt,
|
|
129
132
|
Snowflake,
|
|
130
133
|
} from "@gajae-code/utils";
|
|
134
|
+
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
131
135
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
132
136
|
import { reset as resetCapabilities } from "../capability";
|
|
133
137
|
import type { Rule } from "../capability/rule";
|
|
@@ -238,6 +242,7 @@ import {
|
|
|
238
242
|
} from "../skill-state/active-state";
|
|
239
243
|
import { assertWorkflowMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
240
244
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
245
|
+
import { buildVolatileProjectContext } from "../system-prompt";
|
|
241
246
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
242
247
|
import {
|
|
243
248
|
buildDiscoverableToolSearchIndex,
|
|
@@ -252,7 +257,7 @@ import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
|
252
257
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
253
258
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
254
259
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
255
|
-
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
260
|
+
import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
|
|
256
261
|
import { registerResourceGcSession } from "../tools/resource-gc";
|
|
257
262
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
|
|
258
263
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
@@ -264,6 +269,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
264
269
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
265
270
|
import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
266
271
|
import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
|
|
272
|
+
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
267
273
|
import type { AuthStorage } from "./auth-storage";
|
|
268
274
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
269
275
|
import {
|
|
@@ -271,6 +277,7 @@ import {
|
|
|
271
277
|
type ContributionPrepResult,
|
|
272
278
|
prepareContributionPrep,
|
|
273
279
|
} from "./contribution-prep";
|
|
280
|
+
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
274
281
|
import {
|
|
275
282
|
type BashExecutionMessage,
|
|
276
283
|
type CompactionSummaryMessage,
|
|
@@ -396,6 +403,8 @@ export interface AgentSessionConfig {
|
|
|
396
403
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
397
404
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
398
405
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
406
|
+
/** Initial workspace tree snapshot used for the first volatile per-turn context message. */
|
|
407
|
+
workspaceTree?: WorkspaceTree;
|
|
399
408
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
400
409
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
401
410
|
requestedToolNames?: ReadonlySet<string>;
|
|
@@ -616,6 +625,13 @@ const IRC_REPLY_MAX_BYTES = 4096;
|
|
|
616
625
|
*/
|
|
617
626
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
618
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Throttle window for the per-turn volatile workspace-tree scan. Date/cwd are
|
|
630
|
+
* refreshed every turn; the mtime-sorted tree is rescanned + re-embedded at most
|
|
631
|
+
* once per this interval to bound prompt-hot-path IO and history accumulation.
|
|
632
|
+
*/
|
|
633
|
+
const VOLATILE_TREE_TTL_MS = 30_000;
|
|
634
|
+
|
|
619
635
|
/**
|
|
620
636
|
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
621
637
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
@@ -936,6 +952,136 @@ export type BeforeAgentStartContributor = (event: {
|
|
|
936
952
|
sessionId: string | undefined;
|
|
937
953
|
}) => Promise<BeforeAgentStartInternalMessage | undefined>;
|
|
938
954
|
|
|
955
|
+
export class WorkerIntegrationRequestScheduler {
|
|
956
|
+
#inFlight: Promise<void> | undefined = undefined;
|
|
957
|
+
#pending = false;
|
|
958
|
+
|
|
959
|
+
constructor(readonly request: () => Promise<void>) {}
|
|
960
|
+
|
|
961
|
+
enqueue(): void {
|
|
962
|
+
if (this.#inFlight) {
|
|
963
|
+
this.#pending = true;
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
this.#start();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async flush(): Promise<void> {
|
|
970
|
+
while (this.#inFlight) {
|
|
971
|
+
await this.#inFlight;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
#start(): void {
|
|
976
|
+
this.#pending = false;
|
|
977
|
+
// Isolate request rejections so flush() can never reject or deadlock: the
|
|
978
|
+
// request is expected to handle/log its own errors, but a throwing generic
|
|
979
|
+
// request must still clear #inFlight and drain any trailing pending run.
|
|
980
|
+
this.#inFlight = this.request()
|
|
981
|
+
.catch(() => {})
|
|
982
|
+
.finally(() => {
|
|
983
|
+
this.#inFlight = undefined;
|
|
984
|
+
if (this.#pending) this.#start();
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export type StreamingEditParsedToolCall = {
|
|
990
|
+
toolCall: ToolCall;
|
|
991
|
+
path: string;
|
|
992
|
+
resolvedPath: string;
|
|
993
|
+
diff?: string;
|
|
994
|
+
op?: string;
|
|
995
|
+
rename?: string;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
export type StreamingEditParsedCacheEntry = {
|
|
999
|
+
version: string;
|
|
1000
|
+
parsed: StreamingEditParsedToolCall | undefined;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
function stableStreamingEditArgsVersion(args: unknown): string | undefined {
|
|
1004
|
+
if (typeof args === "string") return args;
|
|
1005
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.stringify(args);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function getStreamingEditToolCallForEvent(
|
|
1014
|
+
event: AgentEvent,
|
|
1015
|
+
cache: Map<string, StreamingEditParsedCacheEntry>,
|
|
1016
|
+
resolvePath: (filePath: string) => string | undefined,
|
|
1017
|
+
): StreamingEditParsedToolCall | undefined {
|
|
1018
|
+
if (event.type !== "message_update") return undefined;
|
|
1019
|
+
if (event.message.role !== "assistant") return undefined;
|
|
1020
|
+
|
|
1021
|
+
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
1022
|
+
const messageContent = event.message.content;
|
|
1023
|
+
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
1024
|
+
return undefined;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
1028
|
+
if (toolCall.name !== "edit") return undefined;
|
|
1029
|
+
|
|
1030
|
+
const version = stableStreamingEditArgsVersion(toolCall.arguments);
|
|
1031
|
+
if (version === undefined) return undefined;
|
|
1032
|
+
const cacheKey = String(contentIndex);
|
|
1033
|
+
const cached = cache.get(cacheKey);
|
|
1034
|
+
if (cached?.version === version) return cached.parsed;
|
|
1035
|
+
|
|
1036
|
+
let args: unknown = toolCall.arguments;
|
|
1037
|
+
if (typeof args === "string") {
|
|
1038
|
+
try {
|
|
1039
|
+
args = JSON.parse(args) as unknown;
|
|
1040
|
+
} catch {
|
|
1041
|
+
cache.delete(cacheKey);
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
1046
|
+
if ("old_text" in args || "new_text" in args) return undefined;
|
|
1047
|
+
|
|
1048
|
+
const argsRecord = args as Record<string, unknown>;
|
|
1049
|
+
const path = typeof argsRecord.path === "string" ? argsRecord.path : undefined;
|
|
1050
|
+
if (!path) return undefined;
|
|
1051
|
+
const resolvedPath = resolvePath(path);
|
|
1052
|
+
if (resolvedPath === undefined) return undefined;
|
|
1053
|
+
|
|
1054
|
+
const parsed = {
|
|
1055
|
+
toolCall,
|
|
1056
|
+
path,
|
|
1057
|
+
resolvedPath,
|
|
1058
|
+
diff: typeof argsRecord.diff === "string" ? argsRecord.diff : undefined,
|
|
1059
|
+
op: typeof argsRecord.op === "string" ? argsRecord.op : undefined,
|
|
1060
|
+
rename: typeof argsRecord.rename === "string" ? argsRecord.rename : undefined,
|
|
1061
|
+
};
|
|
1062
|
+
cache.set(cacheKey, { version, parsed });
|
|
1063
|
+
return parsed;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Test-only counters for AgentSession event fan-out hot-path assertions. */
|
|
1067
|
+
export const __agentSessionPerfCounters = {
|
|
1068
|
+
listenerSnapshotRebuilds: 0,
|
|
1069
|
+
messageUpdateExtensionQueues: 0,
|
|
1070
|
+
reset(): void {
|
|
1071
|
+
this.listenerSnapshotRebuilds = 0;
|
|
1072
|
+
this.messageUpdateExtensionQueues = 0;
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
export function buildContextInjectionSignature(kind: string, parts: readonly string[]): string {
|
|
1077
|
+
const hash = crypto.createHash("sha256");
|
|
1078
|
+
hash.update(kind);
|
|
1079
|
+
for (const part of parts) {
|
|
1080
|
+
hash.update("\0");
|
|
1081
|
+
hash.update(part);
|
|
1082
|
+
}
|
|
1083
|
+
return hash.digest("base64url");
|
|
1084
|
+
}
|
|
939
1085
|
export class AgentSession {
|
|
940
1086
|
readonly agent: Agent;
|
|
941
1087
|
readonly sessionManager: SessionManager;
|
|
@@ -959,6 +1105,12 @@ export class AgentSession {
|
|
|
959
1105
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
960
1106
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
961
1107
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
1108
|
+
#eventListenerSnapshot: readonly AgentSessionEventListener[] = Object.freeze([]);
|
|
1109
|
+
|
|
1110
|
+
#rebuildEventListenerSnapshot(): void {
|
|
1111
|
+
this.#eventListenerSnapshot = Object.freeze([...this.#eventListeners]);
|
|
1112
|
+
__agentSessionPerfCounters.listenerSnapshotRebuilds += 1;
|
|
1113
|
+
}
|
|
962
1114
|
|
|
963
1115
|
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
964
1116
|
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
@@ -977,13 +1129,18 @@ export class AgentSession {
|
|
|
977
1129
|
#goalModeState: GoalModeState | undefined;
|
|
978
1130
|
#workflowGateEmitter: WorkflowGateEmitter | undefined;
|
|
979
1131
|
#goalRuntime: GoalRuntime;
|
|
1132
|
+
#lastInjectedGoalContextSig: string | undefined = undefined;
|
|
1133
|
+
#lastInjectedPlanContextSig: string | undefined = undefined;
|
|
980
1134
|
#goalTurnCounter = 0;
|
|
1135
|
+
#streamingEditParsedToolCallCache = new Map<string, StreamingEditParsedCacheEntry>();
|
|
981
1136
|
#planReferenceSent = false;
|
|
982
1137
|
#planReferencePath = "local://PLAN.md";
|
|
983
1138
|
#clientBridge: ClientBridge | undefined;
|
|
984
1139
|
#allowAcpAgentInitiatedTurns = false;
|
|
985
1140
|
/** Per-session memory of allow_always / reject_always decisions for gated tools. */
|
|
986
1141
|
#acpPermissionDecisions: Map<string, "allow_always" | "reject_always"> = new Map();
|
|
1142
|
+
#guardedToolWrapperCache = new WeakMap<AgentTool, Map<string, AgentTool>>();
|
|
1143
|
+
#acpPermissionWrapperVersion = 0;
|
|
987
1144
|
|
|
988
1145
|
// Compaction state
|
|
989
1146
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
@@ -1045,6 +1202,11 @@ export class AgentSession {
|
|
|
1045
1202
|
// Extension system
|
|
1046
1203
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
1047
1204
|
#turnIndex = 0;
|
|
1205
|
+
#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(async () => {
|
|
1206
|
+
await requestGjcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env).catch(error => {
|
|
1207
|
+
logger.warn("GJC team worker integration request failed", { error: String(error) });
|
|
1208
|
+
});
|
|
1209
|
+
});
|
|
1048
1210
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1049
1211
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1050
1212
|
|
|
@@ -1077,6 +1239,10 @@ export class AgentSession {
|
|
|
1077
1239
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1078
1240
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1079
1241
|
#baseSystemPrompt: string[];
|
|
1242
|
+
#initialWorkspaceTree: WorkspaceTree | undefined;
|
|
1243
|
+
/** Throttle cache for the per-turn volatile workspace-tree scan (see #buildVolatileProjectContextMessage). */
|
|
1244
|
+
#cachedWorkspaceTree: WorkspaceTree | undefined;
|
|
1245
|
+
#cachedWorkspaceTreeAt = 0;
|
|
1080
1246
|
/**
|
|
1081
1247
|
* Signature of the (toolNames, tool descriptions) tuple passed to the most
|
|
1082
1248
|
* recent successful `rebuildSystemPrompt` call. Used to skip redundant rebuilds
|
|
@@ -1136,10 +1302,31 @@ export class AgentSession {
|
|
|
1136
1302
|
|
|
1137
1303
|
#streamingEditAbortTriggered = false;
|
|
1138
1304
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
1305
|
+
#streamingEditToolCallStates = new Map<
|
|
1306
|
+
string,
|
|
1307
|
+
{
|
|
1308
|
+
op?: string;
|
|
1309
|
+
resolvedPath?: string;
|
|
1310
|
+
lastProcessedOffset: number;
|
|
1311
|
+
processedPrefix: string;
|
|
1312
|
+
settledVerdict?: "aborted" | "non-edit" | "non-update";
|
|
1313
|
+
debugProcessedChars: number;
|
|
1314
|
+
debugCheckedRemovedLines: number;
|
|
1315
|
+
debugFullChecks: number;
|
|
1316
|
+
debugGuardRuns: number;
|
|
1317
|
+
}
|
|
1318
|
+
>();
|
|
1139
1319
|
|
|
1140
1320
|
#streamingEditPrecheckedToolCallIds = new Set<string>();
|
|
1141
1321
|
|
|
1142
1322
|
#streamingEditFileCache = new Map<string, string>();
|
|
1323
|
+
readonly streamingEditDebugCounters = {
|
|
1324
|
+
guardRuns: 0,
|
|
1325
|
+
processedChars: 0,
|
|
1326
|
+
checkedRemovedLines: 0,
|
|
1327
|
+
fullChecks: 0,
|
|
1328
|
+
nonEditDeterminations: 0,
|
|
1329
|
+
};
|
|
1143
1330
|
#promptInFlightCount = 0;
|
|
1144
1331
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
1145
1332
|
// Local subscribers and extension hooks both receive the deferred event from
|
|
@@ -1327,6 +1514,7 @@ export class AgentSession {
|
|
|
1327
1514
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1328
1515
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1329
1516
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1517
|
+
this.#initialWorkspaceTree = config.workspaceTree;
|
|
1330
1518
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1331
1519
|
this.#discoverableToolAllowedNames = config.discoverableToolAllowedNames
|
|
1332
1520
|
? new Set(config.discoverableToolAllowedNames.map(name => name.toLowerCase()))
|
|
@@ -1357,16 +1545,6 @@ export class AgentSession {
|
|
|
1357
1545
|
this.#agentRegistry = config.agentRegistry;
|
|
1358
1546
|
this.#providerSessionId = config.providerSessionId;
|
|
1359
1547
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
1360
|
-
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1361
|
-
const event: AgentEvent = {
|
|
1362
|
-
type: "message_update",
|
|
1363
|
-
message,
|
|
1364
|
-
assistantMessageEvent,
|
|
1365
|
-
};
|
|
1366
|
-
const generation = this.#promptGeneration;
|
|
1367
|
-
this.#preCacheStreamingEditFile(event);
|
|
1368
|
-
this.#maybeAbortStreamingEdit(event, generation);
|
|
1369
|
-
});
|
|
1370
1548
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
1371
1549
|
this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
|
|
1372
1550
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
@@ -1514,13 +1692,30 @@ export class AgentSession {
|
|
|
1514
1692
|
}
|
|
1515
1693
|
|
|
1516
1694
|
async buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed> {
|
|
1695
|
+
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1696
|
+
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1697
|
+
if (maxMessages <= 0 || maxTokens <= 0) {
|
|
1698
|
+
return {
|
|
1699
|
+
messages: [],
|
|
1700
|
+
agentMessages: [],
|
|
1701
|
+
metadata: {
|
|
1702
|
+
sourceSessionId: this.sessionId,
|
|
1703
|
+
parentMessageCount: this.messages.length,
|
|
1704
|
+
includedMessages: 0,
|
|
1705
|
+
skippedMessages: 0,
|
|
1706
|
+
approximateTokens: 0,
|
|
1707
|
+
maxMessages,
|
|
1708
|
+
maxTokens,
|
|
1709
|
+
skippedReasons: {},
|
|
1710
|
+
},
|
|
1711
|
+
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1517
1714
|
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
1518
1715
|
const convertedMessages = await this.#convertToLlm(transformedMessages);
|
|
1519
1716
|
const providerMessages = this.model
|
|
1520
1717
|
? normalizeMessagesForProvider(convertedMessages, this.model)
|
|
1521
1718
|
: convertedMessages;
|
|
1522
|
-
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1523
|
-
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1524
1719
|
const selected: Message[] = [];
|
|
1525
1720
|
const skippedReasons: Record<string, number> = {};
|
|
1526
1721
|
let skippedMessages = 0;
|
|
@@ -1567,6 +1762,33 @@ export class AgentSession {
|
|
|
1567
1762
|
return cloned;
|
|
1568
1763
|
};
|
|
1569
1764
|
|
|
1765
|
+
const truncateMessageToTokenBudget = (message: Message): { message: Message; tokens: number } => {
|
|
1766
|
+
const notice = `\n\n[fork-context seed: newest message truncated to fit the ${maxTokens}-token budget]`;
|
|
1767
|
+
const contentText = Array.isArray(message.content)
|
|
1768
|
+
? message.content.map(block => (block.type === "text" ? block.text : "")).join("\n\n")
|
|
1769
|
+
: String(message.content ?? "");
|
|
1770
|
+
let low = 0;
|
|
1771
|
+
let high = contentText.length;
|
|
1772
|
+
let bestMessage: Message = { ...message, content: [{ type: "text", text: notice.trimStart() }] };
|
|
1773
|
+
let bestTokens = estimateMessageTokensHeuristic(bestMessage);
|
|
1774
|
+
while (low <= high) {
|
|
1775
|
+
const mid = Math.floor((low + high) / 2);
|
|
1776
|
+
const candidate: Message = {
|
|
1777
|
+
...message,
|
|
1778
|
+
content: [{ type: "text", text: `${contentText.slice(0, mid)}${notice}` }],
|
|
1779
|
+
};
|
|
1780
|
+
const candidateTokens = estimateMessageTokensHeuristic(candidate);
|
|
1781
|
+
if (candidateTokens <= maxTokens) {
|
|
1782
|
+
bestMessage = candidate;
|
|
1783
|
+
bestTokens = candidateTokens;
|
|
1784
|
+
low = mid + 1;
|
|
1785
|
+
} else {
|
|
1786
|
+
high = mid - 1;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return { message: bestMessage, tokens: bestTokens };
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1570
1792
|
for (let i = providerMessages.length - 1; i >= 0; i--) {
|
|
1571
1793
|
if (selected.length >= maxMessages) {
|
|
1572
1794
|
recordSkip("message-limit");
|
|
@@ -1580,8 +1802,16 @@ export class AgentSession {
|
|
|
1580
1802
|
// messages. `continue` here would skip the oversized recent message and scavenge
|
|
1581
1803
|
// smaller OLDER ones, yielding a non-contiguous seed that misrepresents the
|
|
1582
1804
|
// conversation and violates the recency contract of the receipt/last-turn/bounded modes.
|
|
1583
|
-
if (
|
|
1805
|
+
if (approximateTokens + messageTokens > maxTokens) {
|
|
1584
1806
|
recordSkip("token-limit");
|
|
1807
|
+
if (selected.length === 0) {
|
|
1808
|
+
const truncated = truncateMessageToTokenBudget(sanitized);
|
|
1809
|
+
if (truncated.tokens <= maxTokens) {
|
|
1810
|
+
selected.unshift(truncated.message);
|
|
1811
|
+
approximateTokens = truncated.tokens;
|
|
1812
|
+
skippedReasons["newest-message-truncated"] = (skippedReasons["newest-message-truncated"] ?? 0) + 1;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1585
1815
|
break;
|
|
1586
1816
|
}
|
|
1587
1817
|
selected.unshift(sanitized);
|
|
@@ -1736,15 +1966,23 @@ export class AgentSession {
|
|
|
1736
1966
|
manager.cancelAll({ ownerId: this.#agentId });
|
|
1737
1967
|
}
|
|
1738
1968
|
|
|
1969
|
+
#suppressOwnAsyncJobDeliveries(): void {
|
|
1970
|
+
if (!this.#agentId) return;
|
|
1971
|
+
const manager = AsyncJobManager.instance();
|
|
1972
|
+
if (!manager) return;
|
|
1973
|
+
const pendingJobIds = manager.getDeliveryState({ ownerId: this.#agentId }).pendingJobIds;
|
|
1974
|
+
if (pendingJobIds.length > 0) {
|
|
1975
|
+
manager.acknowledgeDeliveries(pendingJobIds);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1739
1979
|
// =========================================================================
|
|
1740
1980
|
// Event Subscription
|
|
1741
1981
|
// =========================================================================
|
|
1742
1982
|
|
|
1743
1983
|
/** Emit an event to all listeners */
|
|
1744
1984
|
#emit(event: AgentSessionEvent): void {
|
|
1745
|
-
|
|
1746
|
-
const listeners = [...this.#eventListeners];
|
|
1747
|
-
for (const l of listeners) {
|
|
1985
|
+
for (const l of this.#eventListenerSnapshot) {
|
|
1748
1986
|
l(event);
|
|
1749
1987
|
}
|
|
1750
1988
|
}
|
|
@@ -1774,19 +2012,22 @@ export class AgentSession {
|
|
|
1774
2012
|
}
|
|
1775
2013
|
|
|
1776
2014
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
2015
|
+
if (event.type === "message_update") {
|
|
2016
|
+
// Fast path: message_update maps to no sidecar state, so we must not
|
|
2017
|
+
// build the persistRuntimeState closure here (per-token hot path).
|
|
2018
|
+
this.#emit(event);
|
|
2019
|
+
if (this.#hasStreamingExtensionHandlers()) {
|
|
2020
|
+
__agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
|
|
2021
|
+
void this.#queueExtensionEvent(event);
|
|
2022
|
+
}
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1777
2025
|
const persistRuntimeState = () =>
|
|
1778
2026
|
persistCoordinatorRuntimeStateFromEvent(event, {
|
|
1779
2027
|
sessionId: this.sessionId,
|
|
1780
2028
|
cwd: this.sessionManager.getCwd(),
|
|
1781
2029
|
sessionFile: this.sessionManager.getSessionFile(),
|
|
1782
2030
|
});
|
|
1783
|
-
|
|
1784
|
-
if (event.type === "message_update") {
|
|
1785
|
-
this.#emit(event);
|
|
1786
|
-
void persistRuntimeState();
|
|
1787
|
-
void this.#queueExtensionEvent(event);
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
2031
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
1791
2032
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
1792
2033
|
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
@@ -1893,7 +2134,7 @@ export class AgentSession {
|
|
|
1893
2134
|
}
|
|
1894
2135
|
}
|
|
1895
2136
|
|
|
1896
|
-
if (event.type === "turn_start") {
|
|
2137
|
+
if (event.type === "turn_start" && this.#goalRuntime.shouldTrackTurnBaseline()) {
|
|
1897
2138
|
const usage = this.getSessionStats().tokens;
|
|
1898
2139
|
this.#goalRuntime.onTurnStart(`turn-${++this.#goalTurnCounter}`, {
|
|
1899
2140
|
input: usage.input,
|
|
@@ -2645,8 +2886,10 @@ export class AgentSession {
|
|
|
2645
2886
|
// The TTSR manager was already claimed at bucket time; only persistence remains.
|
|
2646
2887
|
const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
|
|
2647
2888
|
if (ruleNames.length > 0) {
|
|
2648
|
-
this.
|
|
2889
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => ruleNames.includes(record.name));
|
|
2890
|
+
this.sessionManager.appendTtsrInjection(ruleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2649
2891
|
}
|
|
2892
|
+
|
|
2650
2893
|
return {
|
|
2651
2894
|
content: [{ type: "text", text: reminder }, ...ctx.result.content],
|
|
2652
2895
|
};
|
|
@@ -2671,7 +2914,8 @@ export class AgentSession {
|
|
|
2671
2914
|
return;
|
|
2672
2915
|
}
|
|
2673
2916
|
this.#ttsrManager?.markInjectedByNames(uniqueRuleNames);
|
|
2674
|
-
this.
|
|
2917
|
+
const records = this.#ttsrManager?.getInjectedRecords().filter(record => uniqueRuleNames.includes(record.name));
|
|
2918
|
+
this.sessionManager.appendTtsrInjection(uniqueRuleNames, records, this.#ttsrManager?.getMessageCount());
|
|
2675
2919
|
}
|
|
2676
2920
|
|
|
2677
2921
|
#findTtsrAssistantIndex(targetTimestamp: number | undefined): number {
|
|
@@ -2855,55 +3099,16 @@ export class AgentSession {
|
|
|
2855
3099
|
#resetStreamingEditState(): void {
|
|
2856
3100
|
this.#streamingEditAbortTriggered = false;
|
|
2857
3101
|
this.#streamingEditCheckedLineCounts.clear();
|
|
3102
|
+
this.#streamingEditToolCallStates.clear();
|
|
2858
3103
|
this.#streamingEditPrecheckedToolCallIds.clear();
|
|
3104
|
+
this.#streamingEditParsedToolCallCache.clear();
|
|
2859
3105
|
this.#streamingEditFileCache.clear();
|
|
2860
3106
|
}
|
|
2861
3107
|
|
|
2862
|
-
#getStreamingEditToolCall(event: AgentEvent):
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
resolvedPath: string;
|
|
2867
|
-
diff?: string;
|
|
2868
|
-
op?: string;
|
|
2869
|
-
rename?: string;
|
|
2870
|
-
}
|
|
2871
|
-
| undefined {
|
|
2872
|
-
if (event.type !== "message_update") return undefined;
|
|
2873
|
-
if (event.message.role !== "assistant") return undefined;
|
|
2874
|
-
|
|
2875
|
-
const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
|
|
2876
|
-
const messageContent = event.message.content;
|
|
2877
|
-
if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) {
|
|
2878
|
-
return undefined;
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
2882
|
-
if (toolCall.name !== "edit") return undefined;
|
|
2883
|
-
|
|
2884
|
-
const args = toolCall.arguments;
|
|
2885
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
2886
|
-
if ("old_text" in args || "new_text" in args) return undefined;
|
|
2887
|
-
|
|
2888
|
-
const path = typeof args.path === "string" ? args.path : undefined;
|
|
2889
|
-
if (!path) return undefined;
|
|
2890
|
-
|
|
2891
|
-
// `local://` URLs (e.g. local://PLAN.md for plan-mode) resolve to a real
|
|
2892
|
-
// on-disk artifacts path; pre-caching works as long as we ask the
|
|
2893
|
-
// local-protocol handler. Other internal-scheme URLs have no stable filesystem representation;
|
|
2894
|
-
// skip pre-cache entirely for those — the edit tool itself will reject
|
|
2895
|
-
// them through its normal dispatch path.
|
|
2896
|
-
const resolvedPath = this.#resolveSessionFsPath(path);
|
|
2897
|
-
if (resolvedPath === undefined) return undefined;
|
|
2898
|
-
|
|
2899
|
-
return {
|
|
2900
|
-
toolCall,
|
|
2901
|
-
path,
|
|
2902
|
-
resolvedPath,
|
|
2903
|
-
diff: typeof args.diff === "string" ? args.diff : undefined,
|
|
2904
|
-
op: typeof args.op === "string" ? args.op : undefined,
|
|
2905
|
-
rename: typeof args.rename === "string" ? args.rename : undefined,
|
|
2906
|
-
};
|
|
3108
|
+
#getStreamingEditToolCall(event: AgentEvent): StreamingEditParsedToolCall | undefined {
|
|
3109
|
+
return getStreamingEditToolCallForEvent(event, this.#streamingEditParsedToolCallCache, filePath =>
|
|
3110
|
+
this.#resolveSessionFsPath(filePath),
|
|
3111
|
+
);
|
|
2907
3112
|
}
|
|
2908
3113
|
|
|
2909
3114
|
#lastStreamingEditToolCallId: string | undefined;
|
|
@@ -3022,36 +3227,122 @@ export class AgentSession {
|
|
|
3022
3227
|
const assistantEvent = event.assistantMessageEvent;
|
|
3023
3228
|
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
3024
3229
|
|
|
3230
|
+
const contentIndex = assistantEvent.contentIndex ?? 0;
|
|
3231
|
+
const messageContent = event.message.role === "assistant" ? event.message.content : undefined;
|
|
3232
|
+
const candidateToolCall = Array.isArray(messageContent)
|
|
3233
|
+
? (messageContent[contentIndex] as ToolCall | undefined)
|
|
3234
|
+
: undefined;
|
|
3235
|
+
const candidateToolCallId = candidateToolCall?.type === "toolCall" ? candidateToolCall.id : undefined;
|
|
3236
|
+
if (candidateToolCallId) {
|
|
3237
|
+
const cached = this.#streamingEditToolCallStates.get(candidateToolCallId);
|
|
3238
|
+
if (
|
|
3239
|
+
cached?.settledVerdict === "aborted" ||
|
|
3240
|
+
cached?.settledVerdict === "non-edit" ||
|
|
3241
|
+
cached?.settledVerdict === "non-update"
|
|
3242
|
+
) {
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3025
3247
|
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
3026
|
-
if (!streamingEdit?.toolCall.id)
|
|
3248
|
+
if (!streamingEdit?.toolCall.id) {
|
|
3249
|
+
if (candidateToolCallId) {
|
|
3250
|
+
this.#streamingEditToolCallStates.set(candidateToolCallId, {
|
|
3251
|
+
lastProcessedOffset: 0,
|
|
3252
|
+
processedPrefix: "",
|
|
3253
|
+
settledVerdict: "non-edit",
|
|
3254
|
+
debugProcessedChars: 0,
|
|
3255
|
+
debugCheckedRemovedLines: 0,
|
|
3256
|
+
debugFullChecks: 0,
|
|
3257
|
+
debugGuardRuns: 0,
|
|
3258
|
+
});
|
|
3259
|
+
this.streamingEditDebugCounters.nonEditDeterminations += 1;
|
|
3260
|
+
}
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3027
3263
|
|
|
3028
3264
|
const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
|
|
3265
|
+
let state = this.#streamingEditToolCallStates.get(toolCall.id);
|
|
3266
|
+
if (!state) {
|
|
3267
|
+
state = {
|
|
3268
|
+
op,
|
|
3269
|
+
resolvedPath,
|
|
3270
|
+
lastProcessedOffset: 0,
|
|
3271
|
+
processedPrefix: "",
|
|
3272
|
+
debugProcessedChars: 0,
|
|
3273
|
+
debugCheckedRemovedLines: 0,
|
|
3274
|
+
debugFullChecks: 0,
|
|
3275
|
+
debugGuardRuns: 0,
|
|
3276
|
+
};
|
|
3277
|
+
this.#streamingEditToolCallStates.set(toolCall.id, state);
|
|
3278
|
+
} else {
|
|
3279
|
+
state.op = op;
|
|
3280
|
+
state.resolvedPath = resolvedPath;
|
|
3281
|
+
}
|
|
3282
|
+
state.debugGuardRuns += 1;
|
|
3283
|
+
this.streamingEditDebugCounters.guardRuns += 1;
|
|
3284
|
+
|
|
3285
|
+
if (op && op !== "update") {
|
|
3286
|
+
state.settledVerdict = "non-update";
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3029
3289
|
if (!diff) return;
|
|
3030
|
-
if (op && op !== "update") return;
|
|
3031
3290
|
|
|
3032
|
-
if (!diff.includes("\n")) return;
|
|
3033
3291
|
const lastNewlineIndex = diff.lastIndexOf("\n");
|
|
3034
3292
|
if (lastNewlineIndex < 0) return;
|
|
3035
|
-
const
|
|
3036
|
-
if (
|
|
3293
|
+
const completeDiff = diff.slice(0, lastNewlineIndex + 1);
|
|
3294
|
+
if (completeDiff.trim().length === 0) return;
|
|
3295
|
+
|
|
3296
|
+
let diffForCheck = completeDiff;
|
|
3297
|
+
let fullCheck = false;
|
|
3298
|
+
// Obfuscated diffs are intentionally checked through the full path because
|
|
3299
|
+
// deobfuscation is not proven chunk-composable across streaming deltas.
|
|
3300
|
+
if (this.#obfuscator) {
|
|
3301
|
+
fullCheck = true;
|
|
3302
|
+
} else if (completeDiff.length < state.lastProcessedOffset || !completeDiff.startsWith(state.processedPrefix)) {
|
|
3303
|
+
fullCheck = true;
|
|
3304
|
+
} else {
|
|
3305
|
+
diffForCheck = completeDiff.slice(state.lastProcessedOffset);
|
|
3306
|
+
}
|
|
3307
|
+
if (!diffForCheck) return;
|
|
3037
3308
|
|
|
3038
3309
|
let normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
|
|
3039
3310
|
if (!normalizedDiff) return;
|
|
3040
|
-
// Deobfuscate the diff so removed lines match real file content
|
|
3041
3311
|
if (this.#obfuscator) normalizedDiff = this.#obfuscator.deobfuscate(normalizedDiff);
|
|
3042
3312
|
if (!normalizedDiff) return;
|
|
3043
3313
|
const lines = normalizedDiff.split("\n");
|
|
3044
3314
|
const hasChangeLine = lines.some(line => line.startsWith("+") || line.startsWith("-"));
|
|
3045
|
-
if (!hasChangeLine)
|
|
3315
|
+
if (!hasChangeLine) {
|
|
3316
|
+
if (!fullCheck) {
|
|
3317
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3318
|
+
state.processedPrefix = completeDiff;
|
|
3319
|
+
}
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
if (fullCheck) {
|
|
3324
|
+
state.debugFullChecks += 1;
|
|
3325
|
+
this.streamingEditDebugCounters.fullChecks += 1;
|
|
3326
|
+
}
|
|
3327
|
+
state.debugProcessedChars += diffForCheck.length;
|
|
3328
|
+
this.streamingEditDebugCounters.processedChars += diffForCheck.length;
|
|
3046
3329
|
|
|
3047
3330
|
const lineCount = lines.length;
|
|
3048
|
-
const lastChecked = this.#streamingEditCheckedLineCounts.get(toolCall.id);
|
|
3049
|
-
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
3050
3331
|
this.#streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
3051
3332
|
|
|
3052
3333
|
const removedLines = lines
|
|
3053
3334
|
.filter(line => line.startsWith("-") && !line.startsWith("--- "))
|
|
3054
3335
|
.map(line => line.slice(1));
|
|
3336
|
+
state.debugCheckedRemovedLines += removedLines.length;
|
|
3337
|
+
this.streamingEditDebugCounters.checkedRemovedLines += removedLines.length;
|
|
3338
|
+
if (!fullCheck) {
|
|
3339
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3340
|
+
state.processedPrefix = completeDiff;
|
|
3341
|
+
} else if (!this.#obfuscator) {
|
|
3342
|
+
state.lastProcessedOffset = completeDiff.length;
|
|
3343
|
+
state.processedPrefix = completeDiff;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3055
3346
|
if (removedLines.length > 0) {
|
|
3056
3347
|
let cachedContent = this.#streamingEditFileCache.get(resolvedPath);
|
|
3057
3348
|
if (cachedContent === undefined) {
|
|
@@ -3062,6 +3353,7 @@ export class AgentSession {
|
|
|
3062
3353
|
const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line)));
|
|
3063
3354
|
if (missing) {
|
|
3064
3355
|
this.#streamingEditAbortTriggered = true;
|
|
3356
|
+
state.settledVerdict = "aborted";
|
|
3065
3357
|
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
3066
3358
|
toolCallId: toolCall.id,
|
|
3067
3359
|
path,
|
|
@@ -3141,12 +3433,21 @@ export class AgentSession {
|
|
|
3141
3433
|
}
|
|
3142
3434
|
}
|
|
3143
3435
|
|
|
3436
|
+
#requestWorkerIntegrationAttempt(): void {
|
|
3437
|
+
this.#workerIntegrationScheduler.enqueue();
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
async #flushWorkerIntegrationAttempt(): Promise<void> {
|
|
3441
|
+
await this.#workerIntegrationScheduler.flush();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3144
3444
|
/** Emit extension events based on session events */
|
|
3145
3445
|
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
3146
3446
|
if (event.type === "turn_end") {
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3447
|
+
this.#requestWorkerIntegrationAttempt();
|
|
3448
|
+
}
|
|
3449
|
+
if (event.type === "agent_end") {
|
|
3450
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3150
3451
|
}
|
|
3151
3452
|
if (!this.#extensionRunner) return;
|
|
3152
3453
|
if (event.type === "agent_start") {
|
|
@@ -3278,12 +3579,14 @@ export class AgentSession {
|
|
|
3278
3579
|
*/
|
|
3279
3580
|
subscribe(listener: AgentSessionEventListener): () => void {
|
|
3280
3581
|
this.#eventListeners.push(listener);
|
|
3582
|
+
this.#rebuildEventListenerSnapshot();
|
|
3281
3583
|
|
|
3282
3584
|
// Return unsubscribe function for this specific listener
|
|
3283
3585
|
return () => {
|
|
3284
3586
|
const index = this.#eventListeners.indexOf(listener);
|
|
3285
3587
|
if (index !== -1) {
|
|
3286
3588
|
this.#eventListeners.splice(index, 1);
|
|
3589
|
+
this.#rebuildEventListenerSnapshot();
|
|
3287
3590
|
}
|
|
3288
3591
|
};
|
|
3289
3592
|
}
|
|
@@ -3360,6 +3663,7 @@ export class AgentSession {
|
|
|
3360
3663
|
} catch (error) {
|
|
3361
3664
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
3362
3665
|
}
|
|
3666
|
+
await this.#flushWorkerIntegrationAttempt();
|
|
3363
3667
|
await this.#cancelPostPromptTasks();
|
|
3364
3668
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
3365
3669
|
// leak its background bash/task work into the parent's manager. Only
|
|
@@ -3420,6 +3724,7 @@ export class AgentSession {
|
|
|
3420
3724
|
this.#unsubscribeAppendOnly = undefined;
|
|
3421
3725
|
}
|
|
3422
3726
|
this.#eventListeners = [];
|
|
3727
|
+
this.#rebuildEventListenerSnapshot();
|
|
3423
3728
|
}
|
|
3424
3729
|
|
|
3425
3730
|
/**
|
|
@@ -3953,7 +4258,7 @@ export class AgentSession {
|
|
|
3953
4258
|
* prompts or tool execution can run.
|
|
3954
4259
|
*/
|
|
3955
4260
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3956
|
-
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
4261
|
+
if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
|
|
3957
4262
|
return new Proxy(tool, {
|
|
3958
4263
|
get: (target, prop) => {
|
|
3959
4264
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -3976,8 +4281,25 @@ export class AgentSession {
|
|
|
3976
4281
|
}) as T;
|
|
3977
4282
|
}
|
|
3978
4283
|
|
|
4284
|
+
#guardedToolWrapperCacheKey(): string {
|
|
4285
|
+
const bridge = this.#clientBridge;
|
|
4286
|
+
const acpEnabled = Boolean(bridge?.capabilities.requestPermission && bridge.requestPermission);
|
|
4287
|
+
const activeSkill = this.#activeSkillState?.skill ?? "";
|
|
4288
|
+
const activeSkillSession = this.#activeSkillState?.sessionId ?? "";
|
|
4289
|
+
return [
|
|
4290
|
+
"deep-interview-mutation-v1",
|
|
4291
|
+
"ultragoal-ask-v1",
|
|
4292
|
+
`active=${activeSkill}:${activeSkillSession}`,
|
|
4293
|
+
`acp=${acpEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
|
|
4294
|
+
].join("|");
|
|
4295
|
+
}
|
|
4296
|
+
|
|
3979
4297
|
#prepareToolForExecution<T extends AgentTool>(tool: T): T {
|
|
3980
|
-
|
|
4298
|
+
const cacheKey = this.#guardedToolWrapperCacheKey();
|
|
4299
|
+
let wrappersByVersion = this.#guardedToolWrapperCache.get(tool);
|
|
4300
|
+
const cached = wrappersByVersion?.get(cacheKey);
|
|
4301
|
+
if (cached) return cached as T;
|
|
4302
|
+
const wrapped = this.#wrapToolForDeepInterviewMutationGuard(
|
|
3981
4303
|
this.#wrapToolForAcpPermission(
|
|
3982
4304
|
guardToolForUltragoalAsk(
|
|
3983
4305
|
tool,
|
|
@@ -3989,6 +4311,12 @@ export class AgentSession {
|
|
|
3989
4311
|
),
|
|
3990
4312
|
),
|
|
3991
4313
|
);
|
|
4314
|
+
if (!wrappersByVersion) {
|
|
4315
|
+
wrappersByVersion = new Map();
|
|
4316
|
+
this.#guardedToolWrapperCache.set(tool, wrappersByVersion);
|
|
4317
|
+
}
|
|
4318
|
+
wrappersByVersion.set(cacheKey, wrapped);
|
|
4319
|
+
return wrapped;
|
|
3992
4320
|
}
|
|
3993
4321
|
|
|
3994
4322
|
#setGuardedAgentTools(tools: AgentTool[]): void {
|
|
@@ -4183,11 +4511,15 @@ export class AgentSession {
|
|
|
4183
4511
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
4184
4512
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
4185
4513
|
* `#syncEditToolModeAfterModelChange`.
|
|
4186
|
-
*
|
|
4187
|
-
*
|
|
4188
|
-
*
|
|
4189
|
-
*
|
|
4190
|
-
*
|
|
4514
|
+
* Volatile per-turn facts (current date, cwd, and mtime-sorted workspace tree
|
|
4515
|
+
* RENDERING) are intentionally NOT covered. They are delivered as user-role
|
|
4516
|
+
* context by `#buildVolatileProjectContextMessage()`, outside the provider
|
|
4517
|
+
* cached system prefix, so rollover/touched-file changes must not force a
|
|
4518
|
+
* stable prompt rebuild. Note the AGENTS.md file LIST (`agentsMdFiles`, sorted
|
|
4519
|
+
* by name) surfaced by the same workspace scan is still a stable-prefix input
|
|
4520
|
+
* via project-prompt's `<dir-context>`; adding/removing an AGENTS.md is an
|
|
4521
|
+
* intended instruction change, and name-sorting means touched files do not
|
|
4522
|
+
* perturb it.
|
|
4191
4523
|
*/
|
|
4192
4524
|
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
4193
4525
|
// Order-preserving join: any reorder must produce a different signature so
|
|
@@ -4218,8 +4550,7 @@ export class AgentSession {
|
|
|
4218
4550
|
entries.sort();
|
|
4219
4551
|
instructionsSegment = entries.join("\u0006");
|
|
4220
4552
|
}
|
|
4221
|
-
|
|
4222
|
-
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
|
4553
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}`;
|
|
4223
4554
|
}
|
|
4224
4555
|
|
|
4225
4556
|
/**
|
|
@@ -4645,6 +4976,7 @@ export class AgentSession {
|
|
|
4645
4976
|
setClientBridge(bridge: ClientBridge | undefined): void {
|
|
4646
4977
|
this.#clientBridge = bridge;
|
|
4647
4978
|
this.#acpPermissionDecisions.clear();
|
|
4979
|
+
this.#acpPermissionWrapperVersion++;
|
|
4648
4980
|
const activeToolNames = this.getActiveToolNames();
|
|
4649
4981
|
const activeTools = activeToolNames
|
|
4650
4982
|
.map(name => this.#toolRegistry.get(name))
|
|
@@ -4839,6 +5171,94 @@ export class AgentSession {
|
|
|
4839
5171
|
};
|
|
4840
5172
|
}
|
|
4841
5173
|
|
|
5174
|
+
async #buildAutomaticPlanModeMessage(): Promise<CustomMessage | null> {
|
|
5175
|
+
const state = this.#planModeState;
|
|
5176
|
+
if (!state?.enabled) {
|
|
5177
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5178
|
+
return null;
|
|
5179
|
+
}
|
|
5180
|
+
const message = await this.#buildPlanModeMessage();
|
|
5181
|
+
if (!message) {
|
|
5182
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5183
|
+
return null;
|
|
5184
|
+
}
|
|
5185
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5186
|
+
const signature = buildContextInjectionSignature("plan-mode-context", [
|
|
5187
|
+
"enabled",
|
|
5188
|
+
state.planFilePath,
|
|
5189
|
+
state.workflow ?? "",
|
|
5190
|
+
content,
|
|
5191
|
+
]);
|
|
5192
|
+
if (signature === this.#lastInjectedPlanContextSig) return null;
|
|
5193
|
+
this.#lastInjectedPlanContextSig = signature;
|
|
5194
|
+
return message;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
#buildAutomaticGoalModeMessage(): CustomMessage | null {
|
|
5198
|
+
const state = this.#goalModeState;
|
|
5199
|
+
if (!state?.enabled || state.goal.status !== "active") {
|
|
5200
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5201
|
+
return null;
|
|
5202
|
+
}
|
|
5203
|
+
const message = this.#buildGoalModeMessage();
|
|
5204
|
+
if (!message) {
|
|
5205
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5206
|
+
return null;
|
|
5207
|
+
}
|
|
5208
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
5209
|
+
const signature = buildContextInjectionSignature("goal-mode-context", ["enabled", state.goal.id, content]);
|
|
5210
|
+
if (signature === this.#lastInjectedGoalContextSig) return null;
|
|
5211
|
+
this.#lastInjectedGoalContextSig = signature;
|
|
5212
|
+
return message;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
/**
|
|
5216
|
+
* Clear the goal/plan static-once injection signatures so the next prompt
|
|
5217
|
+
* re-injects the active mode context once. MUST be called whenever the live
|
|
5218
|
+
* message set is rebuilt in a way that can evict a previously injected
|
|
5219
|
+
* goal-mode-context/plan-mode-context copy (compaction/pruning/handoff
|
|
5220
|
+
* replaceMessages) or when a signature was consumed but the message was not
|
|
5221
|
+
* delivered (prompt-generation abort). Forward-omission relies on the injected
|
|
5222
|
+
* copy surviving in context; this guards that invariant.
|
|
5223
|
+
*/
|
|
5224
|
+
#resetInjectedContextSignatures(): void {
|
|
5225
|
+
this.#lastInjectedPlanContextSig = undefined;
|
|
5226
|
+
this.#lastInjectedGoalContextSig = undefined;
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
async #buildVolatileProjectContextMessage(): Promise<CustomMessage> {
|
|
5230
|
+
const cwd = this.sessionManager.getCwd();
|
|
5231
|
+
// Date + cwd are refreshed every turn (cheap). The mtime-sorted workspace
|
|
5232
|
+
// tree is expensive to scan and large to carry, so throttle it: rebuild at
|
|
5233
|
+
// most once per VOLATILE_TREE_TTL_MS and only embed the tree block on turns
|
|
5234
|
+
// where the scan actually refreshed. This bounds both the per-turn IO cost
|
|
5235
|
+
// and the accumulation of stale tree copies in history, while keeping the
|
|
5236
|
+
// content outside the cached system prefix.
|
|
5237
|
+
let includeTree: WorkspaceTree | undefined;
|
|
5238
|
+
if (this.#initialWorkspaceTree) {
|
|
5239
|
+
this.#cachedWorkspaceTree = this.#initialWorkspaceTree;
|
|
5240
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5241
|
+
this.#initialWorkspaceTree = undefined;
|
|
5242
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5243
|
+
} else if (Date.now() - this.#cachedWorkspaceTreeAt >= VOLATILE_TREE_TTL_MS) {
|
|
5244
|
+
try {
|
|
5245
|
+
this.#cachedWorkspaceTree = await buildWorkspaceTree(cwd, { timeoutMs: 5000 });
|
|
5246
|
+
} catch {
|
|
5247
|
+
this.#cachedWorkspaceTree = undefined;
|
|
5248
|
+
}
|
|
5249
|
+
this.#cachedWorkspaceTreeAt = Date.now();
|
|
5250
|
+
includeTree = this.#cachedWorkspaceTree;
|
|
5251
|
+
}
|
|
5252
|
+
return {
|
|
5253
|
+
role: "custom",
|
|
5254
|
+
customType: "volatile-project-context",
|
|
5255
|
+
content: buildVolatileProjectContext({ cwd, workspaceTree: includeTree }),
|
|
5256
|
+
display: false,
|
|
5257
|
+
attribution: "agent",
|
|
5258
|
+
timestamp: Date.now(),
|
|
5259
|
+
};
|
|
5260
|
+
}
|
|
5261
|
+
|
|
4842
5262
|
/**
|
|
4843
5263
|
* Send a prompt to the agent.
|
|
4844
5264
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -5089,14 +5509,16 @@ export class AgentSession {
|
|
|
5089
5509
|
if (planReferenceMessage) {
|
|
5090
5510
|
messages.push(planReferenceMessage);
|
|
5091
5511
|
}
|
|
5092
|
-
const planModeMessage = await this.#
|
|
5512
|
+
const planModeMessage = await this.#buildAutomaticPlanModeMessage();
|
|
5093
5513
|
if (planModeMessage) {
|
|
5094
5514
|
messages.push(planModeMessage);
|
|
5095
5515
|
}
|
|
5096
|
-
const goalModeMessage = this.#
|
|
5516
|
+
const goalModeMessage = this.#buildAutomaticGoalModeMessage();
|
|
5097
5517
|
if (goalModeMessage) {
|
|
5098
5518
|
messages.push(goalModeMessage);
|
|
5099
5519
|
}
|
|
5520
|
+
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
5521
|
+
messages.push(volatileProjectContextMessage);
|
|
5100
5522
|
if (options?.prependMessages) {
|
|
5101
5523
|
messages.push(...options.prependMessages);
|
|
5102
5524
|
}
|
|
@@ -5106,6 +5528,7 @@ export class AgentSession {
|
|
|
5106
5528
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
5107
5529
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
5108
5530
|
if (this.#promptGeneration !== generation) {
|
|
5531
|
+
this.#resetInjectedContextSignatures();
|
|
5109
5532
|
return;
|
|
5110
5533
|
}
|
|
5111
5534
|
|
|
@@ -5118,9 +5541,28 @@ export class AgentSession {
|
|
|
5118
5541
|
// Auto-read @filepath mentions
|
|
5119
5542
|
const fileMentions = extractFileMentions(expandedText);
|
|
5120
5543
|
if (fileMentions.length > 0) {
|
|
5121
|
-
const
|
|
5544
|
+
const cwd = this.sessionManager.getCwd();
|
|
5545
|
+
// Collect resolved paths already shown (read or mentioned) in the recent
|
|
5546
|
+
// window so a repeat @mention emits a compact note instead of the full body.
|
|
5547
|
+
const RECENT_MENTION_WINDOW = 40;
|
|
5548
|
+
const recentlyShownPaths = new Set<string>();
|
|
5549
|
+
for (const entry of this.sessionManager.getBranch().slice(-RECENT_MENTION_WINDOW)) {
|
|
5550
|
+
if (entry.type !== "message") continue;
|
|
5551
|
+
const msg = entry.message;
|
|
5552
|
+
if (msg.role === "fileMention") {
|
|
5553
|
+
for (const file of msg.files) {
|
|
5554
|
+
if (!file.duplicate && !file.pruned) recentlyShownPaths.add(resolveReadPath(file.path, cwd));
|
|
5555
|
+
}
|
|
5556
|
+
} else if (msg.role === "toolResult") {
|
|
5557
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
5558
|
+
if (typeof resolved === "string" && resolved) recentlyShownPaths.add(resolveReadPath(resolved, cwd));
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
const fileMentionMessages = await generateFileMentionMessages(fileMentions, cwd, {
|
|
5122
5562
|
autoResizeImages: this.settings.get("images.autoResize"),
|
|
5123
5563
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
5564
|
+
maxInlineBytes: this.settings.get("tools.fileMentionInlineBytes") * 1024,
|
|
5565
|
+
recentlyShownPaths,
|
|
5124
5566
|
});
|
|
5125
5567
|
messages.push(...fileMentionMessages);
|
|
5126
5568
|
}
|
|
@@ -5170,8 +5612,12 @@ export class AgentSession {
|
|
|
5170
5612
|
this.#appendBeforeAgentStartCustomMessages(messages, contributed, promptAttribution, message.role);
|
|
5171
5613
|
}
|
|
5172
5614
|
|
|
5173
|
-
// Bail out if a newer abort/prompt cycle has started since we began setup
|
|
5615
|
+
// Bail out if a newer abort/prompt cycle has started since we began setup.
|
|
5616
|
+
// The injection signatures were consumed while building the automatic
|
|
5617
|
+
// goal/plan context above, but the message is not delivered on this
|
|
5618
|
+
// aborted turn, so reset them here so the next real turn re-injects once.
|
|
5174
5619
|
if (this.#promptGeneration !== generation) {
|
|
5620
|
+
this.#resetInjectedContextSignatures();
|
|
5175
5621
|
return;
|
|
5176
5622
|
}
|
|
5177
5623
|
|
|
@@ -5879,6 +6325,9 @@ export class AgentSession {
|
|
|
5879
6325
|
if (eviction.evictedEntries > 0) await this.sessionManager.rewriteEntries();
|
|
5880
6326
|
const sessionContext = this.buildDisplaySessionContext();
|
|
5881
6327
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6328
|
+
// Compaction can evict a previously injected goal/plan-mode-context copy from
|
|
6329
|
+
// live context; clear the static-once signatures so the next prompt re-injects.
|
|
6330
|
+
this.#resetInjectedContextSignatures();
|
|
5882
6331
|
this.#syncTodoPhasesFromBranch();
|
|
5883
6332
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
5884
6333
|
|
|
@@ -6097,6 +6546,41 @@ export class AgentSession {
|
|
|
6097
6546
|
return true;
|
|
6098
6547
|
}
|
|
6099
6548
|
|
|
6549
|
+
/**
|
|
6550
|
+
* Clear active conversational/model context while preserving the current
|
|
6551
|
+
* session identity and durable history trail.
|
|
6552
|
+
*/
|
|
6553
|
+
async clearContext(): Promise<boolean> {
|
|
6554
|
+
const sessionId = this.sessionId;
|
|
6555
|
+
this.#disconnectFromAgent();
|
|
6556
|
+
await this.abort();
|
|
6557
|
+
this.#cancelOwnAsyncJobs();
|
|
6558
|
+
this.#suppressOwnAsyncJobDeliveries();
|
|
6559
|
+
this.yieldQueue.clear();
|
|
6560
|
+
this.#pendingBackgroundExchanges = [];
|
|
6561
|
+
this.#closeAllProviderSessions("context clear");
|
|
6562
|
+
this.agent.reset();
|
|
6563
|
+
await this.sessionManager.flush();
|
|
6564
|
+
this.sessionManager.appendContextClearEntry({ sessionId });
|
|
6565
|
+
this.setTodoPhases([]);
|
|
6566
|
+
this.#syncAgentSessionId(sessionId);
|
|
6567
|
+
this.#steeringMessages = [];
|
|
6568
|
+
this.#followUpMessages = [];
|
|
6569
|
+
this.#pendingNextTurnMessages = [];
|
|
6570
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6571
|
+
|
|
6572
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6573
|
+
if (this.model) {
|
|
6574
|
+
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
6575
|
+
}
|
|
6576
|
+
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
6577
|
+
this.#todoReminderCount = 0;
|
|
6578
|
+
this.#planReferenceSent = false;
|
|
6579
|
+
this.#planReferencePath = "local://PLAN.md";
|
|
6580
|
+
this.#reconnectToAgent();
|
|
6581
|
+
return true;
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6100
6584
|
/**
|
|
6101
6585
|
* Set a display name for the current session.
|
|
6102
6586
|
*/
|
|
@@ -6668,24 +7152,92 @@ export class AgentSession {
|
|
|
6668
7152
|
const branchEntries = this.sessionManager.getBranch();
|
|
6669
7153
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6670
7154
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6671
|
-
const
|
|
6672
|
-
|
|
7155
|
+
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
7156
|
+
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
7157
|
+
);
|
|
7158
|
+
const tokensSaved =
|
|
7159
|
+
result.tokensSaved + argumentResult.argumentTokensSaved + Math.round(fileMentionResult.bytesSaved / 4);
|
|
7160
|
+
const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount + fileMentionResult.changed.length;
|
|
6673
7161
|
if (prunedCount === 0) {
|
|
6674
7162
|
return undefined;
|
|
6675
7163
|
}
|
|
6676
7164
|
|
|
6677
7165
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
6678
7166
|
// the pruning mutations must be written back into the canonical store.
|
|
6679
|
-
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries];
|
|
7167
|
+
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
6680
7168
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
6681
7169
|
await this.sessionManager.rewriteEntries();
|
|
6682
7170
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6683
7171
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7172
|
+
// Pruning can evict a previously injected goal/plan-mode-context copy; clear
|
|
7173
|
+
// the static-once signatures so the next prompt re-injects the mode context.
|
|
7174
|
+
this.#resetInjectedContextSignatures();
|
|
6684
7175
|
this.#syncTodoPhasesFromBranch();
|
|
6685
7176
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6686
7177
|
return { prunedCount, tokensSaved };
|
|
6687
7178
|
}
|
|
6688
7179
|
|
|
7180
|
+
/**
|
|
7181
|
+
* Approximate cost of the prompt-cache-epoch reset that pruning forces:
|
|
7182
|
+
* pruning rewrites already-sent history, invalidating the provider's cached
|
|
7183
|
+
* prefix, so the next turn re-bills that prefix as fresh input. Use the cached
|
|
7184
|
+
* prefix tokens from the last provider usage as the reset-cost estimate.
|
|
7185
|
+
*/
|
|
7186
|
+
#lastCacheEpochResetCost(): number {
|
|
7187
|
+
const messages = this.messages;
|
|
7188
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7189
|
+
const msg = messages[i];
|
|
7190
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
7191
|
+
const usage = (msg as AssistantMessage).usage;
|
|
7192
|
+
return (usage?.cacheRead ?? 0) + (usage?.cacheWrite ?? 0);
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
return 0;
|
|
7196
|
+
}
|
|
7197
|
+
|
|
7198
|
+
/**
|
|
7199
|
+
* Evidence-gated below-threshold maintenance pruning (Finding 13). Runs
|
|
7200
|
+
* #pruneToolOutputs ONCE, below the compaction threshold, only when it is
|
|
7201
|
+
* opted in AND the estimated stale-prunable savings both clear a high minimum
|
|
7202
|
+
* and exceed the one-time cache-epoch reset cost (so remaining-turn savings pay
|
|
7203
|
+
* it back). Default-off/blocked until live evidence justifies enabling. Emits
|
|
7204
|
+
* explicit maintenance/reset telemetry.
|
|
7205
|
+
*/
|
|
7206
|
+
async #maybeRunBelowThresholdMaintenancePrune(): Promise<void> {
|
|
7207
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
7208
|
+
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
7209
|
+
|
|
7210
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG);
|
|
7211
|
+
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
7212
|
+
if (
|
|
7213
|
+
!shouldRunMaintenancePrune({
|
|
7214
|
+
enabled: compactionSettings.maintenancePruningEnabled,
|
|
7215
|
+
estimatedSavings: estimate.tokensSaved,
|
|
7216
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7217
|
+
cacheEpochResetCost,
|
|
7218
|
+
})
|
|
7219
|
+
) {
|
|
7220
|
+
return;
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
7224
|
+
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
7225
|
+
|
|
7226
|
+
const resetReason = `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
7227
|
+
await this.#emitSessionEvent({
|
|
7228
|
+
type: "notice",
|
|
7229
|
+
level: "info",
|
|
7230
|
+
source: "maintenance-prune",
|
|
7231
|
+
message: `Maintenance pruning reclaimed ${pruneResult.tokensSaved} tokens from ${pruneResult.prunedCount} stale tool outputs (${resetReason}).`,
|
|
7232
|
+
});
|
|
7233
|
+
logger.info("Below-threshold maintenance pruning ran", {
|
|
7234
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
7235
|
+
prunedCount: pruneResult.prunedCount,
|
|
7236
|
+
cacheEpochResetCost,
|
|
7237
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
7238
|
+
});
|
|
7239
|
+
}
|
|
7240
|
+
|
|
6689
7241
|
/**
|
|
6690
7242
|
* Manually compact the session context.
|
|
6691
7243
|
* Aborts current agent operation first.
|
|
@@ -6708,7 +7260,9 @@ export class AgentSession {
|
|
|
6708
7260
|
|
|
6709
7261
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6710
7262
|
const pathEntries = this.sessionManager.getBranch();
|
|
6711
|
-
const preparation = prepareCompaction(pathEntries, compactionSettings
|
|
7263
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
7264
|
+
tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
|
|
7265
|
+
});
|
|
6712
7266
|
if (!preparation) {
|
|
6713
7267
|
// Check why we can't compact
|
|
6714
7268
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -7020,6 +7574,9 @@ export class AgentSession {
|
|
|
7020
7574
|
// Rebuild agent messages from session
|
|
7021
7575
|
const sessionContext = this.buildDisplaySessionContext();
|
|
7022
7576
|
this.agent.replaceMessages(sessionContext.messages);
|
|
7577
|
+
// Handoff rebuilds live context and can evict a previously injected
|
|
7578
|
+
// goal/plan-mode-context copy; clear the static-once signatures.
|
|
7579
|
+
this.#resetInjectedContextSignatures();
|
|
7023
7580
|
this.#syncTodoPhasesFromBranch();
|
|
7024
7581
|
|
|
7025
7582
|
return { document: handoffText, savedPath };
|
|
@@ -7202,7 +7759,12 @@ export class AgentSession {
|
|
|
7202
7759
|
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
7203
7760
|
// Auto maintenance should track actual context fullness.
|
|
7204
7761
|
const autoCompactionOutputReserveTokens = 0;
|
|
7205
|
-
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens))
|
|
7762
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
7763
|
+
// Below the compaction threshold: optionally run evidence-gated maintenance
|
|
7764
|
+
// pruning (opt-in, high savings + cache-epoch payback required).
|
|
7765
|
+
await this.#maybeRunBelowThresholdMaintenancePrune();
|
|
7766
|
+
return;
|
|
7767
|
+
}
|
|
7206
7768
|
|
|
7207
7769
|
const pruneResult = await this.#pruneToolOutputs();
|
|
7208
7770
|
if (pruneResult) {
|
|
@@ -7252,6 +7814,7 @@ export class AgentSession {
|
|
|
7252
7814
|
}
|
|
7253
7815
|
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
7254
7816
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
7817
|
+
this.#resetInjectedContextSignatures();
|
|
7255
7818
|
try {
|
|
7256
7819
|
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
7257
7820
|
startedAt: checkpointState.startedAt,
|
|
@@ -7340,7 +7903,7 @@ export class AgentSession {
|
|
|
7340
7903
|
return undefined;
|
|
7341
7904
|
}
|
|
7342
7905
|
|
|
7343
|
-
if (!this.#toolRegistry.has("todo_write")) {
|
|
7906
|
+
if (!this.#toolRegistry.has("todo_write") || !this.getActiveToolNames().includes("todo_write")) {
|
|
7344
7907
|
logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
|
|
7345
7908
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
7346
7909
|
});
|
|
@@ -7557,13 +8120,13 @@ export class AgentSession {
|
|
|
7557
8120
|
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
7558
8121
|
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
7559
8122
|
const providerId = model?.provider;
|
|
7560
|
-
const enable =
|
|
8123
|
+
const enable = resolveAppendOnlyMode(setting, providerId ?? "");
|
|
7561
8124
|
const prev = this.#lastAppendOnlyResolution;
|
|
7562
8125
|
if (prev && prev.enable === enable && prev.providerId === providerId) return;
|
|
7563
8126
|
this.#lastAppendOnlyResolution = { enable, providerId };
|
|
7564
8127
|
|
|
7565
8128
|
if (enable && !this.agent.appendOnlyContext) {
|
|
7566
|
-
this.agent.setAppendOnlyContext(
|
|
8129
|
+
this.agent.setAppendOnlyContext(createAppendOnlyContextManager(providerId));
|
|
7567
8130
|
} else if (enable && this.agent.appendOnlyContext) {
|
|
7568
8131
|
// Already active — invalidate prefix + log so the next turn
|
|
7569
8132
|
// rebuilds for the current model's normalization.
|
|
@@ -8166,7 +8729,13 @@ export class AgentSession {
|
|
|
8166
8729
|
|
|
8167
8730
|
const pathEntries = this.sessionManager.getBranch();
|
|
8168
8731
|
|
|
8169
|
-
|
|
8732
|
+
// Emergency/overflow-recovery compaction is conservative: apply the token
|
|
8733
|
+
// correction only when it SHRINKS the keep window (ratio >= 1), never when
|
|
8734
|
+
// it would grow it, so recovery cannot re-overflow the provider window.
|
|
8735
|
+
const overflowRatio = this.#computeCompactionTokenCorrectionRatio();
|
|
8736
|
+
const preparation = prepareCompaction(pathEntries, compactionSettings, {
|
|
8737
|
+
tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
|
|
8738
|
+
});
|
|
8170
8739
|
if (!preparation) {
|
|
8171
8740
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8172
8741
|
await this.#emitSessionEvent({
|
|
@@ -9966,6 +10535,7 @@ export class AgentSession {
|
|
|
9966
10535
|
}
|
|
9967
10536
|
|
|
9968
10537
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10538
|
+
this.#resetInjectedContextSignatures();
|
|
9969
10539
|
this.#syncTodoPhasesFromBranch();
|
|
9970
10540
|
if (switchingToDifferentSession) {
|
|
9971
10541
|
this.#closeAllProviderSessions("session switch");
|
|
@@ -10138,6 +10708,7 @@ export class AgentSession {
|
|
|
10138
10708
|
|
|
10139
10709
|
if (!skipConversationRestore) {
|
|
10140
10710
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10711
|
+
this.#resetInjectedContextSignatures();
|
|
10141
10712
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10142
10713
|
}
|
|
10143
10714
|
|
|
@@ -10305,6 +10876,7 @@ export class AgentSession {
|
|
|
10305
10876
|
const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
|
|
10306
10877
|
await this.#restoreMCPSelectionsForSessionContext(displayContext);
|
|
10307
10878
|
this.agent.replaceMessages(displayContext.messages);
|
|
10879
|
+
this.#resetInjectedContextSignatures();
|
|
10308
10880
|
this.#syncTodoPhasesFromBranch();
|
|
10309
10881
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10310
10882
|
|
|
@@ -10503,6 +11075,57 @@ export class AgentSession {
|
|
|
10503
11075
|
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
10504
11076
|
}
|
|
10505
11077
|
|
|
11078
|
+
/** Count inline image blocks in a message (for bucketing the fixed image token estimate). */
|
|
11079
|
+
#countImageBlocks(message: AgentMessage): number {
|
|
11080
|
+
const content = (message as { content?: unknown }).content;
|
|
11081
|
+
if (!Array.isArray(content)) return 0;
|
|
11082
|
+
let count = 0;
|
|
11083
|
+
for (const block of content) {
|
|
11084
|
+
if (block && typeof block === "object" && (block as { type?: unknown }).type === "image") count++;
|
|
11085
|
+
}
|
|
11086
|
+
return count;
|
|
11087
|
+
}
|
|
11088
|
+
|
|
11089
|
+
/**
|
|
11090
|
+
* Observed heuristic→actual token correction for the compaction keep window
|
|
11091
|
+
* (Finding 7). Compares the provider's real prompt tokens against the chars/4
|
|
11092
|
+
* heuristic estimate of the same content (stable system prefix + history through
|
|
11093
|
+
* the last usage-bearing assistant turn). Image-bearing content is bucketed out
|
|
11094
|
+
* of BOTH sides using the identical fixed IMAGE_TOKEN_ESTIMATE so the 1200-token
|
|
11095
|
+
* image charge cannot skew the text ratio. Returns undefined when data is
|
|
11096
|
+
* insufficient, so prepareCompaction applies no correction (never the confounded
|
|
11097
|
+
* raw promptTokens/estimatedTokens quotient). Clamped to [0.5, 2] downstream.
|
|
11098
|
+
*/
|
|
11099
|
+
#computeCompactionTokenCorrectionRatio(): number | undefined {
|
|
11100
|
+
const messages = this.messages;
|
|
11101
|
+
let lastUsageIndex = -1;
|
|
11102
|
+
let lastUsage: Usage | undefined;
|
|
11103
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11104
|
+
const msg = messages[i];
|
|
11105
|
+
if (msg.role === "assistant" && (msg as AssistantMessage).usage) {
|
|
11106
|
+
lastUsage = (msg as AssistantMessage).usage;
|
|
11107
|
+
lastUsageIndex = i;
|
|
11108
|
+
break;
|
|
11109
|
+
}
|
|
11110
|
+
}
|
|
11111
|
+
if (!lastUsage || lastUsageIndex < 0) return undefined;
|
|
11112
|
+
const actual = calculatePromptTokens(lastUsage);
|
|
11113
|
+
if (!(actual > 0)) return undefined;
|
|
11114
|
+
|
|
11115
|
+
let heuristic = 0;
|
|
11116
|
+
for (const block of this.agent.state.systemPrompt) heuristic += estimateTextTokensHeuristic(block);
|
|
11117
|
+
let imageBlocks = 0;
|
|
11118
|
+
for (let i = 0; i <= lastUsageIndex; i++) {
|
|
11119
|
+
heuristic += this.#estimateMessageDisplayTokens(messages[i]);
|
|
11120
|
+
imageBlocks += this.#countImageBlocks(messages[i]);
|
|
11121
|
+
}
|
|
11122
|
+
const imgAdjust = imageBlocks * IMAGE_TOKEN_ESTIMATE;
|
|
11123
|
+
const num = actual - imgAdjust;
|
|
11124
|
+
const den = heuristic - imgAdjust;
|
|
11125
|
+
if (!(num > 0) || !(den > 0)) return undefined;
|
|
11126
|
+
return num / den;
|
|
11127
|
+
}
|
|
11128
|
+
|
|
10506
11129
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
10507
11130
|
tokens: number;
|
|
10508
11131
|
} {
|
|
@@ -10791,6 +11414,10 @@ export class AgentSession {
|
|
|
10791
11414
|
return this.#extensionRunner?.hasHandlers(eventType) ?? false;
|
|
10792
11415
|
}
|
|
10793
11416
|
|
|
11417
|
+
#hasStreamingExtensionHandlers(): boolean {
|
|
11418
|
+
return this.hasExtensionHandlers("message_update");
|
|
11419
|
+
}
|
|
11420
|
+
|
|
10794
11421
|
/**
|
|
10795
11422
|
* Register a first-party internal before-agent-start contributor. Returns an
|
|
10796
11423
|
* unregister function. This is NOT user-facing hook discovery; it is an
|