mixdog 0.9.41 → 0.9.44
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/package.json +3 -2
- package/scripts/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/ansi-color-capability-test.mjs +90 -0
- package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +256 -1
- package/scripts/generate-runtime-manifest.mjs +39 -22
- package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +38 -39
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/maintenance-default-routes-test.mjs +164 -0
- package/scripts/max-output-recovery-test.mjs +49 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
- package/scripts/provider-toolcall-test.mjs +23 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/routing-corpus.mjs +1 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
- package/scripts/tui-transcript-jitter-harness.mjs +58 -0
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +5 -3
- package/src/rules/lead/lead-brief.md +5 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -14
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
- package/src/runtime/memory/data/runtime-manifest.json +11 -11
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +267 -72
- package/src/standalone/explore-tool.mjs +17 -16
- package/src/tui/app/provider-setup-picker.mjs +1 -0
- package/src/tui/app/use-transcript-window.mjs +5 -1
- package/src/tui/components/StatusLine.jsx +11 -32
- package/src/tui/dist/index.mjs +257 -106
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/tui/index.jsx +3 -2
- package/src/tui/markdown/streaming-markdown.mjs +35 -9
- package/src/tui/statusline-ansi-bridge.mjs +17 -7
- package/src/ui/ansi.mjs +85 -5
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline-format.mjs +7 -7
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +29 -20
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -60
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isOffloadedToolResultText } from './tool-result-offload.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
4
|
+
import { contentImageDescriptors, contentToText } from '../providers/media-normalization.mjs';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Conservative, Unicode-aware token estimator.
|
|
@@ -26,6 +27,8 @@ function readSafetyMultiplier() {
|
|
|
26
27
|
return 1.1;
|
|
27
28
|
}
|
|
28
29
|
const TOKEN_ESTIMATE_SAFETY_MULTIPLIER = readSafetyMultiplier();
|
|
30
|
+
export const IMAGE_VISUAL_TOKEN_ALLOWANCE = 4_096;
|
|
31
|
+
const IMAGE_TILE_TOKEN_ALLOWANCE = 512;
|
|
29
32
|
|
|
30
33
|
// Per-code-point token-cost weight. Tuned to overcount, not match exactly.
|
|
31
34
|
function codePointTokenWeight(cp) {
|
|
@@ -72,12 +75,59 @@ export function estimateTokens(text) {
|
|
|
72
75
|
if (s.length === 0) return 0;
|
|
73
76
|
let weighted = 0;
|
|
74
77
|
for (const ch of s) weighted += codePointTokenWeight(ch.codePointAt(0));
|
|
78
|
+
// Encoded blobs, minified JSON and generated identifiers do not get the
|
|
79
|
+
// word/whitespace merges that make prose approach chars/4. Long printable
|
|
80
|
+
// ASCII runs are commonly 0.5-0.8 tokens/byte; retain a conservative floor
|
|
81
|
+
// for those runs without penalizing ordinary spaced prose.
|
|
82
|
+
let denseAsciiFloor = 0;
|
|
83
|
+
for (const match of s.matchAll(/[\x21-\x7e]{16,}/g)) {
|
|
84
|
+
denseAsciiFloor += match[0].length * 0.75;
|
|
85
|
+
}
|
|
86
|
+
const encodedWords = s.match(/\b(?=[A-Za-z0-9]{8,}\b)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]+\b/g) || [];
|
|
87
|
+
if (encodedWords.length >= 3) {
|
|
88
|
+
// Encoded/generated identifiers are often wrapped at short columns or
|
|
89
|
+
// separated by spaces. Their individual runs can stay below the long-
|
|
90
|
+
// run threshold while still receiving almost no prose-style BPE merges.
|
|
91
|
+
const encodedChars = encodedWords.reduce((sum, word) => sum + word.length, 0);
|
|
92
|
+
denseAsciiFloor = Math.max(
|
|
93
|
+
denseAsciiFloor,
|
|
94
|
+
(encodedChars * 0.75) + ((s.length - encodedChars) * 0.25),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const lines = s.split(/\r?\n/).filter(line => line.trim());
|
|
98
|
+
const nonWhitespace = s.match(/\S/g)?.length || 0;
|
|
99
|
+
const structural = s.match(/[\[\]{}":,=<>|\\]/g)?.length || 0;
|
|
100
|
+
const jsonLikeLines = lines.filter(line => /^\s*[\[{].*[\]}],?\s*$/.test(line)).length;
|
|
101
|
+
if (lines.length >= 3 && nonWhitespace > 0
|
|
102
|
+
&& (jsonLikeLines >= Math.ceil(lines.length / 2) || structural / nonWhitespace >= 0.12)) {
|
|
103
|
+
// JSONL, compact tables and generated line protocols can consist
|
|
104
|
+
// entirely of short runs while still tokenizing like minified data.
|
|
105
|
+
denseAsciiFloor = Math.max(denseAsciiFloor, (nonWhitespace * 0.7) + ((s.length - nonWhitespace) * 0.25));
|
|
106
|
+
}
|
|
75
107
|
const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
|
|
76
|
-
return Math.ceil(Math.max(weighted, asciiFloor) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
108
|
+
return Math.ceil(Math.max(weighted, asciiFloor, denseAsciiFloor) * TOKEN_ESTIMATE_SAFETY_MULTIPLIER);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function nativeBlocksEstimateText(value) {
|
|
112
|
+
const list = Array.isArray(value) ? value : [value];
|
|
113
|
+
return list.map((block) => {
|
|
114
|
+
const images = contentImageDescriptors(block);
|
|
115
|
+
if (images.length) {
|
|
116
|
+
return JSON.stringify(images.map(({ width, height, detail }) => ({
|
|
117
|
+
type: 'image', width, height, detail,
|
|
118
|
+
})));
|
|
119
|
+
}
|
|
120
|
+
try { return JSON.stringify(block); }
|
|
121
|
+
catch { return String(block ?? ''); }
|
|
122
|
+
}).join('\n');
|
|
77
123
|
}
|
|
78
124
|
export function messageEstimateText(m) {
|
|
79
125
|
if (!m || typeof m !== 'object') return '';
|
|
80
|
-
|
|
126
|
+
// Multimodal image payloads remain on the live message for provider sends,
|
|
127
|
+
// but their base64/data-url JSON is not text and must not dominate local
|
|
128
|
+
// context estimates. Use the same media-aware text projection for every
|
|
129
|
+
// estimate consumer (live gauge, compaction fallback, and summaries).
|
|
130
|
+
let text = contentToText(m.content, '');
|
|
81
131
|
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
|
|
82
132
|
try { text += `\n${JSON.stringify(m.toolCalls)}`; }
|
|
83
133
|
catch { text += `\n[${m.toolCalls.length} tool calls]`; }
|
|
@@ -86,14 +136,47 @@ export function messageEstimateText(m) {
|
|
|
86
136
|
// signature / redacted data) and are re-sent on tool-continuation turns, so
|
|
87
137
|
// they consume real input tokens. Count them or trim/compact undercounts.
|
|
88
138
|
if (m.role === 'assistant' && Array.isArray(m.thinkingBlocks) && m.thinkingBlocks.length) {
|
|
89
|
-
|
|
90
|
-
|
|
139
|
+
text += `\n${nativeBlocksEstimateText(m.thinkingBlocks)}`;
|
|
140
|
+
}
|
|
141
|
+
// Some provider adapters replay their native assistant representation
|
|
142
|
+
// instead of content/toolCalls. Project it through the media normalizer so
|
|
143
|
+
// text/tool metadata and opaque reasoning are counted without base64 image
|
|
144
|
+
// bytes dominating the estimate.
|
|
145
|
+
if (m.role === 'assistant' && Array.isArray(m.assistantBlocks) && m.assistantBlocks.length) {
|
|
146
|
+
text += `\n${nativeBlocksEstimateText(m.assistantBlocks)}`;
|
|
147
|
+
}
|
|
148
|
+
if (m.role === 'assistant' && Array.isArray(m.reasoningItems) && m.reasoningItems.length) {
|
|
149
|
+
text += `\n${nativeBlocksEstimateText(m.reasoningItems)}`;
|
|
91
150
|
}
|
|
92
151
|
if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
|
|
93
152
|
return text;
|
|
94
153
|
}
|
|
154
|
+
function imageDescriptorAllowance(descriptor) {
|
|
155
|
+
if (descriptor.width && descriptor.height) {
|
|
156
|
+
const tiles = Math.ceil(descriptor.width / 512) * Math.ceil(descriptor.height / 512);
|
|
157
|
+
// Caller-supplied dimensions/detail are not uniformly preserved by all
|
|
158
|
+
// provider normalizers, so they may raise the allowance for a known
|
|
159
|
+
// multi-tile image but can never lower the conservative unknown-image
|
|
160
|
+
// fallback. This avoids trusting metadata the provider never sees.
|
|
161
|
+
return Math.max(IMAGE_VISUAL_TOKEN_ALLOWANCE, IMAGE_TILE_TOKEN_ALLOWANCE * (tiles + 1));
|
|
162
|
+
}
|
|
163
|
+
// Unknown-size auto/high images may be tiled by the provider. A single
|
|
164
|
+
// 1k allowance is optimistic for common screenshots and documents.
|
|
165
|
+
return IMAGE_VISUAL_TOKEN_ALLOWANCE;
|
|
166
|
+
}
|
|
167
|
+
function messageImageDescriptors(m) {
|
|
168
|
+
if (!m || typeof m !== 'object') return [];
|
|
169
|
+
return [
|
|
170
|
+
...contentImageDescriptors(m.content),
|
|
171
|
+
...(m.role === 'assistant' ? contentImageDescriptors(m.assistantBlocks) : []),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
function messageImageAllowance(m) {
|
|
175
|
+
if (!m || typeof m !== 'object') return 0;
|
|
176
|
+
return messageImageDescriptors(m).reduce((sum, descriptor) => sum + imageDescriptorAllowance(descriptor), 0);
|
|
177
|
+
}
|
|
95
178
|
export function estimateMessageTokens(m) {
|
|
96
|
-
return estimateTokens(messageEstimateText(m)) + 4;
|
|
179
|
+
return estimateTokens(messageEstimateText(m)) + messageImageAllowance(m) + 4;
|
|
97
180
|
}
|
|
98
181
|
export function estimateMessagesTokens(messages) {
|
|
99
182
|
return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
@@ -112,28 +195,11 @@ const contextMessageMemo = new WeakMap();
|
|
|
112
195
|
const contextTranscriptMemo = new WeakMap();
|
|
113
196
|
|
|
114
197
|
function contextValueFingerprint(value) {
|
|
115
|
-
if (typeof value === 'string') return { value, entries: null };
|
|
116
|
-
if (Array.isArray(value)) return {
|
|
117
|
-
value,
|
|
118
|
-
entries: value.map(contextBlockFingerprint),
|
|
119
|
-
};
|
|
120
|
-
return value && typeof value === 'object'
|
|
121
|
-
? { value, entries: [contextBlockFingerprint(value)] }
|
|
122
|
-
: { value, entries: null };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function contextBlockFingerprint(block) {
|
|
126
|
-
if (!block || typeof block !== 'object') return { value: block };
|
|
127
|
-
const fn = block.function;
|
|
128
198
|
return {
|
|
129
|
-
value
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
arguments: block.arguments,
|
|
134
|
-
input: typeof block.input === 'string' ? block.input : null,
|
|
135
|
-
function: fn,
|
|
136
|
-
functionArguments: typeof fn?.arguments === 'string' ? fn.arguments : null,
|
|
199
|
+
value,
|
|
200
|
+
snapshot: typeof value === 'string'
|
|
201
|
+
? value
|
|
202
|
+
: `${nativeBlocksEstimateText(value)}\0${JSON.stringify(contentImageDescriptors(value))}`,
|
|
137
203
|
};
|
|
138
204
|
}
|
|
139
205
|
|
|
@@ -145,6 +211,7 @@ function contextMessageFingerprint(message) {
|
|
|
145
211
|
toolCalls: contextValueFingerprint(null),
|
|
146
212
|
thinkingBlocks: contextValueFingerprint(null),
|
|
147
213
|
assistantBlocks: contextValueFingerprint(null),
|
|
214
|
+
reasoningItems: contextValueFingerprint(null),
|
|
148
215
|
toolCallId: null,
|
|
149
216
|
};
|
|
150
217
|
}
|
|
@@ -154,30 +221,13 @@ function contextMessageFingerprint(message) {
|
|
|
154
221
|
toolCalls: contextValueFingerprint(message.toolCalls),
|
|
155
222
|
thinkingBlocks: contextValueFingerprint(message.thinkingBlocks),
|
|
156
223
|
assistantBlocks: contextValueFingerprint(message.assistantBlocks),
|
|
224
|
+
reasoningItems: contextValueFingerprint(message.reasoningItems),
|
|
157
225
|
toolCallId: message?.toolCallId || null,
|
|
158
226
|
};
|
|
159
227
|
}
|
|
160
228
|
|
|
161
229
|
function sameContextValueFingerprint(a, b) {
|
|
162
|
-
|
|
163
|
-
if (a.entries === null || b.entries === null) return a.entries === b.entries;
|
|
164
|
-
if (a.entries.length !== b.entries.length) return false;
|
|
165
|
-
for (let index = 0; index < a.entries.length; index += 1) {
|
|
166
|
-
if (!sameContextBlockFingerprint(a.entries[index], b.entries[index])) return false;
|
|
167
|
-
}
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function sameContextBlockFingerprint(a, b) {
|
|
172
|
-
return !!a && !!b
|
|
173
|
-
&& a.value === b.value
|
|
174
|
-
&& a.text === b.text
|
|
175
|
-
&& a.content === b.content
|
|
176
|
-
&& a.args === b.args
|
|
177
|
-
&& Object.is(a.arguments, b.arguments)
|
|
178
|
-
&& a.input === b.input
|
|
179
|
-
&& a.function === b.function
|
|
180
|
-
&& a.functionArguments === b.functionArguments;
|
|
230
|
+
return !!a && !!b && a.value === b.value && a.snapshot === b.snapshot;
|
|
181
231
|
}
|
|
182
232
|
|
|
183
233
|
function sameContextMessageFingerprint(a, b) {
|
|
@@ -186,6 +236,7 @@ function sameContextMessageFingerprint(a, b) {
|
|
|
186
236
|
&& sameContextValueFingerprint(a.toolCalls, b.toolCalls)
|
|
187
237
|
&& sameContextValueFingerprint(a.thinkingBlocks, b.thinkingBlocks)
|
|
188
238
|
&& sameContextValueFingerprint(a.assistantBlocks, b.assistantBlocks)
|
|
239
|
+
&& sameContextValueFingerprint(a.reasoningItems, b.reasoningItems)
|
|
189
240
|
&& a.toolCallId === b.toolCallId;
|
|
190
241
|
}
|
|
191
242
|
|
|
@@ -197,7 +248,7 @@ function contextMessageContribution(message) {
|
|
|
197
248
|
}
|
|
198
249
|
const role = ['system', 'user', 'assistant', 'tool'].includes(fingerprint.role) ? fingerprint.role : 'other';
|
|
199
250
|
const text = messageEstimateText(message);
|
|
200
|
-
const tokens = estimateTokens(text) + 4;
|
|
251
|
+
const tokens = estimateTokens(text) + messageImageAllowance(message) + 4;
|
|
201
252
|
const contribution = {
|
|
202
253
|
role,
|
|
203
254
|
tokens,
|
|
@@ -511,7 +562,7 @@ export function summarizeContextMessages(messages) {
|
|
|
511
562
|
if (!Array.isArray(messages)) return contextSummaryResult(emptyContextSummaryState(), 0);
|
|
512
563
|
let cached = contextTranscriptMemo.get(messages);
|
|
513
564
|
if (!cached || messages.length < cached.count) {
|
|
514
|
-
cached = { count: 0, contributions: [], state: emptyContextSummaryState() };
|
|
565
|
+
cached = { count: 0, contributions: [], state: emptyContextSummaryState(), revision: 0 };
|
|
515
566
|
contextTranscriptMemo.set(messages, cached);
|
|
516
567
|
}
|
|
517
568
|
for (let index = 0; index < messages.length; index += 1) {
|
|
@@ -521,12 +572,42 @@ export function summarizeContextMessages(messages) {
|
|
|
521
572
|
if (previous) applyContextMessageContribution(cached.state, previous, -1);
|
|
522
573
|
cached.contributions[index] = contribution;
|
|
523
574
|
applyContextMessageContribution(cached.state, contribution, 1);
|
|
575
|
+
cached.revision += 1;
|
|
524
576
|
}
|
|
525
577
|
cached.contributions.length = messages.length;
|
|
526
578
|
cached.count = messages.length;
|
|
527
579
|
return contextSummaryResult(cached.state, messages.length);
|
|
528
580
|
}
|
|
529
581
|
|
|
582
|
+
// A stable warm-cache generation for consumers that cache a derived view of
|
|
583
|
+
// the whole transcript. summarizeContextMessages() must run first so mutations
|
|
584
|
+
// to any entry, not merely the tail, advance the generation.
|
|
585
|
+
export function contextMessagesRevision(messages) {
|
|
586
|
+
if (!Array.isArray(messages)) return 0;
|
|
587
|
+
summarizeContextMessages(messages);
|
|
588
|
+
return contextTranscriptMemo.get(messages)?.revision || 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Hash only estimator/provider-visible projections. In particular, images
|
|
592
|
+
// contribute their visual count but never their raw data-url/base64 bytes.
|
|
593
|
+
export function contextMessagesSignature(messages, count = messages?.length) {
|
|
594
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
595
|
+
const end = Math.max(0, Math.min(list.length, Number.isInteger(count) ? count : list.length));
|
|
596
|
+
const hash = createHash('sha256');
|
|
597
|
+
for (let index = 0; index < end; index += 1) {
|
|
598
|
+
const message = list[index];
|
|
599
|
+
hash.update(JSON.stringify([
|
|
600
|
+
message?.role || '',
|
|
601
|
+
message?.toolCallId || '',
|
|
602
|
+
messageEstimateText(message),
|
|
603
|
+
messageImageAllowance(message),
|
|
604
|
+
messageImageDescriptors(message),
|
|
605
|
+
]));
|
|
606
|
+
hash.update('\0');
|
|
607
|
+
}
|
|
608
|
+
return hash.digest('hex');
|
|
609
|
+
}
|
|
610
|
+
|
|
530
611
|
// Per-request overhead the provider injects that never appears in the
|
|
531
612
|
// `messages` array: function-calling preamble + system-prompt framing the
|
|
532
613
|
// provider wraps around the request. The chars/4 message estimate misses all
|
|
@@ -535,35 +616,13 @@ const REQUEST_OVERHEAD_TOKENS = 512;
|
|
|
535
616
|
const toolSchemaTokenMemo = new WeakMap();
|
|
536
617
|
const requestReserveTokenMemo = new WeakMap();
|
|
537
618
|
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const entry = cached.entries[index];
|
|
542
|
-
const tool = tools[index];
|
|
543
|
-
if (entry.tool !== tool
|
|
544
|
-
|| entry.name !== tool?.name
|
|
545
|
-
|| entry.description !== tool?.description
|
|
546
|
-
|| entry.inputSchema !== tool?.inputSchema
|
|
547
|
-
|| entry.input_schema !== tool?.input_schema
|
|
548
|
-
|| entry.parameters !== tool?.parameters
|
|
549
|
-
|| entry.schema !== tool?.schema) return false;
|
|
550
|
-
}
|
|
551
|
-
return true;
|
|
619
|
+
function serializeToolSchemas(tools) {
|
|
620
|
+
try { return JSON.stringify(Array.isArray(tools) ? tools : []); }
|
|
621
|
+
catch { return (Array.isArray(tools) ? tools : []).map(t => String(t?.name ?? '')).join(''); }
|
|
552
622
|
}
|
|
553
623
|
|
|
554
|
-
function
|
|
555
|
-
return
|
|
556
|
-
entries: tools.map((tool) => ({
|
|
557
|
-
tool,
|
|
558
|
-
name: tool?.name,
|
|
559
|
-
description: tool?.description,
|
|
560
|
-
inputSchema: tool?.inputSchema,
|
|
561
|
-
input_schema: tool?.input_schema,
|
|
562
|
-
parameters: tool?.parameters,
|
|
563
|
-
schema: tool?.schema,
|
|
564
|
-
})),
|
|
565
|
-
value,
|
|
566
|
-
};
|
|
624
|
+
export function toolSchemaSignature(tools) {
|
|
625
|
+
return createHash('sha256').update(serializeToolSchemas(tools)).digest('hex');
|
|
567
626
|
}
|
|
568
627
|
|
|
569
628
|
/**
|
|
@@ -576,13 +635,12 @@ function cacheToolArrayValue(tools, value) {
|
|
|
576
635
|
*/
|
|
577
636
|
export function estimateToolSchemaTokens(tools) {
|
|
578
637
|
if (!Array.isArray(tools) || tools.length === 0) return 0;
|
|
638
|
+
const signature = toolSchemaSignature(tools);
|
|
579
639
|
const cached = toolSchemaTokenMemo.get(tools);
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
try { text = JSON.stringify(tools); }
|
|
583
|
-
catch { text = tools.map(t => String(t?.name ?? '')).join(''); }
|
|
640
|
+
if (cached?.signature === signature) return cached.value;
|
|
641
|
+
const text = serializeToolSchemas(tools);
|
|
584
642
|
const tokens = estimateTokens(text);
|
|
585
|
-
toolSchemaTokenMemo.set(tools,
|
|
643
|
+
toolSchemaTokenMemo.set(tools, { signature, value: tokens });
|
|
586
644
|
return tokens;
|
|
587
645
|
}
|
|
588
646
|
|
|
@@ -594,10 +652,11 @@ export function estimateToolSchemaTokens(tools) {
|
|
|
594
652
|
*/
|
|
595
653
|
export function estimateRequestReserveTokens(tools) {
|
|
596
654
|
if (!Array.isArray(tools)) return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
655
|
+
const signature = toolSchemaSignature(tools);
|
|
597
656
|
const cached = requestReserveTokenMemo.get(tools);
|
|
598
|
-
if (
|
|
657
|
+
if (cached?.signature === signature) return cached.value;
|
|
599
658
|
const reserve = estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
600
|
-
requestReserveTokenMemo.set(tools,
|
|
659
|
+
requestReserveTokenMemo.set(tools, { signature, value: reserve });
|
|
601
660
|
return reserve;
|
|
602
661
|
}
|
|
603
662
|
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// runRecallFastTrackCompact stays in the loop (it drives the recall pipeline
|
|
4
4
|
// against live session state).
|
|
5
5
|
import {
|
|
6
|
+
contextMessagesSignature,
|
|
6
7
|
estimateMessagesTokens,
|
|
7
8
|
estimateRequestReserveTokens,
|
|
8
9
|
resolveSessionCompactPolicy,
|
|
10
|
+
toolSchemaSignature,
|
|
9
11
|
} from '../context-utils.mjs';
|
|
10
12
|
import {
|
|
11
13
|
compactTypeIsRecallFastTrack,
|
|
@@ -132,6 +134,7 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
132
134
|
reserveTokens: requestReserve + configuredReserve,
|
|
133
135
|
requestReserveTokens: requestReserve,
|
|
134
136
|
configuredReserveTokens: configuredReserve,
|
|
137
|
+
toolSchemaSignature: toolSchemaSignature(tools),
|
|
135
138
|
};
|
|
136
139
|
}
|
|
137
140
|
/** Transcript + request reserve fallback used until an aligned provider baseline exists. */
|
|
@@ -159,13 +162,20 @@ function providerPressureTokens(sessionRef, usage) {
|
|
|
159
162
|
* covers. Later pressure checks add estimates only for messages after this
|
|
160
163
|
* baseline, matching Claude Code's actual-usage-plus-growth accounting.
|
|
161
164
|
*/
|
|
162
|
-
export function recordProviderContextBaseline(sessionRef, messages, usage, {
|
|
165
|
+
export function recordProviderContextBaseline(sessionRef, messages, usage, {
|
|
166
|
+
boundary = 'complete',
|
|
167
|
+
sendTools = sessionRef?.tools,
|
|
168
|
+
} = {}) {
|
|
163
169
|
if (!sessionRef || !Array.isArray(messages)) return false;
|
|
164
170
|
const tokens = providerPressureTokens(sessionRef, usage);
|
|
165
171
|
if (!tokens) return false;
|
|
166
172
|
sessionRef.contextPressureBaselineTokens = tokens;
|
|
167
173
|
sessionRef.contextPressureBaselineOutputTokens = Math.max(0, Math.round(Number(usage?.outputTokens) || 0));
|
|
168
174
|
sessionRef.contextPressureBaselineMessageCount = messages.length;
|
|
175
|
+
sessionRef.contextPressureBaselinePrefixSignature = contextMessagesSignature(messages);
|
|
176
|
+
sessionRef.contextPressureBaselineProvider = sessionRef.provider || null;
|
|
177
|
+
sessionRef.contextPressureBaselineModel = sessionRef.model || null;
|
|
178
|
+
sessionRef.contextPressureBaselineToolSignature = toolSchemaSignature(sendTools);
|
|
169
179
|
// provider_send usage arrives before the response's assistant message is
|
|
170
180
|
// appended. Mark that request boundary so pressure resolution skips the
|
|
171
181
|
// first subsequent assistant representation: its output (including opaque
|
|
@@ -183,11 +193,15 @@ export function invalidateProviderContextBaseline(sessionRef) {
|
|
|
183
193
|
sessionRef.contextPressureBaselineOutputTokens = null;
|
|
184
194
|
sessionRef.contextPressureBaselineMessageCount = null;
|
|
185
195
|
sessionRef.contextPressureBaselineBoundary = null;
|
|
196
|
+
sessionRef.contextPressureBaselinePrefixSignature = null;
|
|
197
|
+
sessionRef.contextPressureBaselineProvider = null;
|
|
198
|
+
sessionRef.contextPressureBaselineModel = null;
|
|
199
|
+
sessionRef.contextPressureBaselineToolSignature = null;
|
|
186
200
|
sessionRef.contextPressureBaselineUpdatedAt = null;
|
|
187
201
|
sessionRef.lastContextTokensStaleAfterCompact = true;
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
function providerBaselinePressureTokens(messages, sessionRef) {
|
|
204
|
+
function providerBaselinePressureTokens(messages, sessionRef, policy) {
|
|
191
205
|
if (!Array.isArray(messages) || !sessionRef
|
|
192
206
|
|| sessionRef.lastContextTokensStaleAfterCompact === true) return null;
|
|
193
207
|
let tokens = positiveTokenInt(sessionRef.contextPressureBaselineTokens);
|
|
@@ -196,7 +210,11 @@ function providerBaselinePressureTokens(messages, sessionRef) {
|
|
|
196
210
|
const baselineAt = Number(sessionRef.contextPressureBaselineUpdatedAt || 0);
|
|
197
211
|
const compactAt = Number(sessionRef.compaction?.lastChangedAt || sessionRef.compaction?.lastCompactAt || 0);
|
|
198
212
|
if (!tokens || !Number.isInteger(count) || count < 0 || count > messages.length
|
|
199
|
-
|| (compactAt > 0 && baselineAt > 0 && baselineAt < compactAt)
|
|
213
|
+
|| (compactAt > 0 && baselineAt > 0 && baselineAt < compactAt)
|
|
214
|
+
|| sessionRef.contextPressureBaselineProvider !== (sessionRef.provider || null)
|
|
215
|
+
|| sessionRef.contextPressureBaselineModel !== (sessionRef.model || null)
|
|
216
|
+
|| sessionRef.contextPressureBaselineToolSignature !== policy?.toolSchemaSignature
|
|
217
|
+
|| sessionRef.contextPressureBaselinePrefixSignature !== contextMessagesSignature(messages, count)) return null;
|
|
200
218
|
if (sessionRef.contextPressureBaselineBoundary === 'request') {
|
|
201
219
|
const assistantOffset = messages.slice(count).findIndex(message => message?.role === 'assistant');
|
|
202
220
|
if (assistantOffset >= 0) {
|
|
@@ -213,14 +231,14 @@ function providerBaselinePressureTokens(messages, sessionRef) {
|
|
|
213
231
|
const growth = count < messages.length
|
|
214
232
|
? estimateMessagesTokens(messages.slice(count))
|
|
215
233
|
: 0;
|
|
216
|
-
return Math.max(0, tokens + growth);
|
|
234
|
+
return Math.max(0, tokens + growth + Math.max(0, Number(policy?.configuredReserveTokens) || 0));
|
|
217
235
|
} catch {
|
|
218
236
|
return null;
|
|
219
237
|
}
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
export function resolveCompactionPressureTokens(messageTokensEst, policy, { messages, sessionRef } = {}) {
|
|
223
|
-
return providerBaselinePressureTokens(messages, sessionRef)
|
|
241
|
+
return providerBaselinePressureTokens(messages, sessionRef, policy)
|
|
224
242
|
?? compactPressureTokens(messageTokensEst, policy);
|
|
225
243
|
}
|
|
226
244
|
|
|
@@ -43,6 +43,9 @@ export function classifyTerminationReason(response, {
|
|
|
43
43
|
// on its own contract.
|
|
44
44
|
return 'iteration_cap';
|
|
45
45
|
}
|
|
46
|
+
if (_finalStopReason === 'refusal') {
|
|
47
|
+
return 'refusal';
|
|
48
|
+
}
|
|
46
49
|
if (_finalOutputLimitStop || (!_finalHasContent && _finalIncompleteStop)) {
|
|
47
50
|
// Exhausted token-cap recovery is abnormal even with preserved partial
|
|
48
51
|
// text. pause_turn/OTHER retain their prior non-empty completion
|
|
@@ -223,21 +223,30 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
223
223
|
})();
|
|
224
224
|
if (typeof afterToolHook === 'function') {
|
|
225
225
|
try {
|
|
226
|
+
// Tool outcome metadata is runtime-internal. Hooks receive the same
|
|
227
|
+
// model-visible result value they received before transient
|
|
228
|
+
// envelopes existed, never the envelope object itself.
|
|
229
|
+
const {
|
|
230
|
+
result: __res,
|
|
231
|
+
newMessages: __nm,
|
|
232
|
+
explicitSuccess: __explicitSuccess,
|
|
233
|
+
} = normalizeToolEnvelope(__result);
|
|
226
234
|
const hookResult = await afterToolHook({
|
|
227
235
|
name,
|
|
228
236
|
args,
|
|
229
237
|
cwd,
|
|
230
238
|
sessionId: callerSessionId,
|
|
231
239
|
toolCallId: executeOpts.toolCallId || null,
|
|
232
|
-
result:
|
|
240
|
+
result: __res,
|
|
233
241
|
});
|
|
234
242
|
// Envelope-aware hook override: a PostToolUse hook may override the
|
|
235
243
|
// model-VISIBLE tool output (the envelope's `result` / stub), but it
|
|
236
244
|
// must NEVER drop the `newMessages` channel. Split first, apply the
|
|
237
245
|
// override to `result` only, then re-wrap so newMessages survive.
|
|
238
|
-
const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
|
|
239
246
|
const __overridden = resolveToolResultAfterHook(__res, hookResult);
|
|
240
|
-
if (__nm.length
|
|
247
|
+
if (__nm.length || __explicitSuccess) {
|
|
248
|
+
return makeToolEnvelope(__overridden, __nm, { explicitSuccess: __explicitSuccess });
|
|
249
|
+
}
|
|
241
250
|
return __overridden;
|
|
242
251
|
} catch {
|
|
243
252
|
// PostToolUse hooks are best-effort; never let one break the tool result.
|
|
@@ -341,7 +341,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
341
341
|
promptTokens: d.deltaPrompt,
|
|
342
342
|
cachedTokens: d.deltaCachedRead,
|
|
343
343
|
cacheWriteTokens: d.deltaCacheWrite,
|
|
344
|
-
}, { boundary: 'request' });
|
|
344
|
+
}, { boundary: 'request', sendTools: d.sendTools });
|
|
345
345
|
}
|
|
346
346
|
try { askOpts?.onUsageDelta?.(d); } catch {}
|
|
347
347
|
},
|
|
@@ -482,7 +482,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
482
482
|
applyAskTerminalUsageTotals(session, result, {
|
|
483
483
|
skipTotalsIfIncremental: runtime?.usageMetricsTurnIncremental === true,
|
|
484
484
|
});
|
|
485
|
-
recordProviderContextBaseline(session, session.messages, result.lastTurnUsage || result.usage
|
|
485
|
+
recordProviderContextBaseline(session, session.messages, result.lastTurnUsage || result.usage, {
|
|
486
|
+
sendTools: result.lastSendTools,
|
|
487
|
+
});
|
|
486
488
|
// Agent Runtime cache stats — record hit/miss after every successful
|
|
487
489
|
// ask so the registry reflects all agent traffic, not just
|
|
488
490
|
// maintenance cycles. Guarded against any agent-runtime error so
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
* "(no lines in range" — read offset out-of-range (builtin.mjs:739, 3571)
|
|
41
41
|
*
|
|
42
42
|
* @param {unknown} result
|
|
43
|
+
* @param {boolean} [explicitSuccess=false] true only when the tool handler
|
|
44
|
+
* explicitly returned `isError: false`
|
|
43
45
|
* @returns {'normal' | 'error' | 'zero-match'}
|
|
44
46
|
*/
|
|
45
47
|
const ZERO_MATCH_PREFIXES = [
|
|
@@ -56,7 +58,8 @@ const ZERO_MATCH_PREFIXES = [
|
|
|
56
58
|
'(no lines in range',
|
|
57
59
|
];
|
|
58
60
|
|
|
59
|
-
export function classifyResultKind(result) {
|
|
61
|
+
export function classifyResultKind(result, explicitSuccess = false) {
|
|
62
|
+
if (explicitSuccess === true) return 'normal';
|
|
60
63
|
if (typeof result !== 'string') return 'normal';
|
|
61
64
|
const trimmed = result.trimStart();
|
|
62
65
|
if (/^error(?:\s+\[code\b|\s*:)/i.test(trimmed) || /^\[error/i.test(trimmed) || /^\[exit code:/i.test(trimmed)) return 'error';
|
|
@@ -32,6 +32,11 @@ import { crossTurnSignature, crossTurnDedupStub, isEditProgressTool } from './lo
|
|
|
32
32
|
import { getToolKind, isEagerDispatchable, parseNativeToolSearchPayload } from './loop/tool-helpers.mjs';
|
|
33
33
|
import { restoreToolCallBodyForId, dropCompactedBodyArgsForId } from './loop/stored-tool-args.mjs';
|
|
34
34
|
|
|
35
|
+
function classifyToolReturn(value) {
|
|
36
|
+
const normalized = normalizeToolEnvelope(value);
|
|
37
|
+
return classifyResultKind(normalized.result, normalized.explicitSuccess);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export async function processToolBatch(ctx) {
|
|
36
41
|
const {
|
|
37
42
|
calls, messages, tools, cwd, sessionId, sessionRef, signal, opts,
|
|
@@ -258,7 +263,7 @@ export async function processToolBatch(ctx) {
|
|
|
258
263
|
if (!settled.ok) throw settled.error;
|
|
259
264
|
result = settled.value;
|
|
260
265
|
toolEndedAt = eager.endedAt ?? Date.now();
|
|
261
|
-
const _eagerKind =
|
|
266
|
+
const _eagerKind = classifyToolReturn(result);
|
|
262
267
|
if (_eagerKind === 'error') {
|
|
263
268
|
_resultKind = 'error';
|
|
264
269
|
_executeOk = false;
|
|
@@ -286,7 +291,7 @@ export async function processToolBatch(ctx) {
|
|
|
286
291
|
// Boundary: tool-return string convention → structural kind.
|
|
287
292
|
// The only prefix check in this codebase; downstream layers
|
|
288
293
|
// operate on _resultKind.
|
|
289
|
-
if (
|
|
294
|
+
if (classifyToolReturn(result) === 'error') {
|
|
290
295
|
_resultKind = 'error';
|
|
291
296
|
_executeOk = false;
|
|
292
297
|
} else {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - legacy: a string (or existing structured media object) — unchanged, OR
|
|
6
6
|
* - an envelope object:
|
|
7
7
|
* { __toolEnvelope: true, result: <string|structured>,
|
|
8
|
-
* newMessages: [{ role:'user', content:'...' }, ...]
|
|
8
|
+
* newMessages: [{ role:'user', content:'...' }, ...],
|
|
9
|
+
* explicitSuccess?: true }
|
|
9
10
|
*
|
|
10
11
|
* The `__toolEnvelope` marker is deliberately namespaced so it can NEVER be
|
|
11
12
|
* confused with the existing structured media content objects that
|
|
@@ -37,17 +38,18 @@ function isValidNewMessage(m) {
|
|
|
37
38
|
* sees as the tool_result; `newMessages` are appended (as their own
|
|
38
39
|
* messages, e.g. role:'user') AFTER the batch's tool results.
|
|
39
40
|
*/
|
|
40
|
-
export function makeToolEnvelope(result, newMessages = []) {
|
|
41
|
+
export function makeToolEnvelope(result, newMessages = [], options = {}) {
|
|
41
42
|
return {
|
|
42
43
|
[TOOL_ENVELOPE_MARKER]: true,
|
|
43
44
|
result,
|
|
44
45
|
newMessages: Array.isArray(newMessages) ? newMessages.filter(isValidNewMessage) : [],
|
|
46
|
+
...(options.explicitSuccess === true ? { explicitSuccess: true } : {}),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
* Split a tool return value into `{ result, newMessages }`.
|
|
50
|
-
* - legacy string/object → { result: value, newMessages: [] }
|
|
51
|
+
* Split a tool return value into `{ result, newMessages, explicitSuccess }`.
|
|
52
|
+
* - legacy string/object → { result: value, newMessages: [], explicitSuccess: false }
|
|
51
53
|
* - envelope → { result, newMessages } (newMessages validated)
|
|
52
54
|
*/
|
|
53
55
|
export function normalizeToolEnvelope(value) {
|
|
@@ -55,7 +57,7 @@ export function normalizeToolEnvelope(value) {
|
|
|
55
57
|
const newMessages = Array.isArray(value.newMessages)
|
|
56
58
|
? value.newMessages.filter(isValidNewMessage)
|
|
57
59
|
: [];
|
|
58
|
-
return { result: value.result, newMessages };
|
|
60
|
+
return { result: value.result, newMessages, explicitSuccess: value.explicitSuccess === true };
|
|
59
61
|
}
|
|
60
|
-
return { result: value, newMessages: [] };
|
|
62
|
+
return { result: value, newMessages: [], explicitSuccess: false };
|
|
61
63
|
}
|
|
@@ -344,6 +344,15 @@ function stripTrailingPatternArtifacts(v) {
|
|
|
344
344
|
return out;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
function coercePatternStringValues(v) {
|
|
348
|
+
const coerce = (value) => (
|
|
349
|
+
(typeof value === 'number' && Number.isFinite(value)) || typeof value === 'boolean'
|
|
350
|
+
? String(value)
|
|
351
|
+
: value
|
|
352
|
+
);
|
|
353
|
+
return Array.isArray(v) ? v.map(coerce) : coerce(v);
|
|
354
|
+
}
|
|
355
|
+
|
|
347
356
|
// ---- per-tool guards ----
|
|
348
357
|
|
|
349
358
|
function guardGrep(a) {
|
|
@@ -355,9 +364,10 @@ function guardGrep(a) {
|
|
|
355
364
|
// Lossless cleanup of trailing artifacts before validation (item 5b).
|
|
356
365
|
for (const k of patternKeys) {
|
|
357
366
|
if (hasOwn(a, k)) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
367
|
+
const value = coercePatternStringValues(a[k]);
|
|
368
|
+
a[k] = Array.isArray(value)
|
|
369
|
+
? value.map(stripTrailingPatternArtifacts)
|
|
370
|
+
: stripTrailingPatternArtifacts(value);
|
|
361
371
|
}
|
|
362
372
|
}
|
|
363
373
|
|
|
@@ -376,6 +386,7 @@ function guardGrep(a) {
|
|
|
376
386
|
// rg's PCRE2 engine (-P/--pcre2) when the installed rg build supports it,
|
|
377
387
|
// falling back to this same error text only when PCRE2 is unavailable.
|
|
378
388
|
for (const k of globKeys) {
|
|
389
|
+
if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
|
|
379
390
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
380
391
|
return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
381
392
|
}
|
|
@@ -755,6 +766,7 @@ function guardGlob(a) {
|
|
|
755
766
|
}
|
|
756
767
|
}
|
|
757
768
|
for (const k of globPatternKeys) {
|
|
769
|
+
if (hasOwn(a, k)) a[k] = coercePatternStringValues(a[k]);
|
|
758
770
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
759
771
|
return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
760
772
|
}
|
|
@@ -773,7 +773,7 @@ export function foregroundLongCommandHint(command, timeoutMs, args = {}) {
|
|
|
773
773
|
const longSleep = /\bsleep\s+(?:[3-9]\d|\d{3,}|\d+[mh])\b/i.test(cmd) || longPowerShellSleep;
|
|
774
774
|
if (!watchLike && !longSleep) return '';
|
|
775
775
|
if (!longTimeout && !watchLike && !longSleep) return '';
|
|
776
|
-
return 'Error: long foreground command detected.';
|
|
776
|
+
return 'Error: long foreground command detected. Use mode:"async" (background task) or task wait instead of blocking sleeps/watch loops.';
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// Commands that must NOT be promoted to background on a foreground timeout —
|