@pentoshi/clai 3.8.20 → 3.8.21
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 +8 -5
- package/dist/agent/context-breakdown.d.ts +37 -0
- package/dist/agent/context-breakdown.js +145 -0
- package/dist/agent/context-breakdown.js.map +1 -0
- package/dist/agent/reliability-policy.d.ts +85 -0
- package/dist/agent/reliability-policy.js +158 -0
- package/dist/agent/reliability-policy.js.map +1 -0
- package/dist/agent/runner.js +235 -136
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-state.d.ts +11 -1
- package/dist/agent/session-state.js +22 -15
- package/dist/agent/session-state.js.map +1 -1
- package/dist/agent/task-evidence.js +13 -0
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/tool-history.d.ts +12 -0
- package/dist/agent/tool-history.js +159 -0
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/agent/tool-output-formatting.d.ts +4 -0
- package/dist/agent/tool-output-formatting.js +13 -4
- package/dist/agent/tool-output-formatting.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +6 -0
- package/dist/app/controllers/session-controller.js +21 -1
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/commands/providers.js +57 -26
- package/dist/commands/providers.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/llm/gemini.js +4 -1
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/key-rotation.d.ts +52 -0
- package/dist/llm/key-rotation.js +122 -0
- package/dist/llm/key-rotation.js.map +1 -0
- package/dist/llm/provider.d.ts +2 -0
- package/dist/llm/provider.js +8 -0
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/router.d.ts +15 -2
- package/dist/llm/router.js +318 -125
- package/dist/llm/router.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/index.d.ts +25 -0
- package/dist/prompts/index.js +55 -7
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +1 -1
- package/dist/repl/slash-commands.js +14 -8
- package/dist/repl/slash-commands.js.map +1 -1
- package/dist/repl.d.ts +4 -1
- package/dist/repl.js +12 -2
- package/dist/repl.js.map +1 -1
- package/dist/store/config.d.ts +18 -0
- package/dist/store/config.js +9 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +8 -0
- package/dist/store/history.js +38 -0
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.d.ts +55 -0
- package/dist/store/keys.js +226 -21
- package/dist/store/keys.js.map +1 -1
- package/dist/store/logs.js +35 -12
- package/dist/store/logs.js.map +1 -1
- package/dist/store/paths.d.ts +13 -0
- package/dist/store/paths.js +23 -1
- package/dist/store/paths.js.map +1 -1
- package/dist/store/session-workspace.d.ts +79 -0
- package/dist/store/session-workspace.js +199 -0
- package/dist/store/session-workspace.js.map +1 -0
- package/dist/tools/definitions.js +9 -1
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/http.d.ts +8 -0
- package/dist/tools/http.js +86 -2
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/pentest-workflows.js +38 -19
- package/dist/tools/pentest-workflows.js.map +1 -1
- package/dist/tools/registry.js +25 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +1 -1
- package/dist/tui/format-keys.js +29 -6
- package/dist/tui/format-keys.js.map +1 -1
- package/dist/tui-v2/app/command-handlers.js +1 -2
- package/dist/tui-v2/app/command-handlers.js.map +1 -1
- package/dist/tui-v2/app/commands/config-commands.d.ts +1 -2
- package/dist/tui-v2/app/commands/config-commands.js +1 -11
- package/dist/tui-v2/app/commands/config-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/key-commands.d.ts +1 -1
- package/dist/tui-v2/app/commands/key-commands.js +148 -63
- package/dist/tui-v2/app/commands/key-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +6 -0
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/bootstrap/composition-root.js +8 -5
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
- package/dist/tui-v2/components/modal/keys-modal.d.ts +19 -0
- package/dist/tui-v2/components/modal/keys-modal.js +203 -0
- package/dist/tui-v2/components/modal/keys-modal.js.map +1 -0
- package/dist/tui-v2/components/overlay/overlay-host.js +6 -2
- package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
- package/dist/tui-v2/components/pager/pager-line.js +5 -2
- package/dist/tui-v2/components/pager/pager-line.js.map +1 -1
- package/dist/tui-v2/components/pager/pager.js +6 -3
- package/dist/tui-v2/components/pager/pager.js.map +1 -1
- package/dist/tui-v2/components/plan/use-pane-presence.d.ts +6 -4
- package/dist/tui-v2/components/plan/use-pane-presence.js +8 -45
- package/dist/tui-v2/components/plan/use-pane-presence.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +4 -1
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/file-diff-card.js +3 -23
- package/dist/tui-v2/components/transcript/file-diff-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/linkable-text.js +9 -2
- package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -1
- package/dist/tui-v2/components/transcript/selectable-line.d.ts +22 -0
- package/dist/tui-v2/components/transcript/selectable-line.js +24 -0
- package/dist/tui-v2/components/transcript/selectable-line.js.map +1 -0
- package/dist/tui-v2/components/transcript/thinking-block.js +3 -1
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/tool-card.d.ts +5 -3
- package/dist/tui-v2/components/transcript/tool-card.js +39 -33
- package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/use-click-without-drag.js +4 -1
- package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -1
- package/dist/tui-v2/components/transcript/user-message.js +1 -1
- package/dist/tui-v2/components/transcript/user-message.js.map +1 -1
- package/dist/tui-v2/controllers/overlay-controller.d.ts +36 -0
- package/dist/tui-v2/controllers/overlay-controller.js +28 -0
- package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
- package/dist/tui-v2/rendering/batch-sections.d.ts +15 -4
- package/dist/tui-v2/rendering/batch-sections.js +157 -64
- package/dist/tui-v2/rendering/batch-sections.js.map +1 -1
- package/dist/tui-v2/rendering/format-help.js +1 -1
- package/dist/tui-v2/rendering/format-help.js.map +1 -1
- package/dist/tui-v2/rendering/open-tool-output.js +10 -7
- package/dist/tui-v2/rendering/open-tool-output.js.map +1 -1
- package/dist/tui-v2/rendering/pager-markdown.d.ts +9 -0
- package/dist/tui-v2/rendering/pager-markdown.js +46 -0
- package/dist/tui-v2/rendering/pager-markdown.js.map +1 -1
- package/dist/tui-v2/rendering/pager-view-policy.d.ts +6 -3
- package/dist/tui-v2/rendering/pager-view-policy.js +9 -13
- package/dist/tui-v2/rendering/pager-view-policy.js.map +1 -1
- package/dist/tui-v2/rendering/sanitize-display.js +3 -1
- package/dist/tui-v2/rendering/sanitize-display.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
package/dist/agent/runner.js
CHANGED
|
@@ -8,13 +8,16 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { jobManager } from "../tools/jobs.js";
|
|
9
9
|
import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, toolNudge, } from "../prompts/index.js";
|
|
10
10
|
import { getConfig } from "../store/config.js";
|
|
11
|
+
import { beginSessionWorkspace, getActiveSessionWorkspace, } from "../store/session-workspace.js";
|
|
11
12
|
import { groqInputTokenBudget } from "../llm/groq.js";
|
|
12
13
|
import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
|
|
13
14
|
import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
|
|
14
15
|
import { getToolDefinitions, getCompactToolDefinitions, PLAN_TOOL_NAMES, } from "../tools/definitions.js";
|
|
15
|
-
import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, } from "./tool-history.js";
|
|
16
|
+
import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
|
|
16
17
|
import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
|
|
17
|
-
import { compactMessagesWithSummary, estimateMessagesTokens,
|
|
18
|
+
import { compactMessagesWithSummary, estimateMessagesTokens, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
|
|
19
|
+
import { buildContextBreakdown, contextBreakdownAuditPayload, } from "./context-breakdown.js";
|
|
20
|
+
import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
|
|
18
21
|
import { auditLog } from "../store/logs.js";
|
|
19
22
|
import { loadProjectContext } from "../store/project.js";
|
|
20
23
|
import { loadScope, isScopeActive } from "../store/scope.js";
|
|
@@ -85,9 +88,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
85
88
|
if (/\/output\b|open full output|Ctrl\+O or|\.clai\/outputs/i.test(cleaned)) {
|
|
86
89
|
return;
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// API key rotation / retry lines need more room than tool-name chips.
|
|
92
|
+
const keyLine = /^(using |switching |⏳ |all .+ API keys)/i.test(cleaned);
|
|
93
|
+
const maxLen = keyLine ? 96 : 64;
|
|
94
|
+
if (cleaned.length > maxLen) {
|
|
95
|
+
if (keyLine) {
|
|
96
|
+
cleaned = cleaned.slice(0, maxLen - 1) + "…";
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const short = cleaned.match(/^[\w./-]+/);
|
|
100
|
+
cleaned = short ? short[0] : cleaned.slice(0, maxLen - 3) + "…";
|
|
101
|
+
}
|
|
91
102
|
}
|
|
92
103
|
emit({ type: "status", text: cleaned || "working" });
|
|
93
104
|
if (writesDirectly)
|
|
@@ -278,6 +289,11 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
278
289
|
};
|
|
279
290
|
let lastAnswer = "";
|
|
280
291
|
const session = options.session ?? createSessionPolicy();
|
|
292
|
+
// One-shot CLI / tests that never entered TUI/REPL still need an isolated
|
|
293
|
+
// scratch+output workspace. No-op when a session already bound one.
|
|
294
|
+
if (!getActiveSessionWorkspace()) {
|
|
295
|
+
beginSessionWorkspace();
|
|
296
|
+
}
|
|
281
297
|
// Active plan context
|
|
282
298
|
// If this session already has a plan, inject it so the model keeps it in
|
|
283
299
|
// context. When the user has approved it (via /implement) we instruct the
|
|
@@ -323,11 +339,16 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
323
339
|
setActiveProjectRootIfValid(discoveredProjects[0]);
|
|
324
340
|
}
|
|
325
341
|
const buildSystemContent = (native) => {
|
|
342
|
+
const reliability = getReliabilityPolicy();
|
|
326
343
|
const sections = [
|
|
327
344
|
(useCompactSystemPrompt
|
|
328
345
|
? renderCompactAgentSystemPrompt
|
|
329
346
|
: renderAgentSystemPrompt)(toolNames.join(", "), {
|
|
330
347
|
nativeTools: native,
|
|
348
|
+
// E6: slim native constitution when API tool schemas are attached.
|
|
349
|
+
...(native
|
|
350
|
+
? { slimNative: reliability.slimNativePrompt }
|
|
351
|
+
: {}),
|
|
331
352
|
}),
|
|
332
353
|
];
|
|
333
354
|
if (projectContext) {
|
|
@@ -1911,6 +1932,11 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1911
1932
|
// Align with /compact default: small recency + dense memory (not keepRecent=6 fat tails).
|
|
1912
1933
|
const AUTO_COMPACT_KEEP_RECENT = 2;
|
|
1913
1934
|
let lastCompactionMsgCount = 0;
|
|
1935
|
+
/** E5: identical tool bodies within this turn → pointer instead of re-append. */
|
|
1936
|
+
const toolResultHashes = new Map();
|
|
1937
|
+
/** E4: consecutive free-tier stream failures this turn. */
|
|
1938
|
+
let freeTierConsecutiveFailures = 0;
|
|
1939
|
+
let freeTierLargeContextWarned = false;
|
|
1914
1940
|
const summarizeForCompaction = async (summaryPrompt) => {
|
|
1915
1941
|
const response = await completeWithProvider({
|
|
1916
1942
|
provider,
|
|
@@ -1928,7 +1954,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1928
1954
|
};
|
|
1929
1955
|
async function maybeAutoCompact(reason, force = false) {
|
|
1930
1956
|
const beforeTokens = estimateMessagesTokens(messages);
|
|
1931
|
-
|
|
1957
|
+
// E1: soft early compact (default 70k) while hard ceiling remains 100k.
|
|
1958
|
+
const compactTrigger = autoCompactTriggerTokens();
|
|
1959
|
+
if (!force && beforeTokens < compactTrigger)
|
|
1932
1960
|
return;
|
|
1933
1961
|
if (messages.length <= AUTO_COMPACT_KEEP_RECENT + 2)
|
|
1934
1962
|
return;
|
|
@@ -2057,159 +2085,222 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2057
2085
|
({ dialect: toolDialect, native: nativeToolsActive } =
|
|
2058
2086
|
resolveNativeTools(provider, model));
|
|
2059
2087
|
if (messages[0]?.role === "system") {
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2088
|
+
// Recompose only when content actually changes (hour-stable env clock
|
|
2089
|
+
// keeps the constitution prefix identical across steps, which helps
|
|
2090
|
+
// provider prompt caching and avoids needless object churn).
|
|
2091
|
+
const nextSystem = composeCurrentSystemPrompt(nativeToolsActive);
|
|
2092
|
+
if (messages[0].content !== nextSystem) {
|
|
2093
|
+
messages[0] = {
|
|
2094
|
+
role: "system",
|
|
2095
|
+
content: nextSystem,
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2064
2098
|
}
|
|
2065
2099
|
const turnTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
|
|
2066
2100
|
toolsAttached = Boolean(turnTools?.length);
|
|
2101
|
+
const contextBreakdown = buildContextBreakdown(messages, toolsAttached ? turnTools : undefined);
|
|
2102
|
+
// E4: advisory only — never blocks free-tier users.
|
|
2103
|
+
if (!freeTierLargeContextWarned) {
|
|
2104
|
+
const notices = freeTierGuardNotices({
|
|
2105
|
+
provider,
|
|
2106
|
+
estimatedInputTokens: contextBreakdown.estimatedTotalTokens,
|
|
2107
|
+
consecutiveFailures: freeTierConsecutiveFailures,
|
|
2108
|
+
});
|
|
2109
|
+
for (const notice of notices) {
|
|
2110
|
+
if (notice.includes("Large context")) {
|
|
2111
|
+
freeTierLargeContextWarned = true;
|
|
2112
|
+
}
|
|
2113
|
+
writeNotice("info", notice, chalk.dim(` ℹ ${notice}\n`));
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2067
2116
|
await auditLog("agent.turn", {
|
|
2068
2117
|
provider,
|
|
2069
2118
|
model,
|
|
2070
2119
|
tool_protocol: toolsAttached ? "native" : "text",
|
|
2071
2120
|
dialect: toolDialect,
|
|
2072
2121
|
step,
|
|
2122
|
+
// Metadata-only composition metrics (no prompt/tool text).
|
|
2123
|
+
...contextBreakdownAuditPayload(contextBreakdown),
|
|
2124
|
+
compactTriggerTokens: autoCompactTriggerTokens(),
|
|
2125
|
+
maxTokensBudget: resolveStepMaxTokens({
|
|
2126
|
+
nativeToolsActive,
|
|
2127
|
+
toolsAttached,
|
|
2128
|
+
recoveryNudge: retryWithoutThinking,
|
|
2129
|
+
}),
|
|
2073
2130
|
});
|
|
2131
|
+
// Resume / mid-turn abort can leave orphan tool rows or a user
|
|
2132
|
+
// "continue" before tool results. Heal first so multi-key retry and
|
|
2133
|
+
// history reloads don't hard-fail on protocol asserts.
|
|
2134
|
+
// Heal once per step if needed; silent (no toast) — placeholders are
|
|
2135
|
+
// ok=true so the model doesn't thrash on fake exit=130 failures.
|
|
2136
|
+
repairToolProtocol(messages);
|
|
2074
2137
|
assertValidToolProtocol(messages);
|
|
2075
|
-
completion
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
:
|
|
2086
|
-
|
|
2087
|
-
?
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2138
|
+
// E3: adaptive completion budget (still large enough for writes).
|
|
2139
|
+
const stepMaxTokens = resolveStepMaxTokens({
|
|
2140
|
+
nativeToolsActive,
|
|
2141
|
+
toolsAttached,
|
|
2142
|
+
recoveryNudge: retryWithoutThinking,
|
|
2143
|
+
});
|
|
2144
|
+
try {
|
|
2145
|
+
completion = await streamWithProvider({
|
|
2146
|
+
provider,
|
|
2147
|
+
model,
|
|
2148
|
+
allowModelFallback: false,
|
|
2149
|
+
messages,
|
|
2150
|
+
temperature: /minimax-m3/i.test(model) ? 1.0 : 0.2,
|
|
2151
|
+
maxTokens: stepMaxTokens,
|
|
2152
|
+
signal: options.signal,
|
|
2153
|
+
thinking: retryWithoutThinking
|
|
2154
|
+
? { ...config.thinking, enabled: false, effort: "low" }
|
|
2155
|
+
: config.thinking,
|
|
2156
|
+
...(toolsAttached
|
|
2157
|
+
? {
|
|
2158
|
+
tools: turnTools,
|
|
2159
|
+
toolChoice: freshWebSearchRequired && !sawFreshWebSearch
|
|
2160
|
+
? { type: "function", name: "web.search" }
|
|
2161
|
+
: "auto",
|
|
2162
|
+
parallelToolCalls: true,
|
|
2163
|
+
// P2-3: emit tool cards as soon as the function name arrives.
|
|
2164
|
+
onToolCallDelta: (delta) => {
|
|
2165
|
+
if (!delta.name)
|
|
2166
|
+
return;
|
|
2167
|
+
const name = fromWireName(delta.name) ?? delta.name;
|
|
2168
|
+
const existing = deferredToolCalls[delta.index];
|
|
2169
|
+
if (existing) {
|
|
2170
|
+
if (delta.argumentsBytes &&
|
|
2171
|
+
delta.argumentsBytes >= 4096 &&
|
|
2172
|
+
!writesDirectly) {
|
|
2173
|
+
emit({
|
|
2174
|
+
type: "status",
|
|
2175
|
+
text: `${name} (${Math.round(delta.argumentsBytes / 1024)}KB args)`,
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
// Ensure slots are dense so index maps to deferredToolCalls[i].
|
|
2181
|
+
while (deferredToolCalls.length < delta.index) {
|
|
2182
|
+
deferredToolCalls.push({
|
|
2183
|
+
eventId: `tool-${++nextToolEventId}`,
|
|
2184
|
+
call: { name: "…", args: {} },
|
|
2185
|
+
rendered: "",
|
|
2106
2186
|
});
|
|
2107
2187
|
}
|
|
2108
|
-
|
|
2188
|
+
const call = normalizeToolCall({
|
|
2189
|
+
name,
|
|
2190
|
+
args: {},
|
|
2191
|
+
});
|
|
2192
|
+
const eventId = `tool-${++nextToolEventId}`;
|
|
2193
|
+
callIds.push(eventId);
|
|
2194
|
+
alreadyPrintedIds.add(eventId);
|
|
2195
|
+
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
2196
|
+
chalk.gray(` ${formatToolArgs(call)}`);
|
|
2197
|
+
const entry = {
|
|
2198
|
+
eventId,
|
|
2199
|
+
call,
|
|
2200
|
+
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
2201
|
+
};
|
|
2202
|
+
if (deferredToolCalls.length === delta.index) {
|
|
2203
|
+
deferredToolCalls.push(entry);
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
deferredToolCalls[delta.index] = entry;
|
|
2207
|
+
}
|
|
2208
|
+
streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
|
|
2209
|
+
if (!writesDirectly) {
|
|
2210
|
+
emit({ type: "status", text: call.name });
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
spinner.stop();
|
|
2214
|
+
spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
}
|
|
2218
|
+
: {}),
|
|
2219
|
+
}, (token) => {
|
|
2220
|
+
deltaParser?.push(token);
|
|
2221
|
+
generatedTokens += 1;
|
|
2222
|
+
accumulatedText += token;
|
|
2223
|
+
// Early UI cards from text fences only when native tools are off
|
|
2224
|
+
// (native args stream as structured deltas, not prose).
|
|
2225
|
+
if (!toolsAttached) {
|
|
2226
|
+
const parsedCalls = parseAllToolCalls(accumulatedText);
|
|
2227
|
+
if (parsedCalls.length > streamedCallsCount) {
|
|
2228
|
+
if (writesDirectly) {
|
|
2229
|
+
spinner.stop();
|
|
2109
2230
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2231
|
+
while (streamedCallsCount < parsedCalls.length) {
|
|
2232
|
+
const call = parsedCalls[streamedCallsCount];
|
|
2233
|
+
const eventId = `tool-${++nextToolEventId}`;
|
|
2234
|
+
callIds.push(eventId);
|
|
2235
|
+
alreadyPrintedIds.add(eventId);
|
|
2236
|
+
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
2237
|
+
chalk.gray(` ${formatToolArgs(call)}`);
|
|
2112
2238
|
deferredToolCalls.push({
|
|
2113
|
-
eventId
|
|
2114
|
-
call
|
|
2115
|
-
rendered: "",
|
|
2239
|
+
eventId,
|
|
2240
|
+
call,
|
|
2241
|
+
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
2116
2242
|
});
|
|
2243
|
+
if (!writesDirectly) {
|
|
2244
|
+
emit({ type: "status", text: call.name });
|
|
2245
|
+
}
|
|
2246
|
+
streamedCallsCount += 1;
|
|
2117
2247
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
args: {},
|
|
2121
|
-
});
|
|
2122
|
-
const eventId = `tool-${++nextToolEventId}`;
|
|
2123
|
-
callIds.push(eventId);
|
|
2124
|
-
alreadyPrintedIds.add(eventId);
|
|
2125
|
-
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
2126
|
-
chalk.gray(` ${formatToolArgs(call)}`);
|
|
2127
|
-
const entry = {
|
|
2128
|
-
eventId,
|
|
2129
|
-
call,
|
|
2130
|
-
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
2131
|
-
};
|
|
2132
|
-
if (deferredToolCalls.length === delta.index) {
|
|
2133
|
-
deferredToolCalls.push(entry);
|
|
2134
|
-
}
|
|
2135
|
-
else {
|
|
2136
|
-
deferredToolCalls[delta.index] = entry;
|
|
2137
|
-
}
|
|
2138
|
-
streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
|
|
2139
|
-
if (!writesDirectly) {
|
|
2140
|
-
emit({ type: "status", text: call.name });
|
|
2141
|
-
}
|
|
2142
|
-
else {
|
|
2143
|
-
spinner.stop();
|
|
2144
|
-
spinner = startThinkingSpinner(`tool ${call.name}…`, options.signal);
|
|
2145
|
-
}
|
|
2146
|
-
},
|
|
2147
|
-
}
|
|
2148
|
-
: {}),
|
|
2149
|
-
}, (token) => {
|
|
2150
|
-
deltaParser?.push(token);
|
|
2151
|
-
generatedTokens += 1;
|
|
2152
|
-
accumulatedText += token;
|
|
2153
|
-
// Early UI cards from text fences only when native tools are off
|
|
2154
|
-
// (native args stream as structured deltas, not prose).
|
|
2155
|
-
if (!toolsAttached) {
|
|
2156
|
-
const parsedCalls = parseAllToolCalls(accumulatedText);
|
|
2157
|
-
if (parsedCalls.length > streamedCallsCount) {
|
|
2158
|
-
if (writesDirectly) {
|
|
2159
|
-
spinner.stop();
|
|
2160
|
-
}
|
|
2161
|
-
while (streamedCallsCount < parsedCalls.length) {
|
|
2162
|
-
const call = parsedCalls[streamedCallsCount];
|
|
2163
|
-
const eventId = `tool-${++nextToolEventId}`;
|
|
2164
|
-
callIds.push(eventId);
|
|
2165
|
-
alreadyPrintedIds.add(eventId);
|
|
2166
|
-
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
2167
|
-
chalk.gray(` ${formatToolArgs(call)}`);
|
|
2168
|
-
deferredToolCalls.push({
|
|
2169
|
-
eventId,
|
|
2170
|
-
call,
|
|
2171
|
-
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
2172
|
-
});
|
|
2173
|
-
if (!writesDirectly) {
|
|
2174
|
-
emit({ type: "status", text: call.name });
|
|
2248
|
+
if (writesDirectly) {
|
|
2249
|
+
spinner = startThinkingSpinner(`generating response (${generatedTokens} tokens)`, options.signal);
|
|
2175
2250
|
}
|
|
2176
|
-
streamedCallsCount += 1;
|
|
2177
2251
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2252
|
+
}
|
|
2253
|
+
if (!sawReasoning && /<think/i.test(token)) {
|
|
2254
|
+
sawReasoning = true;
|
|
2255
|
+
inThinking = true;
|
|
2256
|
+
spinner.setLabel("thinking");
|
|
2257
|
+
if (!writesDirectly)
|
|
2258
|
+
emit({ type: "status", text: "thinking" });
|
|
2259
|
+
}
|
|
2260
|
+
if (/<\/think>/i.test(token)) {
|
|
2261
|
+
inThinking = false;
|
|
2262
|
+
spinner.setLabel("generating response (0 tokens)");
|
|
2263
|
+
generatedTokens = 0;
|
|
2264
|
+
}
|
|
2265
|
+
if (inThinking) {
|
|
2266
|
+
const cleaned = token.replace(/<\/?think[^>]*>/gi, "");
|
|
2267
|
+
if (cleaned) {
|
|
2268
|
+
spinner.pushPreview(cleaned);
|
|
2269
|
+
const approx = cleaned.split(/\s+/).filter(Boolean).length;
|
|
2270
|
+
if (approx > 0)
|
|
2271
|
+
spinner.bumpReasoning(approx);
|
|
2180
2272
|
}
|
|
2181
2273
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
spinner.setLabel("thinking");
|
|
2187
|
-
if (!writesDirectly)
|
|
2188
|
-
emit({ type: "status", text: "thinking" });
|
|
2189
|
-
}
|
|
2190
|
-
if (/<\/think>/i.test(token)) {
|
|
2191
|
-
inThinking = false;
|
|
2192
|
-
spinner.setLabel("generating response (0 tokens)");
|
|
2193
|
-
generatedTokens = 0;
|
|
2194
|
-
}
|
|
2195
|
-
if (inThinking) {
|
|
2196
|
-
const cleaned = token.replace(/<\/?think[^>]*>/gi, "");
|
|
2197
|
-
if (cleaned) {
|
|
2198
|
-
spinner.pushPreview(cleaned);
|
|
2199
|
-
const approx = cleaned.split(/\s+/).filter(Boolean).length;
|
|
2200
|
-
if (approx > 0)
|
|
2201
|
-
spinner.bumpReasoning(approx);
|
|
2274
|
+
else {
|
|
2275
|
+
if (generatedTokens % 10 === 0) {
|
|
2276
|
+
spinner.setLabel(`generating response (${generatedTokens} tokens)`);
|
|
2277
|
+
}
|
|
2202
2278
|
}
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2279
|
+
}, (status) => {
|
|
2280
|
+
spinner.stop();
|
|
2281
|
+
writeStatus(status, chalk.dim(status));
|
|
2282
|
+
// Toast only on key *switch* after a failure — never on sticky
|
|
2283
|
+
// "using" or retry countdown ticks (those stay in composer status).
|
|
2284
|
+
if (/^switching /i.test(status.trim())) {
|
|
2285
|
+
writeNotice("warn", status.trim(), chalk.yellow(` ${status.trim()}\n`));
|
|
2207
2286
|
}
|
|
2287
|
+
});
|
|
2288
|
+
freeTierConsecutiveFailures = 0;
|
|
2289
|
+
}
|
|
2290
|
+
catch (streamError) {
|
|
2291
|
+
// E4: track free-tier failures for advisory notices (never blocks).
|
|
2292
|
+
freeTierConsecutiveFailures += 1;
|
|
2293
|
+
for (const notice of freeTierGuardNotices({
|
|
2294
|
+
provider,
|
|
2295
|
+
estimatedInputTokens: contextBreakdown.estimatedTotalTokens,
|
|
2296
|
+
consecutiveFailures: freeTierConsecutiveFailures,
|
|
2297
|
+
})) {
|
|
2298
|
+
if (notice.includes("Large context"))
|
|
2299
|
+
continue; // already shown above
|
|
2300
|
+
writeNotice("warn", notice, chalk.yellow(` ⚠ ${notice}\n`));
|
|
2208
2301
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
writeStatus(status, chalk.dim(status));
|
|
2212
|
-
});
|
|
2302
|
+
throw streamError;
|
|
2303
|
+
}
|
|
2213
2304
|
}
|
|
2214
2305
|
finally {
|
|
2215
2306
|
// Always clear the spinner — abort, network error, or success.
|
|
@@ -2977,9 +3068,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2977
3068
|
planCreatedThisTurn = true;
|
|
2978
3069
|
}
|
|
2979
3070
|
productiveSteps += 1;
|
|
3071
|
+
// E5: collapse identical large tool bodies within this turn to a pointer.
|
|
3072
|
+
const deduped = dedupeToolContextOutput({
|
|
3073
|
+
content: res.contextOutput,
|
|
3074
|
+
toolName: res.call.name,
|
|
3075
|
+
artifactPath: res.result.outputPath,
|
|
3076
|
+
seenHashes: toolResultHashes,
|
|
3077
|
+
});
|
|
3078
|
+
const contextForHistory = deduped.content;
|
|
2980
3079
|
// Soft plan-mode note on tool payloads only (never a user message).
|
|
2981
3080
|
// Stop once a plan with tasks exists so we don't nag after plan.create.
|
|
2982
|
-
let toolContent = `Tool ${res.call.name} result (exit=${res.result.exitCode ?? 0}, ok=${res.result.ok}):\n${
|
|
3081
|
+
let toolContent = `Tool ${res.call.name} result (exit=${res.result.exitCode ?? 0}, ok=${res.result.ok}):\n${contextForHistory}`;
|
|
2983
3082
|
const reminded = maybeAppendPlanModeReminder(toolContent, {
|
|
2984
3083
|
isPlanMode,
|
|
2985
3084
|
planApproved: session.planApproved.value,
|