@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6
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 +74 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/cli/update-cli.d.ts +11 -1
- package/dist/types/config/model-registry.d.ts +18 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -0
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +6 -0
- package/dist/types/eval/js/shared/runtime.d.ts +5 -0
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +7 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +10 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -2
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +31 -2
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +15 -0
- package/dist/types/tools/index.d.ts +17 -2
- package/dist/types/tools/render-utils.d.ts +1 -1
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/git.d.ts +6 -0
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/package.json +29 -9
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +4 -3
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- package/src/commit/agentic/tools/split-commit.ts +8 -1
- package/src/config/model-provider-priority.ts +1 -0
- package/src/config/model-registry.ts +81 -2
- package/src/debug/index.ts +4 -8
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/helpers.ts +2 -1
- package/src/edit/diff.ts +114 -4
- package/src/edit/hashline/diff.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/modes/patch.ts +6 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/edit/renderer.ts +12 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/backend.ts +15 -0
- package/src/eval/js/context-manager.ts +4 -2
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +7 -1
- package/src/eval/js/shared/helpers.ts +53 -6
- package/src/eval/js/shared/runtime.ts +8 -0
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/py/executor.ts +12 -0
- package/src/eval/py/index.ts +7 -1
- package/src/eval/py/prelude.py +43 -4
- package/src/eval/py/runner.py +1 -0
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/extensions/types.ts +8 -1
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +13 -0
- package/src/lsp/render.ts +8 -6
- package/src/mcp/oauth-flow.ts +3 -3
- package/src/mcp/render.ts +7 -1
- package/src/modes/components/agent-dashboard.ts +6 -4
- package/src/modes/components/custom-editor.ts +12 -6
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +4 -4
- package/src/modes/components/read-tool-group.ts +10 -3
- package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/types.ts +23 -8
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/transcript-container.ts +17 -10
- package/src/modes/components/user-message.ts +6 -3
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/mcp-command-controller.ts +52 -17
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/image-references.ts +13 -7
- package/src/modes/interactive-mode.ts +35 -3
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
- package/src/modes/theme/theme.ts +95 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/ui-helpers.ts +14 -5
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/eval.md +4 -4
- package/src/sdk.ts +31 -14
- package/src/session/agent-session.ts +290 -196
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/index.ts +9 -1
- package/src/task/render.ts +36 -14
- package/src/tools/ask.ts +14 -5
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +14 -2
- package/src/tools/browser/render.ts +5 -2
- package/src/tools/browser/tab-worker.ts +211 -91
- package/src/tools/debug.ts +5 -2
- package/src/tools/eval-render.ts +6 -3
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh-renderer.ts +29 -15
- package/src/tools/index.ts +32 -4
- package/src/tools/inspect-image-renderer.ts +12 -5
- package/src/tools/job.ts +9 -6
- package/src/tools/memory-render.ts +19 -5
- package/src/tools/read.ts +165 -18
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/ssh.ts +4 -1
- package/src/tools/todo.ts +8 -1
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/tui/code-cell.ts +1 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/git.ts +41 -0
- package/src/utils/image-loading.ts +31 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import * as crypto from "node:crypto";
|
|
17
16
|
import * as fs from "node:fs";
|
|
18
17
|
import * as os from "node:os";
|
|
19
18
|
import * as path from "node:path";
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
resolveTelemetry,
|
|
34
33
|
ThinkingLevel,
|
|
35
34
|
} from "@oh-my-pi/pi-agent-core";
|
|
35
|
+
|
|
36
36
|
import {
|
|
37
37
|
AGGRESSIVE_SHAKE_CONFIG,
|
|
38
38
|
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
|
@@ -77,6 +77,7 @@ import type {
|
|
|
77
77
|
import {
|
|
78
78
|
calculateRateLimitBackoffMs,
|
|
79
79
|
clearAnthropicFastModeFallback,
|
|
80
|
+
deriveClaudeDeviceId,
|
|
80
81
|
Effort,
|
|
81
82
|
getSupportedEfforts,
|
|
82
83
|
isContextOverflow,
|
|
@@ -215,6 +216,7 @@ import { parseCommandArgs } from "../utils/command-args";
|
|
|
215
216
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
216
217
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
217
218
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
219
|
+
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
218
220
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
219
221
|
import type { AuthStorage } from "./auth-storage";
|
|
220
222
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
@@ -227,6 +229,7 @@ import {
|
|
|
227
229
|
type PythonExecutionMessage,
|
|
228
230
|
readPendingDisplayTag,
|
|
229
231
|
SILENT_ABORT_MARKER,
|
|
232
|
+
SKILL_PROMPT_MESSAGE_TYPE,
|
|
230
233
|
stripImagesFromMessage,
|
|
231
234
|
} from "./messages";
|
|
232
235
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
@@ -531,15 +534,6 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
531
534
|
}
|
|
532
535
|
|
|
533
536
|
const IRC_REPLY_MAX_BYTES = 4096;
|
|
534
|
-
export const ANTHROPIC_TOOL_CALL_BATCH_CAP = 4;
|
|
535
|
-
const CLAUDE_OPUS_4_8_MODEL_ID = /(?:^|[./_-])claude-opus-4[.-]8\b/i;
|
|
536
|
-
|
|
537
|
-
export function resolveToolCallBatchCapForModel(model: Model | undefined): number | undefined {
|
|
538
|
-
if (!model) return undefined;
|
|
539
|
-
return model.provider === "anthropic" && CLAUDE_OPUS_4_8_MODEL_ID.test(model.id)
|
|
540
|
-
? ANTHROPIC_TOOL_CALL_BATCH_CAP
|
|
541
|
-
: undefined;
|
|
542
|
-
}
|
|
543
537
|
|
|
544
538
|
/**
|
|
545
539
|
* Collapse degenerate IRC ephemeral replies before they hit the relay.
|
|
@@ -613,14 +607,10 @@ function buildSessionMetadata(
|
|
|
613
607
|
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
614
608
|
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
615
609
|
userId.account_uuid = accountUuid;
|
|
616
|
-
// Claude Code's `device_id` is a stable 64-hex install
|
|
617
|
-
// omp's persistent install id
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
userId.device_id = crypto
|
|
621
|
-
.createHash("sha256")
|
|
622
|
-
.update(`omp-claude-device-id-v1:${getInstallId()}`)
|
|
623
|
-
.digest("hex");
|
|
610
|
+
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
611
|
+
// identifier. Include both omp's persistent install id and the Claude
|
|
612
|
+
// account UUID so two accounts on the same install do not share a device.
|
|
613
|
+
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
624
614
|
}
|
|
625
615
|
}
|
|
626
616
|
return { user_id: JSON.stringify(userId) };
|
|
@@ -1102,10 +1092,6 @@ export class AgentSession {
|
|
|
1102
1092
|
this.#flushPendingAgentEnd();
|
|
1103
1093
|
}
|
|
1104
1094
|
|
|
1105
|
-
#syncToolCallBatchCap(model: Model | undefined = this.model): void {
|
|
1106
|
-
this.agent.maxToolCallsPerTurn = resolveToolCallBatchCapForModel(model);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1095
|
#flushPendingAgentEnd(): void {
|
|
1110
1096
|
const pending = this.#pendingAgentEndEmit;
|
|
1111
1097
|
if (!pending) return;
|
|
@@ -1224,7 +1210,6 @@ export class AgentSession {
|
|
|
1224
1210
|
this.#agentId = config.agentId;
|
|
1225
1211
|
this.#agentRegistry = config.agentRegistry;
|
|
1226
1212
|
this.#providerSessionId = config.providerSessionId;
|
|
1227
|
-
this.#syncToolCallBatchCap();
|
|
1228
1213
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1229
1214
|
const event: AgentEvent = {
|
|
1230
1215
|
type: "message_update",
|
|
@@ -1687,89 +1672,18 @@ export class AgentSession {
|
|
|
1687
1672
|
}
|
|
1688
1673
|
|
|
1689
1674
|
if (matchContext && "delta" in assistantEvent) {
|
|
1675
|
+
const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1690
1676
|
const matches = this.#checkTtsrStream(assistantEvent.delta, matchContext, streamingToolCall);
|
|
1691
|
-
if (matches.length > 0) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
this.#addPendingTtsrInjections(matches);
|
|
1702
|
-
|
|
1703
|
-
if (shouldInterrupt) {
|
|
1704
|
-
// Abort the stream immediately — do not gate on extension callbacks
|
|
1705
|
-
this.#ttsrAbortPending = true;
|
|
1706
|
-
this.#ensureTtsrResumePromise();
|
|
1707
|
-
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
|
1708
|
-
// Notify extensions (fire-and-forget, does not block abort)
|
|
1709
|
-
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
1710
|
-
// Schedule retry after a short delay
|
|
1711
|
-
const retryToken = ++this.#ttsrRetryToken;
|
|
1712
|
-
const generation = this.#promptGeneration;
|
|
1713
|
-
const targetMessageTimestamp =
|
|
1714
|
-
event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
1715
|
-
this.#schedulePostPromptTask(
|
|
1716
|
-
async () => {
|
|
1717
|
-
if (this.#ttsrRetryToken !== retryToken) {
|
|
1718
|
-
this.#resolveTtsrResume();
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
1723
|
-
if (
|
|
1724
|
-
!this.#ttsrAbortPending ||
|
|
1725
|
-
this.#promptGeneration !== generation ||
|
|
1726
|
-
targetAssistantIndex === -1
|
|
1727
|
-
) {
|
|
1728
|
-
this.#ttsrAbortPending = false;
|
|
1729
|
-
this.#pendingTtsrInjections = [];
|
|
1730
|
-
this.#perToolTtsrInjections.clear();
|
|
1731
|
-
this.#resolveTtsrResume();
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
this.#ttsrAbortPending = false;
|
|
1735
|
-
this.#perToolTtsrInjections.clear();
|
|
1736
|
-
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
1737
|
-
if (ttsrSettings?.contextMode === "discard") {
|
|
1738
|
-
// Remove the partial/aborted assistant turn from agent state
|
|
1739
|
-
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
1740
|
-
}
|
|
1741
|
-
// Inject TTSR rules as system reminder before retry
|
|
1742
|
-
const injection = this.#getTtsrInjectionContent();
|
|
1743
|
-
if (injection) {
|
|
1744
|
-
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
1745
|
-
this.agent.appendMessage({
|
|
1746
|
-
role: "custom",
|
|
1747
|
-
customType: "ttsr-injection",
|
|
1748
|
-
content: injection.content,
|
|
1749
|
-
display: false,
|
|
1750
|
-
details,
|
|
1751
|
-
attribution: "agent",
|
|
1752
|
-
timestamp: Date.now(),
|
|
1753
|
-
});
|
|
1754
|
-
this.sessionManager.appendCustomMessageEntry(
|
|
1755
|
-
"ttsr-injection",
|
|
1756
|
-
injection.content,
|
|
1757
|
-
false,
|
|
1758
|
-
details,
|
|
1759
|
-
"agent",
|
|
1760
|
-
);
|
|
1761
|
-
this.#markTtsrInjected(details.rules);
|
|
1762
|
-
}
|
|
1763
|
-
try {
|
|
1764
|
-
await this.agent.continue();
|
|
1765
|
-
} catch {
|
|
1766
|
-
this.#resolveTtsrResume();
|
|
1767
|
-
}
|
|
1768
|
-
},
|
|
1769
|
-
{ delayMs: 50 },
|
|
1770
|
-
);
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1677
|
+
if (matches.length > 0 && this.#handleTtsrMatches(matches, matchContext, targetMessageTimestamp)) {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
// ast-grep `astCondition` rules match against the reconstructed edit/write
|
|
1681
|
+
// snapshot, which only exists for tool argument streams. The native worker
|
|
1682
|
+
// call is async, so this path is awaited and self-throttled by the manager.
|
|
1683
|
+
if (matchContext.source === "tool" && this.#ttsrManager?.hasAstRules()) {
|
|
1684
|
+
const astMatches = await this.#checkTtsrAstStream(matchContext, streamingToolCall);
|
|
1685
|
+
if (astMatches.length > 0 && this.#handleTtsrMatches(astMatches, matchContext, targetMessageTimestamp)) {
|
|
1686
|
+
return;
|
|
1773
1687
|
}
|
|
1774
1688
|
}
|
|
1775
1689
|
}
|
|
@@ -2441,19 +2355,134 @@ export class AgentSession {
|
|
|
2441
2355
|
if (!manager) {
|
|
2442
2356
|
return [];
|
|
2443
2357
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
tools.find(t => t.name === toolCall.name) ??
|
|
2448
|
-
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2449
|
-
const digest = tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2450
|
-
if (digest !== undefined) {
|
|
2451
|
-
return manager.checkSnapshot(digest, matchContext);
|
|
2452
|
-
}
|
|
2358
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2359
|
+
if (digest !== undefined) {
|
|
2360
|
+
return manager.checkSnapshot(digest, matchContext);
|
|
2453
2361
|
}
|
|
2454
2362
|
return manager.checkDelta(delta, matchContext);
|
|
2455
2363
|
}
|
|
2456
2364
|
|
|
2365
|
+
/** Reconstruct the tool's normalized source snapshot via its `matcherDigest`, if any. */
|
|
2366
|
+
#resolveTtsrMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
|
|
2367
|
+
if (!toolCall) {
|
|
2368
|
+
return undefined;
|
|
2369
|
+
}
|
|
2370
|
+
const tools = this.agent.state.tools;
|
|
2371
|
+
const tool =
|
|
2372
|
+
tools.find(t => t.name === toolCall.name) ??
|
|
2373
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2374
|
+
return tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* Match ast-grep `astCondition` rules against the reconstructed tool snapshot.
|
|
2379
|
+
*
|
|
2380
|
+
* Only edit/write tool streams expose a `matcherDigest`, which is the real source
|
|
2381
|
+
* the call introduces; AST matching needs that (and a language inferred from the
|
|
2382
|
+
* path argument), so non-digest streams never produce AST matches.
|
|
2383
|
+
*/
|
|
2384
|
+
async #checkTtsrAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
|
|
2385
|
+
const manager = this.#ttsrManager;
|
|
2386
|
+
if (!manager) {
|
|
2387
|
+
return [];
|
|
2388
|
+
}
|
|
2389
|
+
const digest = this.#resolveTtsrMatcherDigest(toolCall);
|
|
2390
|
+
if (digest === undefined) {
|
|
2391
|
+
return [];
|
|
2392
|
+
}
|
|
2393
|
+
return manager.checkAstSnapshot(digest, matchContext);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* Route TTSR matches to either a per-tool injection or a stream-interrupting
|
|
2398
|
+
* retry. Returns true when the stream was aborted and the caller should stop
|
|
2399
|
+
* processing this event.
|
|
2400
|
+
*/
|
|
2401
|
+
#handleTtsrMatches(
|
|
2402
|
+
matches: Rule[],
|
|
2403
|
+
matchContext: TtsrMatchContext,
|
|
2404
|
+
targetMessageTimestamp: number | undefined,
|
|
2405
|
+
): boolean {
|
|
2406
|
+
// Decide first: a non-interrupting tool-source match attaches to the
|
|
2407
|
+
// specific tool call's result instead of driving a loop-wide follow-up.
|
|
2408
|
+
const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
|
|
2409
|
+
const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
|
|
2410
|
+
if (perToolId) {
|
|
2411
|
+
this.#addPerToolTtsrInjections(perToolId, matches);
|
|
2412
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// Queue rules for injection; mark as injected only after successful enqueue.
|
|
2417
|
+
this.#addPendingTtsrInjections(matches);
|
|
2418
|
+
if (!shouldInterrupt) {
|
|
2419
|
+
return false;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
2423
|
+
this.#ttsrAbortPending = true;
|
|
2424
|
+
this.#ensureTtsrResumePromise();
|
|
2425
|
+
this.agent.abort(this.#formatTtsrAbortReason(matches));
|
|
2426
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
2427
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
2428
|
+
// Schedule retry after a short delay
|
|
2429
|
+
const retryToken = ++this.#ttsrRetryToken;
|
|
2430
|
+
const generation = this.#promptGeneration;
|
|
2431
|
+
this.#schedulePostPromptTask(
|
|
2432
|
+
async () => {
|
|
2433
|
+
if (this.#ttsrRetryToken !== retryToken) {
|
|
2434
|
+
this.#resolveTtsrResume();
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
2439
|
+
if (!this.#ttsrAbortPending || this.#promptGeneration !== generation || targetAssistantIndex === -1) {
|
|
2440
|
+
this.#ttsrAbortPending = false;
|
|
2441
|
+
this.#pendingTtsrInjections = [];
|
|
2442
|
+
this.#perToolTtsrInjections.clear();
|
|
2443
|
+
this.#resolveTtsrResume();
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
this.#ttsrAbortPending = false;
|
|
2447
|
+
this.#perToolTtsrInjections.clear();
|
|
2448
|
+
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
2449
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
2450
|
+
// Remove the partial/aborted assistant turn from agent state
|
|
2451
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
2452
|
+
}
|
|
2453
|
+
// Inject TTSR rules as system reminder before retry
|
|
2454
|
+
const injection = this.#getTtsrInjectionContent();
|
|
2455
|
+
if (injection) {
|
|
2456
|
+
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
2457
|
+
this.agent.appendMessage({
|
|
2458
|
+
role: "custom",
|
|
2459
|
+
customType: "ttsr-injection",
|
|
2460
|
+
content: injection.content,
|
|
2461
|
+
display: false,
|
|
2462
|
+
details,
|
|
2463
|
+
attribution: "agent",
|
|
2464
|
+
timestamp: Date.now(),
|
|
2465
|
+
});
|
|
2466
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2467
|
+
"ttsr-injection",
|
|
2468
|
+
injection.content,
|
|
2469
|
+
false,
|
|
2470
|
+
details,
|
|
2471
|
+
"agent",
|
|
2472
|
+
);
|
|
2473
|
+
this.#markTtsrInjected(details.rules);
|
|
2474
|
+
}
|
|
2475
|
+
try {
|
|
2476
|
+
await this.agent.continue();
|
|
2477
|
+
} catch {
|
|
2478
|
+
this.#resolveTtsrResume();
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
{ delayMs: 50 },
|
|
2482
|
+
);
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2457
2486
|
/** Extract path-like arguments from tool call payload for TTSR glob matching. */
|
|
2458
2487
|
#extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
|
|
2459
2488
|
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
@@ -2992,10 +3021,10 @@ export class AgentSession {
|
|
|
2992
3021
|
* `metadata.user_id` shaped like real Claude Code's `getAPIMetadata` output:
|
|
2993
3022
|
* `{ session_id, account_uuid, device_id }`. `account_uuid` is included only
|
|
2994
3023
|
* when an Anthropic OAuth credential with a known account UUID is loaded;
|
|
2995
|
-
* `device_id` is derived from the persistent omp install id
|
|
2996
|
-
* keeps the value in sync with auth-state changes
|
|
2997
|
-
* refresh that surfaces a new account
|
|
2998
|
-
* `#syncAgentSessionId()` on every such event.
|
|
3024
|
+
* `device_id` is derived from both the persistent omp install id and that
|
|
3025
|
+
* account UUID. Resolving live keeps the value in sync with auth-state changes
|
|
3026
|
+
* (login/logout, token refresh that surfaces a new account UUID) without
|
|
3027
|
+
* needing to re-call `#syncAgentSessionId()` on every such event.
|
|
2999
3028
|
*/
|
|
3000
3029
|
#syncAgentSessionId(sessionId?: string): void {
|
|
3001
3030
|
const sid = this.#activeProviderSessionId(sessionId);
|
|
@@ -4286,6 +4315,65 @@ export class AgentSession {
|
|
|
4286
4315
|
};
|
|
4287
4316
|
}
|
|
4288
4317
|
|
|
4318
|
+
async #normalizeMessageContentImages(
|
|
4319
|
+
content: string | (TextContent | ImageContent)[],
|
|
4320
|
+
): Promise<string | (TextContent | ImageContent)[]> {
|
|
4321
|
+
if (typeof content === "string") return content;
|
|
4322
|
+
const images = content.filter((part): part is ImageContent => part.type === "image");
|
|
4323
|
+
if (images.length === 0) return content;
|
|
4324
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4325
|
+
if (!normalizedImages) return content;
|
|
4326
|
+
let imageIndex = 0;
|
|
4327
|
+
return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
async #normalizeAgentMessageImages<T extends AgentMessage>(message: T): Promise<T> {
|
|
4331
|
+
if (!("content" in message)) return message;
|
|
4332
|
+
const content = message.content;
|
|
4333
|
+
if (typeof content !== "string" && !Array.isArray(content)) return message;
|
|
4334
|
+
const normalized = await this.#normalizeMessageContentImages(content as string | (TextContent | ImageContent)[]);
|
|
4335
|
+
if (normalized === content) return message;
|
|
4336
|
+
return { ...message, content: normalized } as T;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
#createMagicKeywordNotices(text: string): CustomMessage[] {
|
|
4340
|
+
const timestamp = Date.now();
|
|
4341
|
+
const turnBudget = parseTurnBudget(text);
|
|
4342
|
+
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4343
|
+
const keywordNotices: CustomMessage[] = [];
|
|
4344
|
+
if (containsUltrathink(text)) {
|
|
4345
|
+
keywordNotices.push({
|
|
4346
|
+
role: "custom",
|
|
4347
|
+
customType: "ultrathink-notice",
|
|
4348
|
+
content: ULTRATHINK_NOTICE,
|
|
4349
|
+
display: false,
|
|
4350
|
+
attribution: "user",
|
|
4351
|
+
timestamp,
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
if (containsOrchestrate(text)) {
|
|
4355
|
+
keywordNotices.push({
|
|
4356
|
+
role: "custom",
|
|
4357
|
+
customType: "orchestrate-notice",
|
|
4358
|
+
content: ORCHESTRATE_NOTICE,
|
|
4359
|
+
display: false,
|
|
4360
|
+
attribution: "user",
|
|
4361
|
+
timestamp,
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
if (containsWorkflow(text)) {
|
|
4365
|
+
keywordNotices.push({
|
|
4366
|
+
role: "custom",
|
|
4367
|
+
customType: "workflow-notice",
|
|
4368
|
+
content: WORKFLOW_NOTICE,
|
|
4369
|
+
display: false,
|
|
4370
|
+
attribution: "user",
|
|
4371
|
+
timestamp,
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
return keywordNotices;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4289
4377
|
/**
|
|
4290
4378
|
* Send a prompt to the agent.
|
|
4291
4379
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -4327,42 +4415,7 @@ export class AgentSession {
|
|
|
4327
4415
|
// Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
|
|
4328
4416
|
// user's message that steer this turn. User-authored prompts only — synthetic /
|
|
4329
4417
|
// agent-initiated turns never trigger them.
|
|
4330
|
-
const keywordNotices
|
|
4331
|
-
if (!options?.synthetic) {
|
|
4332
|
-
const timestamp = Date.now();
|
|
4333
|
-
const turnBudget = parseTurnBudget(expandedText);
|
|
4334
|
-
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4335
|
-
if (containsUltrathink(expandedText)) {
|
|
4336
|
-
keywordNotices.push({
|
|
4337
|
-
role: "custom",
|
|
4338
|
-
customType: "ultrathink-notice",
|
|
4339
|
-
content: ULTRATHINK_NOTICE,
|
|
4340
|
-
display: false,
|
|
4341
|
-
attribution: "user",
|
|
4342
|
-
timestamp,
|
|
4343
|
-
});
|
|
4344
|
-
}
|
|
4345
|
-
if (containsOrchestrate(expandedText)) {
|
|
4346
|
-
keywordNotices.push({
|
|
4347
|
-
role: "custom",
|
|
4348
|
-
customType: "orchestrate-notice",
|
|
4349
|
-
content: ORCHESTRATE_NOTICE,
|
|
4350
|
-
display: false,
|
|
4351
|
-
attribution: "user",
|
|
4352
|
-
timestamp,
|
|
4353
|
-
});
|
|
4354
|
-
}
|
|
4355
|
-
if (containsWorkflow(expandedText)) {
|
|
4356
|
-
keywordNotices.push({
|
|
4357
|
-
role: "custom",
|
|
4358
|
-
customType: "workflow-notice",
|
|
4359
|
-
content: WORKFLOW_NOTICE,
|
|
4360
|
-
display: false,
|
|
4361
|
-
attribution: "user",
|
|
4362
|
-
timestamp,
|
|
4363
|
-
});
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4418
|
+
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
4366
4419
|
|
|
4367
4420
|
// If streaming, queue via steer() or followUp() based on option
|
|
4368
4421
|
if (this.isStreaming) {
|
|
@@ -4385,10 +4438,11 @@ export class AgentSession {
|
|
|
4385
4438
|
const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
|
|
4386
4439
|
const eagerTodoPrelude =
|
|
4387
4440
|
!options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
|
|
4441
|
+
const normalizedImages = await normalizeModelContextImages(options?.images);
|
|
4388
4442
|
|
|
4389
4443
|
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
|
|
4390
|
-
if (
|
|
4391
|
-
userContent.push(...
|
|
4444
|
+
if (normalizedImages) {
|
|
4445
|
+
userContent.push(...normalizedImages);
|
|
4392
4446
|
}
|
|
4393
4447
|
|
|
4394
4448
|
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
@@ -4405,6 +4459,7 @@ export class AgentSession {
|
|
|
4405
4459
|
try {
|
|
4406
4460
|
await this.#promptWithMessage(message, expandedText, {
|
|
4407
4461
|
...options,
|
|
4462
|
+
images: normalizedImages,
|
|
4408
4463
|
prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
|
|
4409
4464
|
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4410
4465
|
});
|
|
@@ -4430,11 +4485,24 @@ export class AgentSession {
|
|
|
4430
4485
|
.map(content => content.text)
|
|
4431
4486
|
.join("");
|
|
4432
4487
|
|
|
4488
|
+
let keywordNotices: CustomMessage[] = [];
|
|
4489
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
|
|
4490
|
+
const details = message.details;
|
|
4491
|
+
let skillArgs = "";
|
|
4492
|
+
if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
|
|
4493
|
+
skillArgs = details.args;
|
|
4494
|
+
}
|
|
4495
|
+
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4433
4498
|
if (this.isStreaming) {
|
|
4434
4499
|
if (!options?.streamingBehavior) {
|
|
4435
4500
|
throw new AgentBusyError();
|
|
4436
4501
|
}
|
|
4437
4502
|
await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
|
|
4503
|
+
for (const notice of keywordNotices) {
|
|
4504
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
4505
|
+
}
|
|
4438
4506
|
return;
|
|
4439
4507
|
}
|
|
4440
4508
|
|
|
@@ -4448,7 +4516,10 @@ export class AgentSession {
|
|
|
4448
4516
|
timestamp: Date.now(),
|
|
4449
4517
|
};
|
|
4450
4518
|
|
|
4451
|
-
await this.#promptWithMessage(customMessage, textContent,
|
|
4519
|
+
await this.#promptWithMessage(customMessage, textContent, {
|
|
4520
|
+
...options,
|
|
4521
|
+
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4522
|
+
});
|
|
4452
4523
|
}
|
|
4453
4524
|
|
|
4454
4525
|
async #promptWithMessage(
|
|
@@ -4547,7 +4618,9 @@ export class AgentSession {
|
|
|
4547
4618
|
useHashLines: resolveFileDisplayMode(this).hashLines,
|
|
4548
4619
|
snapshotStore: getFileSnapshotStore(this),
|
|
4549
4620
|
});
|
|
4550
|
-
|
|
4621
|
+
for (const fileMentionMessage of fileMentionMessages) {
|
|
4622
|
+
messages.push(await this.#normalizeAgentMessageImages(fileMentionMessage));
|
|
4623
|
+
}
|
|
4551
4624
|
}
|
|
4552
4625
|
|
|
4553
4626
|
const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
|
|
@@ -4563,15 +4636,18 @@ export class AgentSession {
|
|
|
4563
4636
|
const promptAttribution: "user" | "agent" | undefined =
|
|
4564
4637
|
"attribution" in message ? message.attribution : undefined;
|
|
4565
4638
|
for (const msg of result.messages) {
|
|
4566
|
-
messages.push(
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4639
|
+
messages.push(
|
|
4640
|
+
await this.#normalizeAgentMessageImages({
|
|
4641
|
+
role: "custom",
|
|
4642
|
+
customType: msg.customType,
|
|
4643
|
+
content: msg.content,
|
|
4644
|
+
display: msg.display,
|
|
4645
|
+
details: msg.details,
|
|
4646
|
+
attribution:
|
|
4647
|
+
msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
|
|
4648
|
+
timestamp: Date.now(),
|
|
4649
|
+
}),
|
|
4650
|
+
);
|
|
4575
4651
|
}
|
|
4576
4652
|
}
|
|
4577
4653
|
|
|
@@ -4779,11 +4855,12 @@ export class AgentSession {
|
|
|
4779
4855
|
* Internal: Queue a steering message (already expanded, no extension command check).
|
|
4780
4856
|
*/
|
|
4781
4857
|
async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
4858
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4782
4859
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
4783
4860
|
this.#steeringMessages.push({ text: displayText });
|
|
4784
4861
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
4785
|
-
if (
|
|
4786
|
-
content.push(...
|
|
4862
|
+
if (normalizedImages && normalizedImages.length > 0) {
|
|
4863
|
+
content.push(...normalizedImages);
|
|
4787
4864
|
}
|
|
4788
4865
|
this.agent.steer({
|
|
4789
4866
|
role: "user",
|
|
@@ -4798,11 +4875,12 @@ export class AgentSession {
|
|
|
4798
4875
|
* Internal: Queue a follow-up message (already expanded, no extension command check).
|
|
4799
4876
|
*/
|
|
4800
4877
|
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
4878
|
+
const normalizedImages = await normalizeModelContextImages(images);
|
|
4801
4879
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
4802
4880
|
this.#followUpMessages.push({ text: displayText });
|
|
4803
4881
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
4804
|
-
if (
|
|
4805
|
-
content.push(...
|
|
4882
|
+
if (normalizedImages && normalizedImages.length > 0) {
|
|
4883
|
+
content.push(...normalizedImages);
|
|
4806
4884
|
}
|
|
4807
4885
|
this.agent.followUp({
|
|
4808
4886
|
role: "user",
|
|
@@ -4946,16 +5024,17 @@ export class AgentSession {
|
|
|
4946
5024
|
attribution: message.attribution ?? "agent",
|
|
4947
5025
|
timestamp: Date.now(),
|
|
4948
5026
|
};
|
|
5027
|
+
const normalizedAppMessage = await this.#normalizeAgentMessageImages(appMessage);
|
|
4949
5028
|
if (this.isStreaming) {
|
|
4950
5029
|
if (options?.deliverAs === "nextTurn") {
|
|
4951
|
-
this.#queueHiddenNextTurnMessage(
|
|
5030
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, options?.triggerTurn ?? false);
|
|
4952
5031
|
return;
|
|
4953
5032
|
}
|
|
4954
5033
|
|
|
4955
5034
|
if (options?.deliverAs === "followUp") {
|
|
4956
|
-
this.agent.followUp(
|
|
5035
|
+
this.agent.followUp(normalizedAppMessage);
|
|
4957
5036
|
} else {
|
|
4958
|
-
this.agent.steer(
|
|
5037
|
+
this.agent.steer(normalizedAppMessage);
|
|
4959
5038
|
}
|
|
4960
5039
|
return;
|
|
4961
5040
|
}
|
|
@@ -4963,16 +5042,16 @@ export class AgentSession {
|
|
|
4963
5042
|
if (options?.deliverAs === "nextTurn") {
|
|
4964
5043
|
if (options?.triggerTurn) {
|
|
4965
5044
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4966
|
-
this.#queueHiddenNextTurnMessage(
|
|
5045
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
4967
5046
|
return;
|
|
4968
5047
|
}
|
|
4969
|
-
await this.agent.prompt(
|
|
5048
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
4970
5049
|
return;
|
|
4971
5050
|
}
|
|
4972
|
-
this.agent.appendMessage(
|
|
5051
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4973
5052
|
this.sessionManager.appendCustomMessageEntry(
|
|
4974
|
-
|
|
4975
|
-
|
|
5053
|
+
normalizedAppMessage.customType,
|
|
5054
|
+
normalizedAppMessage.content,
|
|
4976
5055
|
message.display,
|
|
4977
5056
|
message.details,
|
|
4978
5057
|
message.attribution ?? "agent",
|
|
@@ -4982,17 +5061,17 @@ export class AgentSession {
|
|
|
4982
5061
|
|
|
4983
5062
|
if (options?.triggerTurn) {
|
|
4984
5063
|
if (this.#clientBridge?.deferAgentInitiatedTurns && !this.#allowAcpAgentInitiatedTurns) {
|
|
4985
|
-
this.#queueHiddenNextTurnMessage(
|
|
5064
|
+
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
4986
5065
|
return;
|
|
4987
5066
|
}
|
|
4988
|
-
await this.agent.prompt(
|
|
5067
|
+
await this.agent.prompt(normalizedAppMessage);
|
|
4989
5068
|
return;
|
|
4990
5069
|
}
|
|
4991
5070
|
|
|
4992
|
-
this.agent.appendMessage(
|
|
5071
|
+
this.agent.appendMessage(normalizedAppMessage);
|
|
4993
5072
|
this.sessionManager.appendCustomMessageEntry(
|
|
4994
|
-
|
|
4995
|
-
|
|
5073
|
+
normalizedAppMessage.customType,
|
|
5074
|
+
normalizedAppMessage.content,
|
|
4996
5075
|
message.display,
|
|
4997
5076
|
message.details,
|
|
4998
5077
|
message.attribution ?? "agent",
|
|
@@ -6749,9 +6828,13 @@ export class AgentSession {
|
|
|
6749
6828
|
return undefined;
|
|
6750
6829
|
}
|
|
6751
6830
|
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6831
|
+
// Must check the active tool set, not just the registry: tool discovery
|
|
6832
|
+
// (tools.discoveryMode === "all") can register `todo` while hiding it from
|
|
6833
|
+
// the exposed tools. Forcing a named tool_choice for an inactive tool makes
|
|
6834
|
+
// the provider reject the request (HTTP 400).
|
|
6835
|
+
if (!this.getActiveToolNames().includes("todo")) {
|
|
6836
|
+
logger.warn("Eager todo enforcement skipped because todo is not active", {
|
|
6837
|
+
activeToolNames: this.getActiveToolNames(),
|
|
6755
6838
|
});
|
|
6756
6839
|
return undefined;
|
|
6757
6840
|
}
|
|
@@ -6913,7 +6996,6 @@ export class AgentSession {
|
|
|
6913
6996
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
6914
6997
|
}
|
|
6915
6998
|
this.agent.setModel(model);
|
|
6916
|
-
this.#syncToolCallBatchCap(model);
|
|
6917
6999
|
|
|
6918
7000
|
// Re-evaluate append-only context mode — provider or setting may have changed
|
|
6919
7001
|
this.#syncAppendOnlyContext(model);
|
|
@@ -7775,16 +7857,32 @@ export class AgentSession {
|
|
|
7775
7857
|
return "handled";
|
|
7776
7858
|
}
|
|
7777
7859
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7778
|
-
//
|
|
7779
|
-
//
|
|
7780
|
-
|
|
7860
|
+
// Detect the dead-loop reported in issue #2119: the threshold check fires,
|
|
7861
|
+
// shake runs, but the resulting context is still above the configured
|
|
7862
|
+
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
7863
|
+
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
7864
|
+
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
7865
|
+
// for the existing "overflow + nothing reclaimed" case. In every recovery
|
|
7866
|
+
// reason we hand off to the summarization-driven context-full path so the
|
|
7867
|
+
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
7868
|
+
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
7869
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7870
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
7871
|
+
const postShakeTokens = contextWindow > 0 ? this.#estimatePendingPromptTokens([]) : 0;
|
|
7872
|
+
const stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
7873
|
+
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
7874
|
+
if (shouldFallBack) {
|
|
7875
|
+
const errorMessage = reclaimed
|
|
7876
|
+
? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
|
|
7877
|
+
: "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
|
|
7781
7878
|
await this.#emitSessionEvent({
|
|
7782
7879
|
type: "auto_compaction_end",
|
|
7783
7880
|
action,
|
|
7784
7881
|
result: undefined,
|
|
7785
7882
|
aborted: false,
|
|
7786
7883
|
willRetry: false,
|
|
7787
|
-
skipped:
|
|
7884
|
+
skipped: !reclaimed,
|
|
7885
|
+
errorMessage,
|
|
7788
7886
|
});
|
|
7789
7887
|
return "fallback";
|
|
7790
7888
|
}
|
|
@@ -9109,7 +9207,6 @@ export class AgentSession {
|
|
|
9109
9207
|
this.#setModelWithProviderSessionReset(match);
|
|
9110
9208
|
} else {
|
|
9111
9209
|
this.agent.setModel(match);
|
|
9112
|
-
this.#syncToolCallBatchCap(match);
|
|
9113
9210
|
}
|
|
9114
9211
|
}
|
|
9115
9212
|
}
|
|
@@ -9192,9 +9289,6 @@ export class AgentSession {
|
|
|
9192
9289
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
9193
9290
|
if (previousModel) {
|
|
9194
9291
|
this.agent.setModel(previousModel);
|
|
9195
|
-
this.#syncToolCallBatchCap(previousModel);
|
|
9196
|
-
} else {
|
|
9197
|
-
this.#syncToolCallBatchCap(undefined);
|
|
9198
9292
|
}
|
|
9199
9293
|
this.#thinkingLevel = previousThinkingLevel;
|
|
9200
9294
|
this.#autoThinking = previousAutoThinking;
|