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.
Files changed (73) hide show
  1. package/README.md +55 -62
  2. package/dist/core/loop.d.ts.map +1 -1
  3. package/dist/core/loop.js +5 -43
  4. package/dist/core/loop.js.map +1 -1
  5. package/dist/index.d.ts +1 -5
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/memory/aux-model-map.d.ts +2 -43
  10. package/dist/memory/aux-model-map.d.ts.map +1 -1
  11. package/dist/memory/aux-model-map.js +3 -85
  12. package/dist/memory/aux-model-map.js.map +1 -1
  13. package/dist/platform/config.d.ts +0 -7
  14. package/dist/platform/config.d.ts.map +1 -1
  15. package/dist/platform/config.js +0 -14
  16. package/dist/platform/config.js.map +1 -1
  17. package/dist/platform/version.d.ts +1 -1
  18. package/dist/platform/version.js +1 -1
  19. package/dist/provider.d.ts +12 -55
  20. package/dist/provider.d.ts.map +1 -1
  21. package/dist/provider.js +99 -156
  22. package/dist/provider.js.map +1 -1
  23. package/dist/registry.d.ts +6 -39
  24. package/dist/registry.d.ts.map +1 -1
  25. package/dist/registry.js +9 -76
  26. package/dist/registry.js.map +1 -1
  27. package/dist/sub-agent/runner.d.ts.map +1 -1
  28. package/dist/sub-agent/runner.js +0 -3
  29. package/dist/sub-agent/runner.js.map +1 -1
  30. package/dist/tools/builtin/bash.js +2 -2
  31. package/dist/tools/builtin/bash.js.map +1 -1
  32. package/dist/tools/builtin/bash_background.js +1 -1
  33. package/dist/tools/builtin/bash_background.js.map +1 -1
  34. package/dist/tools/builtin/gemini_image_qa.d.ts +12 -0
  35. package/dist/tools/builtin/gemini_image_qa.d.ts.map +1 -0
  36. package/dist/tools/builtin/gemini_image_qa.js +195 -0
  37. package/dist/tools/builtin/gemini_image_qa.js.map +1 -0
  38. package/dist/tools/builtin/read.d.ts.map +1 -1
  39. package/dist/tools/builtin/read.js +6 -6
  40. package/dist/tools/builtin/read.js.map +1 -1
  41. package/dist/tools/index.d.ts +1 -0
  42. package/dist/tools/index.d.ts.map +1 -1
  43. package/dist/tools/index.js +1 -0
  44. package/dist/tools/index.js.map +1 -1
  45. package/dist/types.d.ts +1 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js +2 -2
  48. package/dist/types.js.map +1 -1
  49. package/package.json +2 -24
  50. package/dist/providers/anthropic.d.ts +0 -328
  51. package/dist/providers/anthropic.d.ts.map +0 -1
  52. package/dist/providers/anthropic.js +0 -857
  53. package/dist/providers/anthropic.js.map +0 -1
  54. package/dist/providers/codex-auth.d.ts +0 -149
  55. package/dist/providers/codex-auth.d.ts.map +0 -1
  56. package/dist/providers/codex-auth.js +0 -332
  57. package/dist/providers/codex-auth.js.map +0 -1
  58. package/dist/providers/codex-stream.d.ts +0 -42
  59. package/dist/providers/codex-stream.d.ts.map +0 -1
  60. package/dist/providers/codex-stream.js +0 -345
  61. package/dist/providers/codex-stream.js.map +0 -1
  62. package/dist/providers/codex-translators.d.ts +0 -105
  63. package/dist/providers/codex-translators.d.ts.map +0 -1
  64. package/dist/providers/codex-translators.js +0 -244
  65. package/dist/providers/codex-translators.js.map +0 -1
  66. package/dist/providers/codex.d.ts +0 -234
  67. package/dist/providers/codex.d.ts.map +0 -1
  68. package/dist/providers/codex.js +0 -461
  69. package/dist/providers/codex.js.map +0 -1
  70. package/dist/providers/fallback.d.ts +0 -71
  71. package/dist/providers/fallback.d.ts.map +0 -1
  72. package/dist/providers/fallback.js +0 -223
  73. 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