maestro-agent-sdk 0.1.37 → 0.1.39
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 +55 -62
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +5 -43
- package/dist/core/loop.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/memory/aux-model-map.d.ts +2 -43
- package/dist/memory/aux-model-map.d.ts.map +1 -1
- package/dist/memory/aux-model-map.js +3 -85
- package/dist/memory/aux-model-map.js.map +1 -1
- package/dist/platform/config.d.ts +0 -7
- package/dist/platform/config.d.ts.map +1 -1
- package/dist/platform/config.js +0 -14
- package/dist/platform/config.js.map +1 -1
- package/dist/platform/version.d.ts +1 -1
- package/dist/platform/version.js +1 -1
- package/dist/provider.d.ts +12 -55
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +99 -156
- package/dist/provider.js.map +1 -1
- package/dist/registry.d.ts +6 -39
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +9 -76
- package/dist/registry.js.map +1 -1
- package/dist/sub-agent/runner.d.ts.map +1 -1
- package/dist/sub-agent/runner.js +0 -3
- package/dist/sub-agent/runner.js.map +1 -1
- package/dist/tools/builtin/bash.js +2 -2
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/tools/builtin/bash_background.js +1 -1
- package/dist/tools/builtin/bash_background.js.map +1 -1
- package/dist/tools/builtin/gemini_image_qa.d.ts +12 -0
- package/dist/tools/builtin/gemini_image_qa.d.ts.map +1 -0
- package/dist/tools/builtin/gemini_image_qa.js +195 -0
- package/dist/tools/builtin/gemini_image_qa.js.map +1 -0
- package/dist/tools/builtin/read.d.ts.map +1 -1
- package/dist/tools/builtin/read.js +6 -6
- package/dist/tools/builtin/read.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +2 -24
- package/dist/providers/anthropic.d.ts +0 -328
- package/dist/providers/anthropic.d.ts.map +0 -1
- package/dist/providers/anthropic.js +0 -857
- package/dist/providers/anthropic.js.map +0 -1
- package/dist/providers/codex-auth.d.ts +0 -149
- package/dist/providers/codex-auth.d.ts.map +0 -1
- package/dist/providers/codex-auth.js +0 -332
- package/dist/providers/codex-auth.js.map +0 -1
- package/dist/providers/codex-stream.d.ts +0 -42
- package/dist/providers/codex-stream.d.ts.map +0 -1
- package/dist/providers/codex-stream.js +0 -345
- package/dist/providers/codex-stream.js.map +0 -1
- package/dist/providers/codex-translators.d.ts +0 -105
- package/dist/providers/codex-translators.d.ts.map +0 -1
- package/dist/providers/codex-translators.js +0 -244
- package/dist/providers/codex-translators.js.map +0 -1
- package/dist/providers/codex.d.ts +0 -234
- package/dist/providers/codex.d.ts.map +0 -1
- package/dist/providers/codex.js +0 -461
- package/dist/providers/codex.js.map +0 -1
- package/dist/providers/fallback.d.ts +0 -71
- package/dist/providers/fallback.d.ts.map +0 -1
- package/dist/providers/fallback.js +0 -223
- package/dist/providers/fallback.js.map +0 -1
|
@@ -1,857 +0,0 @@
|
|
|
1
|
-
import { nodeFetch } from "../providers/node-fetch.js";
|
|
2
|
-
const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
|
|
3
|
-
const ANTHROPIC_VERSION = "2023-06-01";
|
|
4
|
-
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
|
5
|
-
/** Anthropic accepts at most 4 cache_control markers per request. We use 3
|
|
6
|
-
* (system, tools, last message) — leaves headroom if a future change wants
|
|
7
|
-
* one more without restructuring the breakpoint plan. */
|
|
8
|
-
const MAX_CACHE_BREAKPOINTS = 4;
|
|
9
|
-
/**
|
|
10
|
-
* Raw Anthropic Messages API adapter.
|
|
11
|
-
*
|
|
12
|
-
* Uses fetch directly (no `@anthropic-ai/sdk` dependency) — keeps the dep
|
|
13
|
-
* surface minimal and lets hosts that already pull in the official SDK (or
|
|
14
|
-
* the higher-level `@anthropic-ai/claude-agent-sdk` Claude-CLI wrapper) use
|
|
15
|
-
* either side-by-side without version conflicts.
|
|
16
|
-
*
|
|
17
|
-
* Auth: `ANTHROPIC_API_KEY` env var. Independent from any OAuth-based
|
|
18
|
-
* Claude Code session the host process may have — passing an explicit key
|
|
19
|
-
* to the constructor takes precedence.
|
|
20
|
-
*/
|
|
21
|
-
export class AnthropicProvider {
|
|
22
|
-
apiKey;
|
|
23
|
-
idleTimeoutMs;
|
|
24
|
-
totalTimeoutMs;
|
|
25
|
-
/**
|
|
26
|
-
* @param idleTimeoutMs Socket inactivity timeout for the request/stream
|
|
27
|
-
* (default 600_000, matching Hermes' large-context stale floor). Routed
|
|
28
|
-
* through `node:http` so it actually applies — Bun's global `fetch` caps
|
|
29
|
-
* every request at a hard ~300 s that no signal can raise (see
|
|
30
|
-
* `node-fetch.ts`). Resets on every byte, so long streams are never cut; it
|
|
31
|
-
* only bounds time-to-first-byte and mid-stream stalls.
|
|
32
|
-
* @param totalTimeoutMs Absolute wall-clock ceiling (default 1_800_000).
|
|
33
|
-
*/
|
|
34
|
-
constructor(apiKey, idleTimeoutMs = 600_000, totalTimeoutMs = 1_800_000) {
|
|
35
|
-
this.apiKey = apiKey;
|
|
36
|
-
this.idleTimeoutMs = idleTimeoutMs;
|
|
37
|
-
this.totalTimeoutMs = totalTimeoutMs;
|
|
38
|
-
}
|
|
39
|
-
static fromEnv() {
|
|
40
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
41
|
-
if (!apiKey) {
|
|
42
|
-
throw new Error("Maestro AnthropicProvider: ANTHROPIC_API_KEY env var is not set");
|
|
43
|
-
}
|
|
44
|
-
return new AnthropicProvider(apiKey);
|
|
45
|
-
}
|
|
46
|
-
async complete(opts) {
|
|
47
|
-
// Apply prompt-caching breakpoints to the three slots Anthropic's cache
|
|
48
|
-
// recognizes (system, tools, last message). claude/codex SDKs do this
|
|
49
|
-
// internally; since maestro hits the API raw, we own it here.
|
|
50
|
-
//
|
|
51
|
-
// Cache hits drop input-token cost roughly 10× and shave hundreds of ms
|
|
52
|
-
// off TTFT for long-running multi-turn sessions — the same conversation
|
|
53
|
-
// re-sends the same system + tool schemas every iteration, and most of
|
|
54
|
-
// the prior history is stable across the agent loop's tool-round cycle.
|
|
55
|
-
//
|
|
56
|
-
// We rebuild the body each turn rather than mutating opts.messages in
|
|
57
|
-
// place so the persisted history (read back from JSONL on the next
|
|
58
|
-
// resume) stays free of stale cache_control markers.
|
|
59
|
-
const body = {
|
|
60
|
-
model: opts.model,
|
|
61
|
-
max_tokens: opts.maxTokens ?? 4096,
|
|
62
|
-
system: buildCacheableSystem(opts.system),
|
|
63
|
-
messages: buildCacheableMessages(sanitizeThinkingBlocksForWire(opts.messages)),
|
|
64
|
-
};
|
|
65
|
-
if (opts.tools && opts.tools.length > 0) {
|
|
66
|
-
body.tools = buildCacheableTools(opts.tools);
|
|
67
|
-
}
|
|
68
|
-
applyThinkingBudget(body, opts.thinkingBudget);
|
|
69
|
-
const headers = {
|
|
70
|
-
"content-type": "application/json",
|
|
71
|
-
"x-api-key": this.apiKey,
|
|
72
|
-
"anthropic-version": ANTHROPIC_VERSION,
|
|
73
|
-
};
|
|
74
|
-
applyThinkingHeaders(headers, opts.thinkingBudget);
|
|
75
|
-
const init = {
|
|
76
|
-
method: "POST",
|
|
77
|
-
headers,
|
|
78
|
-
body: JSON.stringify(body),
|
|
79
|
-
idleTimeoutMs: this.idleTimeoutMs,
|
|
80
|
-
totalTimeoutMs: this.totalTimeoutMs,
|
|
81
|
-
...(opts.abortSignal ? { signal: opts.abortSignal } : {}),
|
|
82
|
-
};
|
|
83
|
-
const response = await nodeFetch(ANTHROPIC_API_URL, init);
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
const text = await response.text();
|
|
86
|
-
throw new Error(`Anthropic API ${response.status}: ${text}`);
|
|
87
|
-
}
|
|
88
|
-
const data = (await response.json());
|
|
89
|
-
return {
|
|
90
|
-
content: data.content,
|
|
91
|
-
stopReason: data.stop_reason,
|
|
92
|
-
usage: mapUsage(data.usage),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Streaming variant of `complete()`. Sends the same request body with
|
|
97
|
-
* `stream: true`, parses the resulting SSE event stream into the small
|
|
98
|
-
* set of chunks the agent loop consumes (text_delta, tool_use_start /
|
|
99
|
-
* input_delta / complete, message_complete).
|
|
100
|
-
*
|
|
101
|
-
* The model's network shape is Anthropic SSE — events like
|
|
102
|
-
* `message_start`, `content_block_start`, `content_block_delta`,
|
|
103
|
-
* `content_block_stop`, `message_delta`, `message_stop`, plus periodic
|
|
104
|
-
* `ping` keepalives. Most are housekeeping; the loop only needs the
|
|
105
|
-
* deltas + tool_use lifecycle + terminal stop_reason / usage. Everything
|
|
106
|
-
* else is consumed and dropped by this adapter.
|
|
107
|
-
*/
|
|
108
|
-
async *stream(opts) {
|
|
109
|
-
const body = {
|
|
110
|
-
model: opts.model,
|
|
111
|
-
max_tokens: opts.maxTokens ?? 4096,
|
|
112
|
-
system: buildCacheableSystem(opts.system),
|
|
113
|
-
messages: buildCacheableMessages(sanitizeThinkingBlocksForWire(opts.messages)),
|
|
114
|
-
stream: true,
|
|
115
|
-
};
|
|
116
|
-
if (opts.tools && opts.tools.length > 0) {
|
|
117
|
-
body.tools = buildCacheableTools(opts.tools);
|
|
118
|
-
}
|
|
119
|
-
applyThinkingBudget(body, opts.thinkingBudget);
|
|
120
|
-
const headers = {
|
|
121
|
-
"content-type": "application/json",
|
|
122
|
-
"x-api-key": this.apiKey,
|
|
123
|
-
"anthropic-version": ANTHROPIC_VERSION,
|
|
124
|
-
accept: "text/event-stream",
|
|
125
|
-
};
|
|
126
|
-
applyThinkingHeaders(headers, opts.thinkingBudget);
|
|
127
|
-
const init = {
|
|
128
|
-
method: "POST",
|
|
129
|
-
headers,
|
|
130
|
-
body: JSON.stringify(body),
|
|
131
|
-
idleTimeoutMs: this.idleTimeoutMs,
|
|
132
|
-
totalTimeoutMs: this.totalTimeoutMs,
|
|
133
|
-
...(opts.abortSignal ? { signal: opts.abortSignal } : {}),
|
|
134
|
-
};
|
|
135
|
-
const response = await nodeFetch(ANTHROPIC_API_URL, init);
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
const text = await response.text();
|
|
138
|
-
throw new Error(`Anthropic API ${response.status}: ${text}`);
|
|
139
|
-
}
|
|
140
|
-
if (!response.body) {
|
|
141
|
-
throw new Error("Anthropic API: streaming response missing body");
|
|
142
|
-
}
|
|
143
|
-
// Per-block scratch space. Anthropic indexes content blocks by position
|
|
144
|
-
// in the assistant message; we key on that index so concurrent tool_use
|
|
145
|
-
// blocks (Anthropic allows parallel tool_use in a single response) get
|
|
146
|
-
// their input_json_delta routed to the right accumulator.
|
|
147
|
-
const blockMeta = new Map();
|
|
148
|
-
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
149
|
-
let stopReason = "end_turn";
|
|
150
|
-
for await (const event of parseSseStream(response.body, opts.abortSignal)) {
|
|
151
|
-
switch (event.type) {
|
|
152
|
-
case "message_start": {
|
|
153
|
-
// Initial usage comes here (input tokens + cache stats). output
|
|
154
|
-
// tokens still 0 — they accumulate via message_delta.
|
|
155
|
-
const u = asRecord(event.message)?.usage;
|
|
156
|
-
if (u) {
|
|
157
|
-
usage.inputTokens = u.input_tokens ?? 0;
|
|
158
|
-
usage.outputTokens = u.output_tokens ?? 0;
|
|
159
|
-
if (u.cache_creation_input_tokens !== undefined) {
|
|
160
|
-
usage.cacheCreationInputTokens = u.cache_creation_input_tokens;
|
|
161
|
-
}
|
|
162
|
-
if (u.cache_read_input_tokens !== undefined) {
|
|
163
|
-
usage.cacheReadInputTokens = u.cache_read_input_tokens;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "content_block_start": {
|
|
169
|
-
const idx = event.index ?? 0;
|
|
170
|
-
const cb = asRecord(event.content_block);
|
|
171
|
-
if (cb?.type === "text") {
|
|
172
|
-
blockMeta.set(idx, { type: "text" });
|
|
173
|
-
}
|
|
174
|
-
else if (cb?.type === "tool_use") {
|
|
175
|
-
const id = typeof cb.id === "string" ? cb.id : "";
|
|
176
|
-
const name = typeof cb.name === "string" ? cb.name : "";
|
|
177
|
-
blockMeta.set(idx, { type: "tool_use", id, name });
|
|
178
|
-
yield { type: "tool_use_start", id, name };
|
|
179
|
-
}
|
|
180
|
-
else if (cb?.type === "thinking") {
|
|
181
|
-
const meta = {
|
|
182
|
-
type: "thinking",
|
|
183
|
-
thinking: typeof cb.thinking === "string" ? cb.thinking : "",
|
|
184
|
-
};
|
|
185
|
-
if (typeof cb.signature === "string")
|
|
186
|
-
meta.signature = cb.signature;
|
|
187
|
-
blockMeta.set(idx, meta);
|
|
188
|
-
}
|
|
189
|
-
else if (cb?.type === "redacted_thinking") {
|
|
190
|
-
blockMeta.set(idx, {
|
|
191
|
-
type: "redacted_thinking",
|
|
192
|
-
data: typeof cb.data === "string" ? cb.data : "",
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
case "content_block_delta": {
|
|
198
|
-
const idx = event.index ?? 0;
|
|
199
|
-
const meta = blockMeta.get(idx);
|
|
200
|
-
const delta = asRecord(event.delta);
|
|
201
|
-
if (!meta || !delta)
|
|
202
|
-
break;
|
|
203
|
-
if (meta.type === "text" && delta.type === "text_delta" && delta.text) {
|
|
204
|
-
yield { type: "text_delta", text: String(delta.text) };
|
|
205
|
-
}
|
|
206
|
-
else if (meta.type === "tool_use" &&
|
|
207
|
-
delta.type === "input_json_delta" &&
|
|
208
|
-
typeof delta.partial_json === "string") {
|
|
209
|
-
yield {
|
|
210
|
-
type: "tool_use_input_delta",
|
|
211
|
-
id: meta.id ?? "",
|
|
212
|
-
partial_json: delta.partial_json,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
else if (meta.type === "thinking" &&
|
|
216
|
-
delta.type === "thinking_delta" &&
|
|
217
|
-
typeof delta.thinking === "string") {
|
|
218
|
-
meta.thinking = `${meta.thinking ?? ""}${delta.thinking}`;
|
|
219
|
-
}
|
|
220
|
-
else if (meta.type === "thinking" &&
|
|
221
|
-
delta.type === "signature_delta" &&
|
|
222
|
-
typeof delta.signature === "string") {
|
|
223
|
-
meta.signature = `${meta.signature ?? ""}${delta.signature}`;
|
|
224
|
-
}
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
case "content_block_stop": {
|
|
228
|
-
const idx = event.index ?? 0;
|
|
229
|
-
const meta = blockMeta.get(idx);
|
|
230
|
-
if (meta?.type === "tool_use") {
|
|
231
|
-
yield {
|
|
232
|
-
type: "tool_use_complete",
|
|
233
|
-
id: meta.id ?? "",
|
|
234
|
-
name: meta.name ?? "",
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
else if (meta?.type === "thinking") {
|
|
238
|
-
// CRITICAL: drop the block entirely when no signature was
|
|
239
|
-
// collected. Anthropic requires a `signature` field on every
|
|
240
|
-
// replayed `thinking` block — sending one without it 400s the
|
|
241
|
-
// next API call with
|
|
242
|
-
// "messages.N.content.M.thinking.signature: Field required".
|
|
243
|
-
//
|
|
244
|
-
// Two paths lead here:
|
|
245
|
-
// 1. No `signature_delta` ever arrived (server-side hiccup,
|
|
246
|
-
// mid-block stream interrupt — rare but observed).
|
|
247
|
-
// 2. `signature_delta` arrived with an empty string. The
|
|
248
|
-
// concat `${meta.signature ?? ""}${delta.signature}` then
|
|
249
|
-
// yielded "", which we treat the same as missing.
|
|
250
|
-
//
|
|
251
|
-
// An empty signature isn't a valid signature (Anthropic's
|
|
252
|
-
// verification would reject it anyway), so it's safer to drop
|
|
253
|
-
// the block from history. The model loses some reasoning
|
|
254
|
-
// continuity for this turn, but the conversation continues —
|
|
255
|
-
// strictly better than the alternative (whole turn fails).
|
|
256
|
-
if (typeof meta.signature === "string" && meta.signature.length > 0) {
|
|
257
|
-
yield {
|
|
258
|
-
type: "thinking_complete",
|
|
259
|
-
block: {
|
|
260
|
-
type: "thinking",
|
|
261
|
-
thinking: meta.thinking ?? "",
|
|
262
|
-
signature: meta.signature,
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else if (meta?.type === "redacted_thinking") {
|
|
268
|
-
yield {
|
|
269
|
-
type: "thinking_complete",
|
|
270
|
-
block: { type: "redacted_thinking", data: meta.data ?? "" },
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
case "message_delta": {
|
|
276
|
-
// Carries final stop_reason + cumulative output usage.
|
|
277
|
-
const delta = asRecord(event.delta);
|
|
278
|
-
if (typeof delta?.stop_reason === "string")
|
|
279
|
-
stopReason = delta.stop_reason;
|
|
280
|
-
const u = event.usage;
|
|
281
|
-
if (u?.output_tokens !== undefined)
|
|
282
|
-
usage.outputTokens = u.output_tokens;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
case "message_stop": {
|
|
286
|
-
// Anthropic's "we're done" marker. Emit the terminal chunk.
|
|
287
|
-
yield { type: "message_complete", stopReason, usage };
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
case "error": {
|
|
291
|
-
const err = event.error;
|
|
292
|
-
throw new Error(`Anthropic stream error: ${err?.type ?? "unknown"} — ${err?.message ?? ""}`);
|
|
293
|
-
}
|
|
294
|
-
default:
|
|
295
|
-
// ping, etc. — ignore.
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// Stream ended without an explicit message_stop (server hung up cleanly).
|
|
300
|
-
// Emit a terminal chunk anyway so the loop can finalize its turn.
|
|
301
|
-
yield { type: "message_complete", stopReason, usage };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function asRecord(value) {
|
|
305
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
306
|
-
? value
|
|
307
|
-
: null;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Parse Anthropic's `text/event-stream` response body into typed SSE events.
|
|
311
|
-
*
|
|
312
|
-
* Anthropic frames each event as
|
|
313
|
-
* event: <type>\ndata: <json>\n\n
|
|
314
|
-
* We buffer across chunk boundaries, split on the blank-line terminator,
|
|
315
|
-
* and JSON-parse the `data:` payload. The `event:` line is informational
|
|
316
|
-
* (Anthropic always also embeds the type inside the JSON), so we trust
|
|
317
|
-
* the `data` payload's `type` field as the authoritative event type —
|
|
318
|
-
* matches what the official SDKs do.
|
|
319
|
-
*
|
|
320
|
-
* The abort signal is honored by aborting the underlying ReadableStream
|
|
321
|
-
* reader, which propagates AbortError up into the caller.
|
|
322
|
-
*/
|
|
323
|
-
async function* parseSseStream(body, abortSignal) {
|
|
324
|
-
const reader = body.getReader();
|
|
325
|
-
const decoder = new TextDecoder("utf-8");
|
|
326
|
-
let buf = "";
|
|
327
|
-
const onAbort = () => {
|
|
328
|
-
reader.cancel("aborted").catch(() => { });
|
|
329
|
-
};
|
|
330
|
-
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
331
|
-
try {
|
|
332
|
-
while (true) {
|
|
333
|
-
const { value, done } = await reader.read();
|
|
334
|
-
if (done)
|
|
335
|
-
break;
|
|
336
|
-
buf += decoder.decode(value, { stream: true });
|
|
337
|
-
// Drain every complete event (terminated by a blank line).
|
|
338
|
-
let idx = buf.indexOf("\n\n");
|
|
339
|
-
while (idx >= 0) {
|
|
340
|
-
const raw = buf.slice(0, idx);
|
|
341
|
-
buf = buf.slice(idx + 2);
|
|
342
|
-
const dataLine = raw
|
|
343
|
-
.split("\n")
|
|
344
|
-
.find((l) => l.startsWith("data:"))
|
|
345
|
-
?.slice("data:".length)
|
|
346
|
-
.trim();
|
|
347
|
-
if (dataLine) {
|
|
348
|
-
try {
|
|
349
|
-
yield JSON.parse(dataLine);
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
// Malformed frame — skip rather than abort the stream; Anthropic
|
|
353
|
-
// shouldn't emit these but defensive parsing avoids one bad byte
|
|
354
|
-
// taking down a whole turn.
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
idx = buf.indexOf("\n\n");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
finally {
|
|
362
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
363
|
-
reader.releaseLock();
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Defensive scrub for thinking blocks that arrive at the wire builder without
|
|
368
|
-
* a usable `signature`. Anthropic's API requires every replayed `thinking`
|
|
369
|
-
* block to carry the original signature it produced; sending one without it
|
|
370
|
-
* trips a 400 `messages.N.content.M.thinking.signature: Field required`.
|
|
371
|
-
*
|
|
372
|
-
* In normal operation the streaming code already refuses to emit
|
|
373
|
-
* thinking blocks lacking a signature (see `content_block_stop` in the SSE
|
|
374
|
-
* handler), so this filter is belt-and-suspenders for two recovery cases:
|
|
375
|
-
*
|
|
376
|
-
* 1. A session persisted by an older SDK build (pre-v0.1.20) that DID
|
|
377
|
-
* emit signatureless thinking blocks now loads from disk. Without this
|
|
378
|
-
* filter, the first wire call after upgrading would 400 on the bad
|
|
379
|
-
* block.
|
|
380
|
-
*
|
|
381
|
-
* 2. A future provider-side transformation (compaction, dedup) accidentally
|
|
382
|
-
* drops the field. Filtering here gives us a single chokepoint to lock.
|
|
383
|
-
*
|
|
384
|
-
* Two complications the filter has to handle:
|
|
385
|
-
*
|
|
386
|
-
* - **Anthropic forbids mixing `thinking` and `text` after `tool_use` /
|
|
387
|
-
* `tool_result` in the same message.** Simply removing the bad thinking
|
|
388
|
-
* block can leave a message starting with `text` + `tool_use`, which is
|
|
389
|
-
* still valid. So we only drop the thinking block itself, not surrounding
|
|
390
|
-
* blocks.
|
|
391
|
-
*
|
|
392
|
-
* - **An assistant message can become content-empty after the filter.** A
|
|
393
|
-
* turn like `[thinking(broken)]` reduces to `[]`. Anthropic rejects empty
|
|
394
|
-
* content arrays, so we substitute a single empty text block as a
|
|
395
|
-
* placeholder — same trick `loop.ts` already uses for the scrubber-drops-
|
|
396
|
-
* everything case.
|
|
397
|
-
*/
|
|
398
|
-
export function sanitizeThinkingBlocksForWire(messages) {
|
|
399
|
-
let mutated = false;
|
|
400
|
-
const out = messages.map((msg) => {
|
|
401
|
-
if (!Array.isArray(msg.content))
|
|
402
|
-
return msg;
|
|
403
|
-
let blockChanged = false;
|
|
404
|
-
const filtered = msg.content.filter((block) => {
|
|
405
|
-
if (block.type !== "thinking")
|
|
406
|
-
return true;
|
|
407
|
-
const sig = block.signature;
|
|
408
|
-
const valid = typeof sig === "string" && sig.length > 0;
|
|
409
|
-
if (!valid) {
|
|
410
|
-
blockChanged = true;
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
return true;
|
|
414
|
-
});
|
|
415
|
-
if (!blockChanged)
|
|
416
|
-
return msg;
|
|
417
|
-
mutated = true;
|
|
418
|
-
// Empty content array isn't a valid Anthropic message — keep one
|
|
419
|
-
// placeholder text block so the turn stays well-formed.
|
|
420
|
-
const safe = filtered.length > 0 ? filtered : [{ type: "text", text: "" }];
|
|
421
|
-
return { role: msg.role, content: safe };
|
|
422
|
-
});
|
|
423
|
-
return mutated ? out : messages;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Convert the user-supplied `system` (plain string) into the array shape
|
|
427
|
-
* Anthropic accepts when you need a cache_control marker. A string slot
|
|
428
|
-
* has no cache field, so we lift it into a single text block and tag it
|
|
429
|
-
* ephemeral. Returning the original string unchanged when it's empty keeps
|
|
430
|
-
* the request shape minimal for prompt-less calls.
|
|
431
|
-
*/
|
|
432
|
-
export function buildCacheableSystem(system) {
|
|
433
|
-
if (!system || system.length === 0)
|
|
434
|
-
return system;
|
|
435
|
-
return [{ type: "text", text: system, cache_control: { type: "ephemeral" } }];
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Tag the last tool with cache_control so the (usually large) tool schema
|
|
439
|
-
* block lands in the cache after the first turn. Other tools are passed
|
|
440
|
-
* through verbatim — Anthropic caches the entire tools array prefix up to
|
|
441
|
-
* the last marker, so one breakpoint covers every preceding entry.
|
|
442
|
-
*
|
|
443
|
-
* Returns a new array; the caller's `opts.tools` reference is untouched so
|
|
444
|
-
* subsequent calls don't accumulate markers.
|
|
445
|
-
*/
|
|
446
|
-
export function buildCacheableTools(tools) {
|
|
447
|
-
if (tools.length === 0)
|
|
448
|
-
return [];
|
|
449
|
-
const out = tools.map((t) => ({
|
|
450
|
-
...t,
|
|
451
|
-
}));
|
|
452
|
-
out[out.length - 1] = { ...out[out.length - 1], cache_control: { type: "ephemeral" } };
|
|
453
|
-
return out;
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Tag the last block of the last message with cache_control. This is the
|
|
457
|
-
* rolling breakpoint that moves forward each turn — the second-to-last
|
|
458
|
-
* marker (set on the previous call) ages out into a regular cache prefix,
|
|
459
|
-
* which Anthropic will hit on the new call's read.
|
|
460
|
-
*
|
|
461
|
-
* Three message-shape cases:
|
|
462
|
-
* - last message is `{role, content: string}` → lift to a single text
|
|
463
|
-
* block with cache_control.
|
|
464
|
-
* - last message has a block array → shallow-clone the array and replace
|
|
465
|
-
* just the final block with a copy carrying cache_control.
|
|
466
|
-
* - empty content (defensive) → leave unchanged, no marker.
|
|
467
|
-
*
|
|
468
|
-
* Anthropic allows max 4 cache_control markers per request. With system
|
|
469
|
-
* + tools + this rolling one we sit at 3, leaving headroom; we still cap
|
|
470
|
-
* defensively in case a future change adds another slot.
|
|
471
|
-
*/
|
|
472
|
-
export function buildCacheableMessages(messages) {
|
|
473
|
-
if (messages.length === 0)
|
|
474
|
-
return [];
|
|
475
|
-
const out = messages.map((m) => m);
|
|
476
|
-
const lastIdx = out.length - 1;
|
|
477
|
-
const last = out[lastIdx];
|
|
478
|
-
if (typeof last.content === "string") {
|
|
479
|
-
if (last.content.length === 0)
|
|
480
|
-
return out;
|
|
481
|
-
out[lastIdx] = {
|
|
482
|
-
role: last.role,
|
|
483
|
-
content: [
|
|
484
|
-
{
|
|
485
|
-
type: "text",
|
|
486
|
-
text: last.content,
|
|
487
|
-
cache_control: { type: "ephemeral" },
|
|
488
|
-
},
|
|
489
|
-
],
|
|
490
|
-
};
|
|
491
|
-
return out;
|
|
492
|
-
}
|
|
493
|
-
if (Array.isArray(last.content) && last.content.length > 0) {
|
|
494
|
-
const blocks = [...last.content];
|
|
495
|
-
const tailIdx = blocks.length - 1;
|
|
496
|
-
blocks[tailIdx] = {
|
|
497
|
-
...blocks[tailIdx],
|
|
498
|
-
cache_control: { type: "ephemeral" },
|
|
499
|
-
};
|
|
500
|
-
out[lastIdx] = { role: last.role, content: blocks };
|
|
501
|
-
}
|
|
502
|
-
return out;
|
|
503
|
-
}
|
|
504
|
-
// Export the cap so tests can assert we don't drift past Anthropic's limit.
|
|
505
|
-
export const __cacheBreakpointCap = MAX_CACHE_BREAKPOINTS;
|
|
506
|
-
/**
|
|
507
|
-
* Patch `body` with the Anthropic extended-thinking payload when a budget is
|
|
508
|
-
* supplied. No-op when `budget` is undefined / 0 — the model emits no
|
|
509
|
-
* reasoning chain, same as claude/codex with effort omitted.
|
|
510
|
-
*
|
|
511
|
-
* Anthropic requires `max_tokens > thinking.budget_tokens`; if the caller's
|
|
512
|
-
* max_tokens is too small we lift it past `budget + 1024` so the API doesn't
|
|
513
|
-
* reject the request. Caller's explicit ceiling wins when it's already
|
|
514
|
-
* larger — we never SHRINK max_tokens.
|
|
515
|
-
*
|
|
516
|
-
* Thinking is only valid on Claude Sonnet 4 / Opus 4 / Haiku 4.5 (and later).
|
|
517
|
-
* maestroRegistry currently only ships sonnet, so we don't gate on model id
|
|
518
|
-
* here; if Phase 5 multi-provider lands an older model, the provider for
|
|
519
|
-
* that model just ignores `thinkingBudget` (this helper is Anthropic-only).
|
|
520
|
-
*/
|
|
521
|
-
export function applyThinkingBudget(body, budget) {
|
|
522
|
-
if (!budget || budget <= 0)
|
|
523
|
-
return;
|
|
524
|
-
body.thinking = { type: "enabled", budget_tokens: budget };
|
|
525
|
-
const minMax = budget + 1024;
|
|
526
|
-
const current = typeof body.max_tokens === "number" ? body.max_tokens : 0;
|
|
527
|
-
if (current < minMax)
|
|
528
|
-
body.max_tokens = minMax;
|
|
529
|
-
}
|
|
530
|
-
function applyThinkingHeaders(headers, budget) {
|
|
531
|
-
if (!budget || budget <= 0)
|
|
532
|
-
return;
|
|
533
|
-
headers["anthropic-beta"] = INTERLEAVED_THINKING_BETA;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Map the SDK's shared `EffortLevel` to an Anthropic thinking budget in
|
|
537
|
-
* tokens. Maestro accepts `low|medium|high|xhigh|max` (see
|
|
538
|
-
* `MAESTRO_EFFORT_VALUES`); other values return undefined so the caller skips
|
|
539
|
-
* thinking entirely.
|
|
540
|
-
*
|
|
541
|
-
* Scale (v0.1.19+ — aligned with the prompt-keyword tier ladder):
|
|
542
|
-
* - low → undefined (thinking off — latency-priority working mode)
|
|
543
|
-
* - medium → undefined (thinking off — default; matches Claude Code's
|
|
544
|
-
* "off unless asked" behavior. Persona text still
|
|
545
|
-
* shapes the model's working style, but the API
|
|
546
|
-
* ships without a `thinking` payload.)
|
|
547
|
-
* - high → 4 096 (T1 — same as the `think` / `생각해줘` keyword)
|
|
548
|
-
* - xhigh → 10 000 (T2 — same as `think hard` / `깊이 생각`)
|
|
549
|
-
* - max → 31 999 (T3 — same as `ultrathink` / `끝까지 생각`)
|
|
550
|
-
*
|
|
551
|
-
* Why only high/xhigh/max grant thinking: the lower three rungs are the
|
|
552
|
-
* conversational defaults where fast tool dispatch beats deeper reasoning
|
|
553
|
-
* (Telegram bot, chat UI, simple file ops). Users who want extended
|
|
554
|
-
* thinking either escalate via the effort knob explicitly or type the
|
|
555
|
-
* keyword in the prompt — both routes land on the same three tier budgets,
|
|
556
|
-
* so behavior is predictable across surfaces.
|
|
557
|
-
*
|
|
558
|
-
* Symmetry with `detectThinkingKeyword`: the three "on" tiers here mirror
|
|
559
|
-
* the three keyword tiers exactly. Whichever path activates thinking, the
|
|
560
|
-
* model sees one of {4096, 10000, 31999} — no fourth budget exists.
|
|
561
|
-
* `resolveThinkingBudget` (provider.ts) picks the higher of the two if
|
|
562
|
-
* both fire, so an `effort: "high"` call that happens to contain
|
|
563
|
-
* "ultrathink" in the prompt still gets T3, not T1.
|
|
564
|
-
*
|
|
565
|
-
* Budgets are *ceilings*, not enforced spending: the model decides how
|
|
566
|
-
* much to actually emit inside the budget. A 31999 ceiling on a simple
|
|
567
|
-
* task still costs almost nothing — the savings come from latency
|
|
568
|
-
* (model returns sooner when it knows the budget is smaller).
|
|
569
|
-
*/
|
|
570
|
-
export function effortToThinkingBudget(e) {
|
|
571
|
-
switch (e) {
|
|
572
|
-
case "low":
|
|
573
|
-
return undefined;
|
|
574
|
-
case "medium":
|
|
575
|
-
return undefined;
|
|
576
|
-
case "high":
|
|
577
|
-
return 4096;
|
|
578
|
-
case "xhigh":
|
|
579
|
-
return 10000;
|
|
580
|
-
case "max":
|
|
581
|
-
return 31999;
|
|
582
|
-
default:
|
|
583
|
-
return undefined;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Claude Code-style think-keyword detection.
|
|
588
|
-
*
|
|
589
|
-
* Scans the caller's user prompt for an explicit "think harder" cue. When
|
|
590
|
-
* present, returns the matching thinking-budget tier; otherwise returns
|
|
591
|
-
* undefined (the loop then ships the call with thinking disabled — same
|
|
592
|
-
* default Claude Code uses).
|
|
593
|
-
*
|
|
594
|
-
* Why this exists alongside `effortToThinkingBudget`:
|
|
595
|
-
* - `effort` is a *programmatic* knob set by the SDK consumer (CLI flag,
|
|
596
|
-
* config file, server-side default). It's awkward to plumb through
|
|
597
|
-
* conversational surfaces like a Telegram bot or chat UI where the user
|
|
598
|
-
* just types text.
|
|
599
|
-
* - The keyword path lets end-users toggle thinking via the prompt itself
|
|
600
|
-
* ("think harder", "끝까지 생각해줘") without the host having to wire
|
|
601
|
-
* UI for an effort selector. This matches Claude Code's behavior and
|
|
602
|
-
* is what users coming from CC instinctively try.
|
|
603
|
-
*
|
|
604
|
-
* Tier mapping mirrors Claude Code's documented set:
|
|
605
|
-
* tier 3 (max) → 31999 tokens — "ultrathink", "think harder",
|
|
606
|
-
* "끝까지 생각", "심층 생각"
|
|
607
|
-
* tier 2 (deep) → 10000 tokens — "think hard", "think a lot",
|
|
608
|
-
* "깊이 생각", "깊게 생각"
|
|
609
|
-
* tier 1 (basic) → 4096 tokens — "think" (English), "생각해줘" /
|
|
610
|
-
* "생각해봐" / "잘 생각" (Korean)
|
|
611
|
-
*
|
|
612
|
-
* Why 31999 (not 32768): Anthropic requires `max_tokens > budget`, and
|
|
613
|
-
* 31999 + 1024 = 33023 stays under the 64K max_tokens ceiling on sonnet-4-6
|
|
614
|
-
* while leaving room for the final text response. Picking 32768 would force
|
|
615
|
-
* max_tokens to ≥ 33792 which loses headroom for long answers.
|
|
616
|
-
*
|
|
617
|
-
* Why "think" word-boundary on English but substring on Korean:
|
|
618
|
-
* English splits with whitespace so `\bthink\b` cleanly avoids false hits
|
|
619
|
-
* on "thinking" / "rethink". Korean morphology glues particles to verbs
|
|
620
|
-
* ("생각해줘" / "생각해봐"), so we substring-match the verb stem and accept
|
|
621
|
-
* the false-positive rate (a Korean user saying "그는 잘 생각해" in a code
|
|
622
|
-
* snippet would unintentionally trigger — same trade-off CC made in
|
|
623
|
-
* English).
|
|
624
|
-
*
|
|
625
|
-
* Highest tier wins: if both "think" and "ultrathink" appear (e.g. "think
|
|
626
|
-
* about this — actually ultrathink"), we honor the higher budget. Tier
|
|
627
|
-
* ordering matters because tier-3 substrings contain tier-1's pattern.
|
|
628
|
-
*
|
|
629
|
-
* Pure helper — no side effects, safe to call on every turn. Exposed for
|
|
630
|
-
* tests and for hosts that want to compose their own keyword surfaces
|
|
631
|
-
* (e.g. add Japanese keywords without forking).
|
|
632
|
-
*/
|
|
633
|
-
export function detectThinkingKeyword(prompt) {
|
|
634
|
-
if (!prompt)
|
|
635
|
-
return undefined;
|
|
636
|
-
const lower = prompt.toLowerCase();
|
|
637
|
-
// Tier 3 — max. Order matters: check the highest tier first so a prompt
|
|
638
|
-
// containing "ultrathink" doesn't get downgraded by an earlier "think"
|
|
639
|
-
// match. Korean cues sit alongside the English set; substring match is
|
|
640
|
-
// intentional (see docstring).
|
|
641
|
-
if (/\bultrathink\b/.test(lower) ||
|
|
642
|
-
/\bthink\s+harder\b/.test(lower) ||
|
|
643
|
-
/\bmegathink\b/.test(lower) ||
|
|
644
|
-
prompt.includes("끝까지 생각") ||
|
|
645
|
-
prompt.includes("심층 생각")) {
|
|
646
|
-
return 31999;
|
|
647
|
-
}
|
|
648
|
-
// Tier 2 — deep.
|
|
649
|
-
if (/\bthink\s+hard\b/.test(lower) ||
|
|
650
|
-
/\bthink\s+a\s+lot\b/.test(lower) ||
|
|
651
|
-
prompt.includes("깊이 생각") ||
|
|
652
|
-
prompt.includes("깊게 생각")) {
|
|
653
|
-
return 10000;
|
|
654
|
-
}
|
|
655
|
-
// Tier 1 — basic. The English `\bthink\b` is broad and will fire on any
|
|
656
|
-
// standalone "think" mention; that's the documented Claude Code behavior
|
|
657
|
-
// and matches user intuition. For Korean we require an imperative-ish
|
|
658
|
-
// form ("생각해줘" / "생각해봐") or a deliberate modifier ("잘 생각") to
|
|
659
|
-
// avoid casual triggering on words like "생각" in a description.
|
|
660
|
-
if (/\bthink\b/.test(lower) ||
|
|
661
|
-
prompt.includes("생각해줘") ||
|
|
662
|
-
prompt.includes("생각해봐") ||
|
|
663
|
-
prompt.includes("잘 생각")) {
|
|
664
|
-
return 4096;
|
|
665
|
-
}
|
|
666
|
-
return undefined;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Resolve the actual thinking budget the loop should send on *this turn*,
|
|
670
|
-
* given the base budget (already derived from effort) and the turn's
|
|
671
|
-
* position within the iteration cap.
|
|
672
|
-
*
|
|
673
|
-
* Why turn-adaptive:
|
|
674
|
-
* - Turn 0 (first call) is where the model parses the prompt and lays
|
|
675
|
-
* out its plan. Thinking here is the highest-ROI use of budget —
|
|
676
|
-
* a careful plan saves tool calls later. We send the full base.
|
|
677
|
-
* - Mid turns are tool-dispatch territory: "this grep returned X,
|
|
678
|
-
* now read Y." Interleaved thinking still has value (short
|
|
679
|
-
* reasoning between tool calls is what Anthropic's interleaved
|
|
680
|
-
* beta is for), so we keep the full base here too. Cutting it
|
|
681
|
-
* mid-flow defeats the beta.
|
|
682
|
-
* - The last 3 turns are the *wrap-up zone*. The iteration reminder
|
|
683
|
-
* has already flipped to "finalize NOW, stop tooling and write the
|
|
684
|
-
* final answer." At that point spending another 16K thinking on a
|
|
685
|
-
* turn that mostly emits final text is pure latency waste. We trim
|
|
686
|
-
* to 1/4 of base — enough room for a short "what should this
|
|
687
|
-
* summary include" pass, no room for fresh exploration.
|
|
688
|
-
*
|
|
689
|
-
* Floor: the trimmed budget is clamped to >= 1024 because Anthropic
|
|
690
|
-
* requires `thinking.budget_tokens >= 1024` when thinking is enabled.
|
|
691
|
-
* Returning a value below that would 400 the API. If the caller passed
|
|
692
|
-
* a base smaller than 1024 (shouldn't happen with our effort map, but
|
|
693
|
-
* a host could override), we just return the base as-is — the original
|
|
694
|
-
* `applyThinkingBudget` no-ops zero / undefined, and the user explicitly
|
|
695
|
-
* asked for less.
|
|
696
|
-
*
|
|
697
|
-
* Pure helper — no allocation, no closure — so loop.ts can call it
|
|
698
|
-
* cheaply inside the hot iteration loop. Exported for tests and for
|
|
699
|
-
* hosts that want to compose their own per-turn budgeting.
|
|
700
|
-
*/
|
|
701
|
-
export function thinkingBudgetForTurn(base, iter, maxIter) {
|
|
702
|
-
if (!base || base <= 0)
|
|
703
|
-
return base;
|
|
704
|
-
// Single source of truth — `isWrapUpZone` owns the threshold so this
|
|
705
|
-
// helper, the loop's tool-disable gate, and the wrap-up overlay all
|
|
706
|
-
// fire on exactly the same turn boundary.
|
|
707
|
-
if (isWrapUpZone(iter, maxIter)) {
|
|
708
|
-
const trimmed = Math.floor(base / 4);
|
|
709
|
-
// Floor at 1024 — Anthropic minimum for `thinking.budget_tokens` when
|
|
710
|
-
// thinking is enabled. If base itself is already < 1024 we don't
|
|
711
|
-
// mess with it (see docstring).
|
|
712
|
-
if (base < 1024)
|
|
713
|
-
return base;
|
|
714
|
-
return Math.max(trimmed, 1024);
|
|
715
|
-
}
|
|
716
|
-
return base;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Single source of truth for "is this turn in the wrap-up zone?" Used by
|
|
720
|
-
* three call sites that must all fire on the same boundary:
|
|
721
|
-
* 1. `thinkingBudgetForTurn` — trims the per-turn thinking budget.
|
|
722
|
-
* 2. `loop.ts` callOpts assembly — empties the tools schema (v0.1.17+).
|
|
723
|
-
* 3. `wrapUpOverlayLine` (provider.ts) — emits the reminder cue.
|
|
724
|
-
*
|
|
725
|
-
* Threshold logic:
|
|
726
|
-
* - Tiny caps (`maxIter <= 3`) — return false unconditionally.
|
|
727
|
-
* At maxIter=3 every turn is already a wrap-up turn; gating tools
|
|
728
|
-
* would mean the model can't call any tools from turn 1, which
|
|
729
|
-
* defeats the cap's purpose. Tiny caps trust the model + the
|
|
730
|
-
* iter-line tone to wind things down on their own.
|
|
731
|
-
* - Turn 0 — never wrap-up. The first turn is planning and must
|
|
732
|
-
* keep full capability even when `maxIter` is small (e.g. maxIter=4
|
|
733
|
-
* would otherwise gate the planning turn, which is exactly when
|
|
734
|
-
* the model needs tools most).
|
|
735
|
-
* - Last 3 turns (`maxIter - iter <= 3`) — wrap-up active.
|
|
736
|
-
*
|
|
737
|
-
* Why "last 3" and not "last 1": the model needs at least one full
|
|
738
|
-
* turn after entering wrap-up to actually synthesize and emit the
|
|
739
|
-
* final answer. With a 3-turn lead-in: turn N-3 enters wrap-up (no
|
|
740
|
-
* more tools), turns N-2 / N-1 can iterate on the final text if the
|
|
741
|
-
* first attempt was incomplete, turn N-0 is the hard ceiling.
|
|
742
|
-
*/
|
|
743
|
-
export function isWrapUpZone(iter, maxIter) {
|
|
744
|
-
if (maxIter <= 3)
|
|
745
|
-
return false;
|
|
746
|
-
if (iter <= 0)
|
|
747
|
-
return false;
|
|
748
|
-
return maxIter - iter <= 3;
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Build a system-prompt persona block that names the active effort level
|
|
752
|
-
* AND prescribes concrete behavior the model should adopt this turn.
|
|
753
|
-
*
|
|
754
|
-
* Why a system-prompt block (not a system-reminder line):
|
|
755
|
-
* - First-turn visibility. The per-iteration reminder lives in the user
|
|
756
|
-
* message and is reread each turn, but its `Tool iterations remaining`
|
|
757
|
-
* tone only shifts as the budget drains — at the start of an `xhigh`
|
|
758
|
-
* vs `low` run the model sees nearly identical text. A system-prompt
|
|
759
|
-
* persona lets the model condition every token from turn 1 on the
|
|
760
|
-
* working mode, instead of inferring it indirectly from `maxIter`.
|
|
761
|
-
* - Cache stability. The block is a pure function of `effort`, so the
|
|
762
|
-
* same five strings cycle through every call at a given level. They
|
|
763
|
-
* sit between caller `systemPrompt` and the skills catalog, riding
|
|
764
|
-
* the same prefix-cache boundary that already covers both — no extra
|
|
765
|
-
* invalidations.
|
|
766
|
-
*
|
|
767
|
-
* Effort-shape rationale (terse imperatives win over hedged paragraphs —
|
|
768
|
-
* the model conditions hardest on action verbs, not adjectives):
|
|
769
|
-
* - `low` — answer fast, one Read max, no verification
|
|
770
|
-
* - `medium` — one focused exploration, light cross-check
|
|
771
|
-
* - `high` — multi-file exploration, verify on doubt
|
|
772
|
-
* - `xhigh` — broad survey, hold multiple hypotheses, name edge cases
|
|
773
|
-
* - `max` — exhaustive: every relevant file, tests, all failure modes
|
|
774
|
-
*
|
|
775
|
-
* All five levels emit exactly four bullets (v0.1.16+). Uniform shape
|
|
776
|
-
* keeps the prefix-cache boundary identical-length across levels and
|
|
777
|
-
* makes A/B telemetry honest — a longer level can't outperform a shorter
|
|
778
|
-
* one just because the model had more text to condition on.
|
|
779
|
-
*
|
|
780
|
-
* Returns `undefined` for an unknown / absent effort so callers can skip
|
|
781
|
-
* concatenation entirely; the model then sees only the caller's system
|
|
782
|
-
* prompt, preserving the historical no-effort baseline.
|
|
783
|
-
*
|
|
784
|
-
* Lives in anthropic.ts next to `effortToThinkingBudget` for grep affinity,
|
|
785
|
-
* but the string itself is provider-agnostic — DeepSeek receives the same
|
|
786
|
-
* block via the same caller (provider.ts), so the persona cue is consistent
|
|
787
|
-
* across providers even when `reasoning_effort` is the only native handle
|
|
788
|
-
* the API exposes.
|
|
789
|
-
*/
|
|
790
|
-
export function effortToPersonaPrompt(e) {
|
|
791
|
-
let header;
|
|
792
|
-
let bullets;
|
|
793
|
-
switch (e) {
|
|
794
|
-
case "low":
|
|
795
|
-
header = "You are in **low** effort mode — answer fast.";
|
|
796
|
-
bullets = [
|
|
797
|
-
"Read at most one file. Skip exhaustive search.",
|
|
798
|
-
"No cross-verification unless the user explicitly asked.",
|
|
799
|
-
"Wrap up immediately after the first sufficient answer.",
|
|
800
|
-
"If the question is ambiguous, ask one clarifying question rather than exploring.",
|
|
801
|
-
];
|
|
802
|
-
break;
|
|
803
|
-
case "medium":
|
|
804
|
-
header = "You are in **medium** effort mode — focused work.";
|
|
805
|
-
bullets = [
|
|
806
|
-
"Explore one area thoroughly; do not branch into adjacent files unless directly relevant.",
|
|
807
|
-
"Cross-check only within the same file or function.",
|
|
808
|
-
"If a tool result is ambiguous, do one follow-up read; do not start a new chain.",
|
|
809
|
-
"Answer when the primary question is resolved — do not preemptively extend scope.",
|
|
810
|
-
];
|
|
811
|
-
break;
|
|
812
|
-
case "high":
|
|
813
|
-
header = "You are in **high** effort mode — careful work.";
|
|
814
|
-
bullets = [
|
|
815
|
-
"Multi-file exploration is expected when the question spans modules.",
|
|
816
|
-
"Verify assumptions with a second tool call (grep + read) before asserting.",
|
|
817
|
-
"Surface uncertainties explicitly rather than papering over them.",
|
|
818
|
-
"Still bias toward shipping an answer; do not spelunk indefinitely.",
|
|
819
|
-
];
|
|
820
|
-
break;
|
|
821
|
-
case "xhigh":
|
|
822
|
-
header = "You are in **xhigh** effort mode — thorough investigation.";
|
|
823
|
-
bullets = [
|
|
824
|
-
"Survey the relevant surface broadly before drilling down.",
|
|
825
|
-
"Hold multiple hypotheses; rank them by evidence before committing.",
|
|
826
|
-
"Name edge cases and failure modes even if the happy path is clear.",
|
|
827
|
-
"Justify final claims with concrete code references (file + line).",
|
|
828
|
-
];
|
|
829
|
-
break;
|
|
830
|
-
case "max":
|
|
831
|
-
header = "You are in **max** effort mode — exhaustive analysis.";
|
|
832
|
-
bullets = [
|
|
833
|
-
"Read every related file; do not stop at the first plausible answer.",
|
|
834
|
-
"Enumerate all failure modes you can construct; analyze each.",
|
|
835
|
-
"Cross-verify with independent paths (grep + read + run, if applicable).",
|
|
836
|
-
"Consider writing or updating tests when behavior is non-trivial.",
|
|
837
|
-
];
|
|
838
|
-
break;
|
|
839
|
-
default:
|
|
840
|
-
return undefined;
|
|
841
|
-
}
|
|
842
|
-
return ["## Working mode", header, ...bullets.map((b) => `- ${b}`)].join("\n");
|
|
843
|
-
}
|
|
844
|
-
function mapUsage(u) {
|
|
845
|
-
const out = {
|
|
846
|
-
inputTokens: u.input_tokens,
|
|
847
|
-
outputTokens: u.output_tokens,
|
|
848
|
-
};
|
|
849
|
-
if (u.cache_creation_input_tokens !== undefined) {
|
|
850
|
-
out.cacheCreationInputTokens = u.cache_creation_input_tokens;
|
|
851
|
-
}
|
|
852
|
-
if (u.cache_read_input_tokens !== undefined) {
|
|
853
|
-
out.cacheReadInputTokens = u.cache_read_input_tokens;
|
|
854
|
-
}
|
|
855
|
-
return out;
|
|
856
|
-
}
|
|
857
|
-
//# sourceMappingURL=anthropic.js.map
|