@bubblebrain-ai/bubble 0.0.58 → 0.0.60
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/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
package/dist/agent.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { toolImageObservation } from "./context/tool-images.js";
|
|
1
2
|
/**
|
|
2
3
|
* Agent - The core decision loop.
|
|
3
4
|
* It maintains message state, calls the LLM, executes tools, and auto-continues.
|
|
4
5
|
*/
|
|
5
|
-
import { compactCurrentTurnToolGroups, compactMessages } from "./context/compact.js";
|
|
6
|
+
import { compactCurrentTurnToolGroups, compactMessages, isCompactionSummaryMessage, splitLeadingContext } from "./context/compact.js";
|
|
7
|
+
import { createContextCheckpoint, checkpointMessages, hasCompleteToolGroups } from "./context/checkpoint.js";
|
|
6
8
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { compactMessagesWithLLM } from "./context/compact-llm.js";
|
|
8
9
|
import { estimateToolDefinitionsTokens, getContextBudget, getMaxInputTokens, } from "./context/budget.js";
|
|
9
10
|
import { buildContextUsageSnapshot } from "./context/usage.js";
|
|
10
11
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
11
12
|
import { computeRetryDelayMs, isProviderStreamInterruption, providerStreamRetryLimit, sleepBeforeRetry, } from "./network/retry.js";
|
|
12
13
|
import { projectMessages } from "./context/projector.js";
|
|
13
|
-
import { aggressivePruneMessages, markStableCurrentToolResultsForCache, markToolResultCacheStable } from "./context/prune.js";
|
|
14
|
+
import { aggressivePruneMessages, pruneMessages, markStableCurrentToolResultsForCache, markToolResultCacheStable } from "./context/prune.js";
|
|
14
15
|
import { normalizeToolResultForModel, truncateToolOutputForModel } from "./context/tool-output-truncate.js";
|
|
15
16
|
import { buildDeferredToolsReminder, buildToolFreezeReminder, isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
16
17
|
import { HookBus } from "./orchestrator/hooks.js";
|
|
@@ -127,6 +128,9 @@ export class Agent {
|
|
|
127
128
|
_modeVersion = 0;
|
|
128
129
|
onModeUpdate;
|
|
129
130
|
onCompactionApplied;
|
|
131
|
+
onContextCheckpoint;
|
|
132
|
+
getContextRevision;
|
|
133
|
+
lastCompactionId;
|
|
130
134
|
onMessageAppend;
|
|
131
135
|
onProviderError;
|
|
132
136
|
onToolResult;
|
|
@@ -139,6 +143,7 @@ export class Agent {
|
|
|
139
143
|
budgetLedger;
|
|
140
144
|
budgetSource;
|
|
141
145
|
skillSummaries;
|
|
146
|
+
skillCatalogChars;
|
|
142
147
|
memoryPrompt;
|
|
143
148
|
fileStateTracker;
|
|
144
149
|
agentCategories;
|
|
@@ -166,7 +171,8 @@ export class Agent {
|
|
|
166
171
|
// unanswerable from the outside. `fired` counts successful compaction
|
|
167
172
|
// computations including ones whose rewrite was later rejected — a
|
|
168
173
|
// fired-but-never-written gap is the churn signal.
|
|
169
|
-
compactionStats = { resident: 0, subturn: 0, llm: 0, overflow: 0, fired: 0, droppedMessages: 0 };
|
|
174
|
+
compactionStats = { resident: 0, subturn: 0, llm: 0, heuristic: 0, overflow: 0, fired: 0, droppedMessages: 0 };
|
|
175
|
+
contextEvents = [];
|
|
170
176
|
constructor(options) {
|
|
171
177
|
this.provider = options.provider;
|
|
172
178
|
this.sessionID = options.sessionID;
|
|
@@ -180,6 +186,8 @@ export class Agent {
|
|
|
180
186
|
this.onToolResult = options.onToolResult;
|
|
181
187
|
this.onModeUpdate = options.onModeUpdate;
|
|
182
188
|
this.onCompactionApplied = options.onCompactionApplied;
|
|
189
|
+
this.onContextCheckpoint = options.onContextCheckpoint;
|
|
190
|
+
this.getContextRevision = options.getContextRevision;
|
|
183
191
|
this.hookDefinitions = options.hooks ?? [];
|
|
184
192
|
this.externalHooks = options.externalHooks;
|
|
185
193
|
this.agentRole = options.agentRole ?? "parent";
|
|
@@ -189,6 +197,7 @@ export class Agent {
|
|
|
189
197
|
this.budgetLedger = options.budgetLedger;
|
|
190
198
|
this.budgetSource = options.budgetSource ?? { runId: this.sessionID ?? "agent" };
|
|
191
199
|
this.skillSummaries = options.skills ?? [];
|
|
200
|
+
this.skillCatalogChars = options.skillCatalogChars;
|
|
192
201
|
this.memoryPrompt = options.memoryPrompt;
|
|
193
202
|
this.fileStateTracker = options.fileStateTracker;
|
|
194
203
|
this.agentCategories = options.agentCategories ?? {};
|
|
@@ -230,6 +239,8 @@ export class Agent {
|
|
|
230
239
|
// must see deferred-but-registered tools (an explicit include in a
|
|
231
240
|
// profile is the author pre-unlocking them).
|
|
232
241
|
allTools: () => [...this.tools.values()],
|
|
242
|
+
getSkillSummaries: () => this.getSkillSummaries(),
|
|
243
|
+
getSkillCatalogChars: () => this.getSkillCatalogChars(),
|
|
233
244
|
createChild: (spec) => this.createChildAgent(spec),
|
|
234
245
|
recordProviderError: (error, context) => this.recordProviderError(error, context),
|
|
235
246
|
runExternalHook: (input, abortSignal) => this.runExternalHook(input, abortSignal),
|
|
@@ -365,6 +376,9 @@ export class Agent {
|
|
|
365
376
|
}
|
|
366
377
|
getSystemPromptToolOptions() {
|
|
367
378
|
return {
|
|
379
|
+
// Rebuilt prompts (model switch, /skills changes) keep the Skills catalog.
|
|
380
|
+
skills: this.skillSummaries,
|
|
381
|
+
skillCatalogChars: this.skillCatalogChars,
|
|
368
382
|
...buildToolPromptOptions(this.getActiveToolEntries()),
|
|
369
383
|
// Rendered through the live accessor (design §1.5), so every host-
|
|
370
384
|
// triggered prompt rebuild picks up the current catalog.
|
|
@@ -374,7 +388,13 @@ export class Agent {
|
|
|
374
388
|
memoryPrompt: this.memoryPrompt,
|
|
375
389
|
};
|
|
376
390
|
}
|
|
377
|
-
|
|
391
|
+
getSkillSummaries() {
|
|
392
|
+
return this.skillSummaries.slice();
|
|
393
|
+
}
|
|
394
|
+
getSkillCatalogChars() {
|
|
395
|
+
return this.skillCatalogChars;
|
|
396
|
+
}
|
|
397
|
+
/** Refresh the live Skill catalog after /skills reloads or changes enablement; hosts then rebuild the system prompt. */
|
|
378
398
|
setSkillSummaries(skills) {
|
|
379
399
|
this.skillSummaries = skills.slice();
|
|
380
400
|
this.notifyContextChanged();
|
|
@@ -840,13 +860,18 @@ export class Agent {
|
|
|
840
860
|
// [LLM summary] + [last user msg], and the projector becomes a no-op for
|
|
841
861
|
// budget. If it fails (network error, etc.), the projector's existing
|
|
842
862
|
// algorithmic fallback still kicks in.
|
|
843
|
-
await this.maybeCompactWithLLM(toolDefinitionTokens)
|
|
863
|
+
for await (const event of this.maybeCompactWithLLM(toolDefinitionTokens, abortSignal))
|
|
864
|
+
yield emit(event);
|
|
865
|
+
for (const event of this.contextEvents.splice(0))
|
|
866
|
+
yield emit(event);
|
|
844
867
|
const bufferedStreamingToolCallIds = new Set();
|
|
845
868
|
let currentRequestEstimate;
|
|
869
|
+
let currentContextWindow;
|
|
846
870
|
try {
|
|
847
871
|
markStableCurrentToolResultsForCache(this.messages);
|
|
848
872
|
const projectedMessages = projectMessages(this.messages, {
|
|
849
|
-
|
|
873
|
+
// Semantic compaction is committed above; projection only renders/thins.
|
|
874
|
+
mode: "pruned",
|
|
850
875
|
providerId: this.providerId,
|
|
851
876
|
modelId: this.apiModel,
|
|
852
877
|
// Projection changes message identity/count (meta rendering, pruning,
|
|
@@ -857,6 +882,8 @@ export class Agent {
|
|
|
857
882
|
currentRequestMessageCount = projectedMessages.length;
|
|
858
883
|
const requestBudget = getContextBudget(this.providerId, this.apiModel, projectedMessages, { additionalInputTokens: toolDefinitionTokens });
|
|
859
884
|
currentRequestEstimate = requestBudget.estimatedTokens;
|
|
885
|
+
currentContextWindow = requestBudget.contextWindow;
|
|
886
|
+
yield emit({ type: "context_usage", usedTokens: currentRequestEstimate, contextWindow: currentContextWindow, estimated: true });
|
|
860
887
|
const maxInputTokens = getMaxInputTokens(requestBudget.contextWindow);
|
|
861
888
|
if (maxInputTokens !== undefined && currentRequestEstimate > maxInputTokens) {
|
|
862
889
|
throw new LocalContextPreflightError(currentRequestEstimate, maxInputTokens);
|
|
@@ -1069,6 +1096,11 @@ export class Agent {
|
|
|
1069
1096
|
// turn_start resets the streaming display, so re-issuing the whole
|
|
1070
1097
|
// request is safe.
|
|
1071
1098
|
consecutiveStreamInterruptionRetries += 1;
|
|
1099
|
+
this.persistProviderError(error, {
|
|
1100
|
+
messageCount: currentRequestMessageCount,
|
|
1101
|
+
toolCount: toolDefinitions.length,
|
|
1102
|
+
retry: { attempt: consecutiveStreamInterruptionRetries, maxAttempts: streamRetryLimit },
|
|
1103
|
+
});
|
|
1072
1104
|
yield emit({
|
|
1073
1105
|
type: "provider_retry",
|
|
1074
1106
|
attempt: consecutiveStreamInterruptionRetries,
|
|
@@ -1098,17 +1130,21 @@ export class Agent {
|
|
|
1098
1130
|
throw error;
|
|
1099
1131
|
}
|
|
1100
1132
|
const messagesBeforeRecovery = this.messages;
|
|
1101
|
-
const
|
|
1102
|
-
const anchorBeforeRecovery = this.lastAnchorMessageCount;
|
|
1133
|
+
const revisionBeforeRecovery = this.getContextRevision?.();
|
|
1103
1134
|
const failedRequestEstimate = currentRequestEstimate ?? getContextBudget(this.providerId, this.apiModel, projectMessages(this.messages, {
|
|
1104
|
-
mode: "
|
|
1135
|
+
mode: "pruned",
|
|
1105
1136
|
providerId: this.providerId,
|
|
1106
1137
|
modelId: this.apiModel,
|
|
1107
1138
|
additionalInputTokens: toolDefinitionTokens,
|
|
1108
1139
|
}), { additionalInputTokens: toolDefinitionTokens }).estimatedTokens;
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1140
|
+
const candidate = await this.recoverFromOverflow(consecutiveOverflowRecoveries, error instanceof LocalContextPreflightError ? 0.999_999 : 0.9, abortSignal);
|
|
1141
|
+
if (!candidate) {
|
|
1142
|
+
persistTerminalProviderError(error);
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
const droppedMessages = messagesBeforeRecovery.length - candidate.length;
|
|
1146
|
+
const recoveredRequestEstimate = getContextBudget(this.providerId, this.apiModel, projectMessages(candidate, {
|
|
1147
|
+
mode: "pruned",
|
|
1112
1148
|
providerId: this.providerId,
|
|
1113
1149
|
modelId: this.apiModel,
|
|
1114
1150
|
additionalInputTokens: toolDefinitionTokens,
|
|
@@ -1120,12 +1156,10 @@ export class Agent {
|
|
|
1120
1156
|
const meaningfulProviderProgress = error instanceof LocalContextPreflightError
|
|
1121
1157
|
|| recoveredRequestEstimate <= Math.floor(failedRequestEstimate * 0.9);
|
|
1122
1158
|
if (!madeProgress || !meaningfulProviderProgress) {
|
|
1123
|
-
this.messages = messagesBeforeRecovery;
|
|
1124
|
-
this.lastInputTokens = inputTokensBeforeRecovery;
|
|
1125
|
-
this.lastAnchorMessageCount = anchorBeforeRecovery;
|
|
1126
1159
|
persistTerminalProviderError(error);
|
|
1127
1160
|
throw error;
|
|
1128
1161
|
}
|
|
1162
|
+
this.applyContextCheckpoint(candidate, "overflow", undefined, revisionBeforeRecovery, abortSignal);
|
|
1129
1163
|
consecutiveOverflowRecoveries += 1;
|
|
1130
1164
|
this.compactionStats.overflow += 1;
|
|
1131
1165
|
this.compactionStats.fired += 1;
|
|
@@ -1137,11 +1171,16 @@ export class Agent {
|
|
|
1137
1171
|
afterEstimatedTokens: recoveredRequestEstimate,
|
|
1138
1172
|
});
|
|
1139
1173
|
yield emit({ type: "context_recovered", droppedMessages, reason: "overflow" });
|
|
1174
|
+
yield emit({ type: "context_compaction", status: "completed", compactionId: this.lastCompactionId, persisted: !!this.onContextCheckpoint, preTokens: failedRequestEstimate, postTokens: recoveredRequestEstimate, contextWindow: currentContextWindow });
|
|
1140
1175
|
continue;
|
|
1141
1176
|
}
|
|
1142
1177
|
consecutiveOverflowRecoveries = 0;
|
|
1143
1178
|
consecutiveEmptyAssistantRecoveries = 0;
|
|
1144
1179
|
consecutiveStreamInterruptionRetries = 0;
|
|
1180
|
+
if (turnUsage && Number.isFinite(turnUsage.promptTokens)) {
|
|
1181
|
+
// Provider prompt usage already includes cached input; never add cache hits twice.
|
|
1182
|
+
yield emit({ type: "context_usage", usedTokens: turnUsage.promptTokens + (turnUsage.completionTokens || 0), contextWindow: currentContextWindow, estimated: false });
|
|
1183
|
+
}
|
|
1145
1184
|
// Execute tools if any
|
|
1146
1185
|
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
1147
1186
|
const parsedCalls = [];
|
|
@@ -1448,11 +1487,12 @@ export class Agent {
|
|
|
1448
1487
|
// synchronously, so a mark added on a later turn never reaches disk.
|
|
1449
1488
|
markToolResultCacheStable(tc.name, toolMessage);
|
|
1450
1489
|
this.appendMessage(toolMessage);
|
|
1451
|
-
this.compactResidentHistory();
|
|
1452
1490
|
flushQueuedReminders();
|
|
1453
1491
|
this.onToolResult?.(tc.name, result);
|
|
1454
1492
|
executedResults.push(result);
|
|
1455
1493
|
yield emit({ type: "tool_end", id: tc.id, name: tc.name, result });
|
|
1494
|
+
for (const event of this.contextEvents.splice(0))
|
|
1495
|
+
yield emit(event);
|
|
1456
1496
|
for (const update of this.subagents.drainToolUpdates())
|
|
1457
1497
|
yield emit(update);
|
|
1458
1498
|
if (this._modeVersion !== modeVersionBefore) {
|
|
@@ -1463,6 +1503,14 @@ export class Agent {
|
|
|
1463
1503
|
throwIfAborted(abortSignal);
|
|
1464
1504
|
}
|
|
1465
1505
|
}
|
|
1506
|
+
const imageObservation = toolImageObservation(executedResults);
|
|
1507
|
+
if (imageObservation)
|
|
1508
|
+
this.appendMessage(imageObservation);
|
|
1509
|
+
// A checkpoint must contain the whole batch, never provider repair's
|
|
1510
|
+
// placeholder for a sibling that has not executed yet.
|
|
1511
|
+
this.compactResidentHistory();
|
|
1512
|
+
for (const event of this.contextEvents.splice(0))
|
|
1513
|
+
yield emit(event);
|
|
1466
1514
|
await hookBus.runBeforeContinuation({
|
|
1467
1515
|
agent: this,
|
|
1468
1516
|
cwd,
|
|
@@ -1508,12 +1556,17 @@ export class Agent {
|
|
|
1508
1556
|
}, abortSignal);
|
|
1509
1557
|
for (const event of stopHook.events)
|
|
1510
1558
|
yield emit(event);
|
|
1511
|
-
const willContinue = !!hookState.forceContinuationReason
|
|
1559
|
+
const willContinue = !!hookState.forceContinuationReason
|
|
1560
|
+
|| (options.continueOnPendingInput === true && pendingInputCount() > 0);
|
|
1512
1561
|
yield emit({ type: "turn_end", usage: turnUsage, systemFingerprint: turnSystemFingerprint, willContinue });
|
|
1513
1562
|
if (willContinue) {
|
|
1514
1563
|
delete hookState.forceContinuationReason;
|
|
1515
1564
|
continue;
|
|
1516
1565
|
}
|
|
1566
|
+
// A follow-up can arrive while the consumer handles turn_end. Recheck
|
|
1567
|
+
// synchronously before closing admission so an accepted input is not lost.
|
|
1568
|
+
if (options.continueOnPendingInput && pendingInputCount() > 0)
|
|
1569
|
+
continue;
|
|
1517
1570
|
for (const event of await rejectPendingInputs("no_continuation"))
|
|
1518
1571
|
yield emit(event);
|
|
1519
1572
|
break;
|
|
@@ -1553,25 +1606,15 @@ export class Agent {
|
|
|
1553
1606
|
}, traceContext);
|
|
1554
1607
|
}
|
|
1555
1608
|
}
|
|
1556
|
-
async recoverFromOverflow(attempt, maximumSizeRatio = 0.999_999) {
|
|
1557
|
-
const before = this.messages.length;
|
|
1609
|
+
async recoverFromOverflow(attempt, maximumSizeRatio = 0.999_999, abortSignal) {
|
|
1558
1610
|
const originalMessages = this.messages;
|
|
1559
1611
|
const beforeBytes = serializedMessageBytes(originalMessages);
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|| serializedMessageBytes(candidate) > Math.floor(beforeBytes * maximumSizeRatio))
|
|
1563
|
-
return false;
|
|
1564
|
-
this.messages = candidate;
|
|
1565
|
-
this.lastInputTokens = null;
|
|
1566
|
-
this.lastAnchorMessageCount = null;
|
|
1567
|
-
this.fileStateTracker?.invalidateReadHistory();
|
|
1568
|
-
this.persistCompactionSummary(summary);
|
|
1569
|
-
return true;
|
|
1570
|
-
};
|
|
1612
|
+
const smaller = (candidate) => !!candidate && serializedMessageBytes(candidate) <= Math.floor(beforeBytes * maximumSizeRatio);
|
|
1613
|
+
throwIfAborted(abortSignal);
|
|
1571
1614
|
if (attempt === 0) {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1615
|
+
const pruned = aggressivePruneMessages(originalMessages);
|
|
1616
|
+
if (smaller(pruned))
|
|
1617
|
+
return pruned;
|
|
1575
1618
|
}
|
|
1576
1619
|
// Unlike ordinary pruning, overflow recovery is allowed to thin the most
|
|
1577
1620
|
// recent completed tool batch. Keeping it protected is exactly what made
|
|
@@ -1583,67 +1626,42 @@ export class Agent {
|
|
|
1583
1626
|
const bounded = truncateToolOutputForModel(message.content, this.providerId, this.apiModel, { hardByteLimit: emergencyToolLimit });
|
|
1584
1627
|
return bounded.truncated ? { ...message, content: bounded.content } : message;
|
|
1585
1628
|
});
|
|
1586
|
-
if (
|
|
1587
|
-
return
|
|
1588
|
-
}
|
|
1629
|
+
if (smaller(thinnedLatestTools))
|
|
1630
|
+
return thinnedLatestTools;
|
|
1589
1631
|
const keepRecentTurns = attempt >= 1 ? 1 : 2;
|
|
1590
|
-
|
|
1591
|
-
provider: this.provider,
|
|
1592
|
-
model: this.apiModel,
|
|
1593
|
-
thinkingLevel: this.thinkingLevel,
|
|
1594
|
-
keepRecentTurns,
|
|
1595
|
-
});
|
|
1596
|
-
if (llmResult.compacted && commitIfSmaller(llmResult.messages, llmResult.summary)) {
|
|
1597
|
-
return before - this.messages.length;
|
|
1598
|
-
}
|
|
1599
|
-
// Single-turn capable LLM compactor. compactMessagesWithLLM above no-ops
|
|
1600
|
-
// when there's only one user turn (the "single huge prompt with many tool
|
|
1601
|
-
// calls" case), so try the turn-internal compactor before giving up.
|
|
1632
|
+
// Use the same budget-aware summarizer for single- and multi-turn history.
|
|
1602
1633
|
const { compactWithLLM } = await import("./context/llm-compactor.js");
|
|
1603
1634
|
const singleTurnResult = await compactWithLLM(originalMessages, {
|
|
1604
1635
|
provider: this.provider,
|
|
1605
1636
|
modelId: this.apiModel,
|
|
1637
|
+
providerId: this.providerId,
|
|
1638
|
+
abortSignal,
|
|
1606
1639
|
});
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1640
|
+
throwIfAborted(abortSignal);
|
|
1641
|
+
if (singleTurnResult.compacted && smaller(singleTurnResult.messages))
|
|
1642
|
+
return singleTurnResult.messages;
|
|
1610
1643
|
const fallback = compactMessages(originalMessages, { keepRecentTurns });
|
|
1611
|
-
if (fallback.compacted &&
|
|
1612
|
-
return
|
|
1613
|
-
}
|
|
1644
|
+
if (fallback.compacted && smaller(fallback.messages))
|
|
1645
|
+
return fallback.messages;
|
|
1614
1646
|
const subturn = compactCurrentTurnToolGroups(originalMessages, {
|
|
1615
1647
|
keepRecentGroups: attempt === 0 ? 1 : 0,
|
|
1616
1648
|
});
|
|
1617
|
-
if (subturn.compacted &&
|
|
1618
|
-
return
|
|
1619
|
-
|
|
1620
|
-
//
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
.filter((index) => index >= 0);
|
|
1626
|
-
const oldestIdx = realUserIndexes.length > 1
|
|
1627
|
-
? realUserIndexes[0]
|
|
1628
|
-
: -1;
|
|
1629
|
-
const removeUntil = realUserIndexes.length > 1
|
|
1630
|
-
? realUserIndexes[1]
|
|
1631
|
-
: -1;
|
|
1632
|
-
if (oldestIdx >= 0 && removeUntil > oldestIdx && removeUntil < originalMessages.length) {
|
|
1633
|
-
const candidate = [
|
|
1634
|
-
...originalMessages.slice(0, oldestIdx),
|
|
1635
|
-
...originalMessages.slice(removeUntil),
|
|
1636
|
-
];
|
|
1637
|
-
if (!commitIfSmaller(candidate))
|
|
1638
|
-
return 0;
|
|
1639
|
-
return before - this.messages.length;
|
|
1649
|
+
if (subturn.compacted && smaller(subturn.messages))
|
|
1650
|
+
return subturn.messages;
|
|
1651
|
+
// A two-turn text history has nothing to give at keepRecentTurns 2 and no
|
|
1652
|
+
// tool groups; returning undefined on attempt 0 aborts before attempt 1.
|
|
1653
|
+
if (keepRecentTurns > 1) {
|
|
1654
|
+
const lastTurnOnly = compactMessages(originalMessages, { keepRecentTurns: 1 });
|
|
1655
|
+
if (lastTurnOnly.compacted && smaller(lastTurnOnly.messages))
|
|
1656
|
+
return lastTurnOnly.messages;
|
|
1640
1657
|
}
|
|
1641
|
-
|
|
1658
|
+
// Fail explicitly rather than silently deleting the original instruction.
|
|
1659
|
+
return undefined;
|
|
1642
1660
|
}
|
|
1643
1661
|
compactResidentHistory() {
|
|
1644
1662
|
this.maybeCompactResidentHistory();
|
|
1645
1663
|
}
|
|
1646
|
-
async maybeCompactWithLLM(additionalInputTokens = 0) {
|
|
1664
|
+
async *maybeCompactWithLLM(additionalInputTokens = 0, abortSignal) {
|
|
1647
1665
|
if (!this.providerId || !this.apiModel)
|
|
1648
1666
|
return;
|
|
1649
1667
|
if (this.messages.length === 0)
|
|
@@ -1654,24 +1672,67 @@ export class Agent {
|
|
|
1654
1672
|
const budget = getContextBudget(this.providerId, this.apiModel, this.messages, {
|
|
1655
1673
|
usageAnchorTokens: this.lastInputTokens ?? undefined,
|
|
1656
1674
|
tailMessages: tail,
|
|
1657
|
-
|
|
1675
|
+
// Usage anchors already include the schemas sent with that request.
|
|
1676
|
+
additionalInputTokens: this.lastInputTokens === null ? additionalInputTokens : 0,
|
|
1658
1677
|
});
|
|
1659
1678
|
if (!budget.shouldCompact)
|
|
1660
1679
|
return;
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1680
|
+
yield { type: "context_compaction", status: "started", preTokens: budget.estimatedTokens, contextWindow: budget.contextWindow };
|
|
1681
|
+
let completed = false;
|
|
1682
|
+
const revision = this.getContextRevision?.();
|
|
1683
|
+
try {
|
|
1684
|
+
const { compactWithLLM } = await import("./context/llm-compactor.js");
|
|
1685
|
+
let result = await compactWithLLM(this.messages, {
|
|
1686
|
+
provider: this.provider,
|
|
1687
|
+
providerId: this.providerId,
|
|
1688
|
+
modelId: this.apiModel,
|
|
1689
|
+
abortSignal,
|
|
1690
|
+
});
|
|
1691
|
+
throwIfAborted(abortSignal);
|
|
1692
|
+
let path = "llm";
|
|
1693
|
+
if (!result.compacted) {
|
|
1694
|
+
// The request projection no longer compacts, so this fallback must
|
|
1695
|
+
// escalate as far as the former budgeted passes did (down to one kept
|
|
1696
|
+
// turn / one kept tool group) or a two-turn text history has no way
|
|
1697
|
+
// under the window when the summarizer is unavailable. Take the first
|
|
1698
|
+
// pass that fits the budget, otherwise the smallest one that fired.
|
|
1699
|
+
const passes = [
|
|
1700
|
+
["heuristic", () => compactMessages(this.messages, { keepRecentTurns: 2 })],
|
|
1701
|
+
["heuristic", () => compactMessages(this.messages, { keepRecentTurns: 1 })],
|
|
1702
|
+
["subturn", () => compactCurrentTurnToolGroups(this.messages, { keepRecentGroups: 2 })],
|
|
1703
|
+
["subturn", () => compactCurrentTurnToolGroups(this.messages, { keepRecentGroups: 1 })],
|
|
1704
|
+
];
|
|
1705
|
+
let smallest = Infinity;
|
|
1706
|
+
for (const [passPath, pass] of passes) {
|
|
1707
|
+
const candidate = pass();
|
|
1708
|
+
if (!candidate.compacted || !candidate.messages)
|
|
1709
|
+
continue;
|
|
1710
|
+
const candidateBudget = getContextBudget(this.providerId, this.apiModel, candidate.messages, { additionalInputTokens });
|
|
1711
|
+
if (candidateBudget.estimatedTokens < smallest) {
|
|
1712
|
+
smallest = candidateBudget.estimatedTokens;
|
|
1713
|
+
result = candidate;
|
|
1714
|
+
path = passPath;
|
|
1715
|
+
}
|
|
1716
|
+
if (!candidateBudget.shouldCompact)
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const beforeEstimate = getContextBudget(this.providerId, this.apiModel, this.messages, { additionalInputTokens }).estimatedTokens;
|
|
1721
|
+
const candidateEstimate = result.messages
|
|
1722
|
+
? getContextBudget(this.providerId, this.apiModel, result.messages, { additionalInputTokens }).estimatedTokens : beforeEstimate;
|
|
1723
|
+
if (result.compacted && result.messages && candidateEstimate < beforeEstimate) {
|
|
1724
|
+
this.applyContextCheckpoint(result.messages, "auto", result.summary, revision, abortSignal);
|
|
1725
|
+
this.compactionStats[path] += 1;
|
|
1726
|
+
this.compactionStats.fired += 1;
|
|
1727
|
+
traceEvent("compaction_fired", { path });
|
|
1728
|
+
completed = true;
|
|
1729
|
+
const after = getContextBudget(this.providerId, this.apiModel, this.messages, { additionalInputTokens });
|
|
1730
|
+
yield { type: "context_compaction", status: "completed", compactionId: this.lastCompactionId, persisted: !!this.onContextCheckpoint, preTokens: budget.estimatedTokens, postTokens: after.estimatedTokens, contextWindow: budget.contextWindow };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
finally {
|
|
1734
|
+
if (!completed)
|
|
1735
|
+
yield { type: "context_compaction", status: "failed", preTokens: budget.estimatedTokens, contextWindow: budget.contextWindow };
|
|
1675
1736
|
}
|
|
1676
1737
|
// If LLM compaction failed for any reason, leave this.messages alone —
|
|
1677
1738
|
// the projector's algorithmic budgeted-mode passes will still try.
|
|
@@ -1895,13 +1956,15 @@ export class Agent {
|
|
|
1895
1956
|
return [{ role: "system", content: childSystemPrompt }, ...forked];
|
|
1896
1957
|
}
|
|
1897
1958
|
maybeCompactResidentHistory() {
|
|
1898
|
-
if (this.messages.length === 0) {
|
|
1959
|
+
if (this.messages.length === 0 || !hasCompleteToolGroups(this.messages)) {
|
|
1899
1960
|
return;
|
|
1900
1961
|
}
|
|
1901
1962
|
const before = this.messages;
|
|
1902
1963
|
const beforeChars = estimateResidentChars(before);
|
|
1903
1964
|
const beforeToolChars = estimateToolPayloadChars(before);
|
|
1904
|
-
|
|
1965
|
+
// Compact canonical history, not the provider projection: projection
|
|
1966
|
+
// renders runtime meta as user text and can synthesize missing tool results.
|
|
1967
|
+
let candidate = pruneMessages(before);
|
|
1905
1968
|
const budget = this.providerId && this.apiModel
|
|
1906
1969
|
? getContextBudget(this.providerId, this.apiModel, candidate)
|
|
1907
1970
|
: undefined;
|
|
@@ -1916,7 +1979,9 @@ export class Agent {
|
|
|
1916
1979
|
// now waits for the context-window budget itself; the heap guard stays
|
|
1917
1980
|
// as process life-support, not policy.
|
|
1918
1981
|
const shouldAggressivelyPrune = heapUsed >= RESIDENT_HISTORY_HEAP_HARD_LIMIT;
|
|
1919
|
-
|
|
1982
|
+
// Model summarization runs at the request boundary. Resident compaction is
|
|
1983
|
+
// an emergency heap guard, not a competing token-triggered lossy policy.
|
|
1984
|
+
const shouldCompact = shouldAggressivelyPrune && !!budget?.shouldCompact;
|
|
1920
1985
|
if (shouldAggressivelyPrune) {
|
|
1921
1986
|
candidate = aggressivePruneMessages(candidate);
|
|
1922
1987
|
}
|
|
@@ -1937,6 +2002,7 @@ export class Agent {
|
|
|
1937
2002
|
if (subturn.compacted && subturn.messages) {
|
|
1938
2003
|
candidate = subturn.messages;
|
|
1939
2004
|
compactedPath = "subturn";
|
|
2005
|
+
residentSummary = subturn.summary;
|
|
1940
2006
|
traceEvent("compaction_fired", { path: "subturn", droppedEntries: subturn.droppedEntries });
|
|
1941
2007
|
}
|
|
1942
2008
|
}
|
|
@@ -1951,31 +2017,47 @@ export class Agent {
|
|
|
1951
2017
|
&& (afterChars < beforeChars
|
|
1952
2018
|
|| afterToolChars < beforeToolChars
|
|
1953
2019
|
|| candidate.length < before.length)) {
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2020
|
+
if (!compactedPath) {
|
|
2021
|
+
// Request-only thinning must not mutate durable history. Under heap
|
|
2022
|
+
// pressure below the token threshold, the pruned candidate still has to
|
|
2023
|
+
// replace resident memory or the life-support guard frees nothing; the
|
|
2024
|
+
// session log keeps the full tool output.
|
|
2025
|
+
if (shouldAggressivelyPrune) {
|
|
2026
|
+
this.messages = candidate;
|
|
2027
|
+
this.lastInputTokens = null;
|
|
2028
|
+
this.lastAnchorMessageCount = null;
|
|
2029
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
2030
|
+
}
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
this.applyContextCheckpoint(candidate, "resident", residentSummary, this.getContextRevision?.());
|
|
1958
2034
|
if (compactedPath === "resident") {
|
|
1959
2035
|
this.compactionStats.resident += 1;
|
|
1960
|
-
this.persistCompactionSummary(residentSummary);
|
|
1961
2036
|
}
|
|
1962
2037
|
if (compactedPath === "subturn")
|
|
1963
2038
|
this.compactionStats.subturn += 1;
|
|
2039
|
+
if (compactedPath && budget) {
|
|
2040
|
+
this.contextEvents.push({ type: "context_compaction", status: "completed", compactionId: this.lastCompactionId, persisted: !!this.onContextCheckpoint, preTokens: budget.estimatedTokens,
|
|
2041
|
+
postTokens: getContextBudget(this.providerId, this.apiModel, candidate).estimatedTokens, contextWindow: budget.contextWindow });
|
|
2042
|
+
}
|
|
1964
2043
|
}
|
|
1965
2044
|
}
|
|
1966
|
-
/**
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (!summary?.trim())
|
|
1972
|
-
return;
|
|
1973
|
-
try {
|
|
2045
|
+
/** Persist conversational context first; keep live reminders outside the durable projection. */
|
|
2046
|
+
applyContextCheckpoint(messages, reason, summary, baseRevision, abortSignal) {
|
|
2047
|
+
throwIfAborted(abortSignal);
|
|
2048
|
+
const checkpoint = createContextCheckpoint(messages, reason, summary, baseRevision);
|
|
2049
|
+
this.onContextCheckpoint?.(checkpoint);
|
|
2050
|
+
if (!this.onContextCheckpoint && summary?.trim())
|
|
1974
2051
|
this.onCompactionApplied?.(summary);
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2052
|
+
const { leading } = splitLeadingContext(messages);
|
|
2053
|
+
// The compactor may have folded away reminders from its candidate. Their
|
|
2054
|
+
// lifecycle belongs to the live Agent, not the durable summary/checkpoint.
|
|
2055
|
+
const runtimeReminders = splitLeadingContext(this.messages).body.filter(message => message.role === "meta" && message.includeInLlm !== false && !isCompactionSummaryMessage(message));
|
|
2056
|
+
this.messages = [...leading, ...checkpointMessages(checkpoint), ...runtimeReminders];
|
|
2057
|
+
this.lastCompactionId = checkpoint.compactionId;
|
|
2058
|
+
this.lastInputTokens = null;
|
|
2059
|
+
this.lastAnchorMessageCount = null;
|
|
2060
|
+
this.fileStateTracker?.invalidateReadHistory();
|
|
1979
2061
|
}
|
|
1980
2062
|
appendMessage(message) {
|
|
1981
2063
|
if (message.role === "assistant" && message.content) {
|
|
@@ -1985,7 +2067,7 @@ export class Agent {
|
|
|
1985
2067
|
message.reasoning = sanitizeInternalReasoningText(message.reasoning);
|
|
1986
2068
|
}
|
|
1987
2069
|
if (message.role === "assistant" && message.providerMetadata) {
|
|
1988
|
-
message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata);
|
|
2070
|
+
message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata, message.modelId ?? message.model);
|
|
1989
2071
|
}
|
|
1990
2072
|
this.messages.push(message);
|
|
1991
2073
|
traceEvent("agent_message_append", {
|
|
@@ -2007,6 +2089,7 @@ export class Agent {
|
|
|
2007
2089
|
thinkingLevel: this.thinkingLevel,
|
|
2008
2090
|
messageCount: request.messageCount,
|
|
2009
2091
|
toolCount: request.toolCount,
|
|
2092
|
+
retry: request.retry,
|
|
2010
2093
|
});
|
|
2011
2094
|
}
|
|
2012
2095
|
recordProviderError(error, context) {
|
|
@@ -2058,7 +2141,7 @@ export class Agent {
|
|
|
2058
2141
|
if (this._mode === "plan" && !tool.readOnly) {
|
|
2059
2142
|
return {
|
|
2060
2143
|
content: `Error: Tool "${toolCall.name}" is not allowed in plan mode. ` +
|
|
2061
|
-
`In plan mode you may only use read-only tools (read,
|
|
2144
|
+
`In plan mode you may only use read-only tools (read, ls, grep, web_search, web_fetch, spawn_agent, wait_agent, send_input, skill_search, skill, tool_search, question, exit_plan_mode). ` +
|
|
2062
2145
|
`To modify files or run commands, present your proposal and call exit_plan_mode so the user can review and approve it.`,
|
|
2063
2146
|
isError: true,
|
|
2064
2147
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Claude thinking bound to both the model and the exact preceding conversation.
|
|
2
|
+
* https://platform.claude.com/docs/en/build-with-claude/preserved-thinking
|
|
3
|
+
*/
|
|
4
|
+
export function isClaudeWithBoundThinking(model) {
|
|
5
|
+
const match = model?.match(/(?:^|:)claude-(opus|fable)-(\d+)(?:-(\d{1,2})(?=-|\[|$))?/i);
|
|
6
|
+
if (!match)
|
|
7
|
+
return false;
|
|
8
|
+
const major = Number(match[2]);
|
|
9
|
+
const minor = Number(match[3] ?? 0);
|
|
10
|
+
return major > 5 || (major === 5 && minor >= (match[1].toLowerCase() === "opus" ? 5 : 1));
|
|
11
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PermissionCheckResult, PermissionQuery, PermissionRuleSet } from "../permissions/types.js";
|
|
2
2
|
import type { PermissionMode } from "../types.js";
|
|
3
3
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type BashAllowlist } from "./session-cache.js";
|
|
5
5
|
import type { ApprovalController, ApprovalDecision, ApprovalRequest } from "./types.js";
|
|
6
6
|
export interface ApprovalControllerOptions {
|
|
7
7
|
/** Reads the live agent mode on each request so mode flips take effect immediately. */
|
|
@@ -16,6 +16,11 @@ export interface ApprovalControllerOptions {
|
|
|
16
16
|
};
|
|
17
17
|
/** Session-scoped bash command prefix allowlist. Optional. */
|
|
18
18
|
bashAllowlist?: BashAllowlist;
|
|
19
|
+
/**
|
|
20
|
+
* Session-scoped grants for non-bash requests (MCP tools), keyed by
|
|
21
|
+
* sessionGrantKey. Optional; without it hosts cannot offer "this session".
|
|
22
|
+
*/
|
|
23
|
+
sessionGrants?: Set<string>;
|
|
19
24
|
/** Working directory — used to anchor relative path rules. */
|
|
20
25
|
cwd: string;
|
|
21
26
|
/**
|
|
@@ -34,11 +39,13 @@ export interface ApprovalControllerOptions {
|
|
|
34
39
|
* deny rule match → reject (applies even under bypassPermissions)
|
|
35
40
|
* bypassPermissions → auto-approve, no prompt
|
|
36
41
|
* default + edit|write → auto-approve (workspace paths only; a
|
|
37
|
-
* path outside the workspace
|
|
38
|
-
*
|
|
42
|
+
* path outside the workspace, or a protected
|
|
43
|
+
* one — .git, agent permission settings —
|
|
44
|
+
* falls through to the interactive prompt)
|
|
39
45
|
* plan → reject with instructions to use exit_plan_mode
|
|
40
|
-
* allow rule match → auto-approve
|
|
41
|
-
*
|
|
46
|
+
* allow rule match → auto-approve (never for protected paths)
|
|
47
|
+
* session grant (bash prefix,
|
|
48
|
+
* MCP tool) → auto-approve
|
|
42
49
|
* bash / other → delegate to UI; if no UI, reject
|
|
43
50
|
*
|
|
44
51
|
* Deny rules sit at the top as a hard ceiling: bypassPermissions is a trust
|
|
@@ -49,7 +56,14 @@ export declare class PermissionAwareApprovalController implements ApprovalContro
|
|
|
49
56
|
private readonly options;
|
|
50
57
|
constructor(options: ApprovalControllerOptions);
|
|
51
58
|
checkRules(query: PermissionQuery): PermissionCheckResult;
|
|
52
|
-
request(
|
|
59
|
+
request(original: ApprovalRequest): Promise<ApprovalDecision>;
|
|
60
|
+
/**
|
|
61
|
+
* Facts the host needs to render the prompt correctly: whether a path is
|
|
62
|
+
* protected (never auto-approve it host-side either) and what scope a
|
|
63
|
+
* "this session" approval would cover.
|
|
64
|
+
*/
|
|
65
|
+
private annotate;
|
|
66
|
+
private rememberForSession;
|
|
53
67
|
private requestToQuery;
|
|
54
68
|
private checkRequestRules;
|
|
55
69
|
private runPermissionRequestHook;
|