@loopingai/core 0.5.0 → 0.6.0

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 (304) hide show
  1. package/README.md +6 -4
  2. package/dist/a2a/agent-stub.d.ts +0 -1
  3. package/dist/a2a/agent-stub.js +0 -1
  4. package/dist/a2a/caller.d.ts +0 -1
  5. package/dist/a2a/caller.js +0 -1
  6. package/dist/a2a/card.d.ts +0 -1
  7. package/dist/a2a/card.js +0 -1
  8. package/dist/a2a/context.d.ts +0 -1
  9. package/dist/a2a/context.js +0 -1
  10. package/dist/a2a/executor.d.ts +0 -1
  11. package/dist/a2a/executor.js +0 -1
  12. package/dist/a2a/index.d.ts +0 -1
  13. package/dist/a2a/index.js +0 -1
  14. package/dist/a2a/notify.d.ts +4 -4
  15. package/dist/a2a/notify.js +4 -4
  16. package/dist/a2a/parts.d.ts +0 -1
  17. package/dist/a2a/parts.js +0 -1
  18. package/dist/a2a/push.d.ts +0 -1
  19. package/dist/a2a/push.js +0 -1
  20. package/dist/a2a/task-store.d.ts +0 -1
  21. package/dist/a2a/task-store.js +0 -1
  22. package/dist/a2a/task.d.ts +0 -1
  23. package/dist/a2a/task.js +0 -1
  24. package/dist/a2a/verify.d.ts +0 -1
  25. package/dist/a2a/verify.js +0 -1
  26. package/dist/agent/anthropic/index.d.ts +15 -0
  27. package/dist/agent/anthropic/index.js +19 -0
  28. package/dist/agent/anthropic/language-model.d.ts +59 -0
  29. package/dist/agent/anthropic/language-model.js +442 -0
  30. package/dist/agent/anthropic/prompt.d.ts +84 -0
  31. package/dist/agent/anthropic/prompt.js +541 -0
  32. package/dist/agent/anthropic/runtime.d.ts +79 -0
  33. package/dist/agent/anthropic/runtime.js +130 -0
  34. package/dist/agent/budget.d.ts +0 -1
  35. package/dist/agent/budget.js +0 -1
  36. package/dist/agent/control.d.ts +0 -1
  37. package/dist/agent/control.js +10 -10
  38. package/dist/agent/errors.d.ts +85 -0
  39. package/dist/agent/errors.js +64 -0
  40. package/dist/agent/final-reply.d.ts +14 -14
  41. package/dist/agent/final-reply.js +28 -12
  42. package/dist/agent/history.d.ts +3 -4
  43. package/dist/agent/history.js +2 -3
  44. package/dist/agent/index.d.ts +4 -3
  45. package/dist/agent/index.js +4 -3
  46. package/dist/agent/inference.d.ts +58 -2
  47. package/dist/agent/inference.js +44 -1
  48. package/dist/agent/model.d.ts +42 -26
  49. package/dist/agent/model.js +1 -49
  50. package/dist/agent/session.d.ts +6 -8
  51. package/dist/agent/session.js +3 -4
  52. package/dist/agent/workers-ai/index.d.ts +23 -0
  53. package/dist/agent/workers-ai/index.js +23 -0
  54. package/dist/agent/workers-ai/runtime.d.ts +42 -0
  55. package/dist/agent/workers-ai/runtime.js +63 -0
  56. package/dist/config.d.ts +49 -16
  57. package/dist/config.js +30 -2
  58. package/dist/contract/index.d.ts +0 -1
  59. package/dist/contract/index.js +0 -1
  60. package/dist/contract/plugin.d.ts +63 -4
  61. package/dist/contract/plugin.js +76 -1
  62. package/dist/contract/recipe.d.ts +16 -18
  63. package/dist/contract/recipe.js +0 -1
  64. package/dist/contract/validation.d.ts +0 -1
  65. package/dist/contract/validation.js +0 -1
  66. package/dist/db/db.d.ts +0 -2
  67. package/dist/db/db.js +0 -1
  68. package/dist/db/index.d.ts +0 -1
  69. package/dist/db/index.js +0 -1
  70. package/dist/db/migrations/index.d.ts +0 -1
  71. package/dist/db/migrations/index.js +8 -2
  72. package/dist/db/models/subtasks.d.ts +24 -26
  73. package/dist/db/models/subtasks.js +33 -77
  74. package/dist/db/models/tasks.d.ts +0 -1
  75. package/dist/db/models/tasks.js +0 -1
  76. package/dist/db/schema.d.ts +2 -22
  77. package/dist/db/schema.js +2 -5
  78. package/dist/env.d.ts +0 -1
  79. package/dist/env.js +0 -1
  80. package/dist/host/agent.d.ts +58 -5
  81. package/dist/host/agent.js +63 -10
  82. package/dist/host/index.d.ts +0 -1
  83. package/dist/host/index.js +0 -1
  84. package/dist/host/plugin-host.d.ts +0 -1
  85. package/dist/host/plugin-host.js +0 -1
  86. package/dist/index.d.ts +2 -3
  87. package/dist/index.js +2 -3
  88. package/dist/platform.d.ts +74 -12
  89. package/dist/platform.js +76 -14
  90. package/dist/round/agent.d.ts +36 -32
  91. package/dist/round/agent.js +61 -90
  92. package/dist/round/index.d.ts +3 -3
  93. package/dist/round/index.js +2 -3
  94. package/dist/round/policy.d.ts +2 -3
  95. package/dist/round/policy.js +0 -1
  96. package/dist/round/subagent.d.ts +19 -2
  97. package/dist/round/subagent.js +22 -6
  98. package/dist/round/turn.d.ts +32 -14
  99. package/dist/round/turn.js +83 -17
  100. package/dist/round/workflow.d.ts +30 -9
  101. package/dist/round/workflow.js +146 -78
  102. package/dist/runtime/index.d.ts +4 -3
  103. package/dist/runtime/index.js +6 -1
  104. package/dist/runtime/tool-families.d.ts +0 -1
  105. package/dist/runtime/tool-families.js +0 -1
  106. package/dist/subagent/fingerprint.d.ts +2 -3
  107. package/dist/subagent/fingerprint.js +8 -18
  108. package/dist/subagent/index.d.ts +6 -5
  109. package/dist/subagent/index.js +8 -7
  110. package/dist/subagent/prompt.d.ts +4 -6
  111. package/dist/subagent/prompt.js +0 -9
  112. package/dist/subagent/run.d.ts +8 -2
  113. package/dist/subagent/run.js +59 -10
  114. package/dist/subagent/workspace.d.ts +0 -1
  115. package/dist/subagent/workspace.js +0 -1
  116. package/dist/subtasks/catalog.d.ts +1 -2
  117. package/dist/subtasks/catalog.js +1 -2
  118. package/dist/subtasks/decomposition.d.ts +16 -21
  119. package/dist/subtasks/decomposition.js +27 -76
  120. package/dist/subtasks/delegate.d.ts +20 -2
  121. package/dist/subtasks/delegate.js +21 -17
  122. package/dist/subtasks/index.d.ts +1 -3
  123. package/dist/subtasks/index.js +1 -3
  124. package/dist/subtasks/subtask-types.d.ts +0 -9
  125. package/dist/subtasks/subtask-types.js +0 -8
  126. package/dist/subtasks/types.d.ts +45 -71
  127. package/dist/subtasks/types.js +0 -1
  128. package/dist/testing/auth.d.ts +0 -1
  129. package/dist/testing/auth.js +0 -1
  130. package/dist/testing/do.d.ts +0 -1
  131. package/dist/testing/do.js +0 -1
  132. package/dist/testing/fake-session.d.ts +0 -1
  133. package/dist/testing/fake-session.js +0 -1
  134. package/dist/testing/fixtures.d.ts +0 -1
  135. package/dist/testing/fixtures.js +0 -1
  136. package/dist/testing/harness.d.ts +0 -1
  137. package/dist/testing/harness.js +0 -1
  138. package/dist/testing/index.d.ts +0 -1
  139. package/dist/testing/index.js +0 -1
  140. package/dist/testing/mock-model.d.ts +35 -1
  141. package/dist/testing/mock-model.js +75 -1
  142. package/dist/testing/node.d.ts +0 -1
  143. package/dist/testing/node.js +0 -1
  144. package/dist/testing/vcr-global-setup.d.ts +1 -4
  145. package/dist/testing/vcr-global-setup.js +1 -4
  146. package/dist/testing/vcr-shared.d.ts +0 -1
  147. package/dist/testing/vcr-shared.js +0 -1
  148. package/dist/testing/vcr-spec.d.ts +0 -1
  149. package/dist/testing/vcr-spec.js +0 -1
  150. package/dist/testing/vcr-store.d.ts +0 -1
  151. package/dist/testing/vcr-store.js +0 -1
  152. package/dist/testing/vcr.d.ts +0 -1
  153. package/dist/testing/vcr.js +0 -1
  154. package/dist/worker/define-agent.d.ts +0 -1
  155. package/dist/worker/define-agent.js +0 -1
  156. package/dist/worker/index.d.ts +5 -13
  157. package/dist/worker/index.js +5 -13
  158. package/package.json +20 -2
  159. package/dist/a2a/agent-stub.d.ts.map +0 -1
  160. package/dist/a2a/agent-stub.js.map +0 -1
  161. package/dist/a2a/caller.d.ts.map +0 -1
  162. package/dist/a2a/caller.js.map +0 -1
  163. package/dist/a2a/card.d.ts.map +0 -1
  164. package/dist/a2a/card.js.map +0 -1
  165. package/dist/a2a/context.d.ts.map +0 -1
  166. package/dist/a2a/context.js.map +0 -1
  167. package/dist/a2a/executor.d.ts.map +0 -1
  168. package/dist/a2a/executor.js.map +0 -1
  169. package/dist/a2a/index.d.ts.map +0 -1
  170. package/dist/a2a/index.js.map +0 -1
  171. package/dist/a2a/notify.d.ts.map +0 -1
  172. package/dist/a2a/notify.js.map +0 -1
  173. package/dist/a2a/parts.d.ts.map +0 -1
  174. package/dist/a2a/parts.js.map +0 -1
  175. package/dist/a2a/push.d.ts.map +0 -1
  176. package/dist/a2a/push.js.map +0 -1
  177. package/dist/a2a/task-store.d.ts.map +0 -1
  178. package/dist/a2a/task-store.js.map +0 -1
  179. package/dist/a2a/task.d.ts.map +0 -1
  180. package/dist/a2a/task.js.map +0 -1
  181. package/dist/a2a/verify.d.ts.map +0 -1
  182. package/dist/a2a/verify.js.map +0 -1
  183. package/dist/agent/budget.d.ts.map +0 -1
  184. package/dist/agent/budget.js.map +0 -1
  185. package/dist/agent/control.d.ts.map +0 -1
  186. package/dist/agent/control.js.map +0 -1
  187. package/dist/agent/final-reply.d.ts.map +0 -1
  188. package/dist/agent/final-reply.js.map +0 -1
  189. package/dist/agent/history.d.ts.map +0 -1
  190. package/dist/agent/history.js.map +0 -1
  191. package/dist/agent/index.d.ts.map +0 -1
  192. package/dist/agent/index.js.map +0 -1
  193. package/dist/agent/inference.d.ts.map +0 -1
  194. package/dist/agent/inference.js.map +0 -1
  195. package/dist/agent/model.d.ts.map +0 -1
  196. package/dist/agent/model.js.map +0 -1
  197. package/dist/agent/session.d.ts.map +0 -1
  198. package/dist/agent/session.js.map +0 -1
  199. package/dist/config.d.ts.map +0 -1
  200. package/dist/config.js.map +0 -1
  201. package/dist/contract/index.d.ts.map +0 -1
  202. package/dist/contract/index.js.map +0 -1
  203. package/dist/contract/plugin.d.ts.map +0 -1
  204. package/dist/contract/plugin.js.map +0 -1
  205. package/dist/contract/recipe.d.ts.map +0 -1
  206. package/dist/contract/recipe.js.map +0 -1
  207. package/dist/contract/validation.d.ts.map +0 -1
  208. package/dist/contract/validation.js.map +0 -1
  209. package/dist/db/db.d.ts.map +0 -1
  210. package/dist/db/db.js.map +0 -1
  211. package/dist/db/index.d.ts.map +0 -1
  212. package/dist/db/index.js.map +0 -1
  213. package/dist/db/migrations/index.d.ts.map +0 -1
  214. package/dist/db/migrations/index.js.map +0 -1
  215. package/dist/db/models/subtasks.d.ts.map +0 -1
  216. package/dist/db/models/subtasks.js.map +0 -1
  217. package/dist/db/models/tasks.d.ts.map +0 -1
  218. package/dist/db/models/tasks.js.map +0 -1
  219. package/dist/db/schema.d.ts.map +0 -1
  220. package/dist/db/schema.js.map +0 -1
  221. package/dist/env.d.ts.map +0 -1
  222. package/dist/env.js.map +0 -1
  223. package/dist/host/agent.d.ts.map +0 -1
  224. package/dist/host/agent.js.map +0 -1
  225. package/dist/host/index.d.ts.map +0 -1
  226. package/dist/host/index.js.map +0 -1
  227. package/dist/host/plugin-host.d.ts.map +0 -1
  228. package/dist/host/plugin-host.js.map +0 -1
  229. package/dist/index.d.ts.map +0 -1
  230. package/dist/index.js.map +0 -1
  231. package/dist/platform.d.ts.map +0 -1
  232. package/dist/platform.js.map +0 -1
  233. package/dist/round/agent.d.ts.map +0 -1
  234. package/dist/round/agent.js.map +0 -1
  235. package/dist/round/index.d.ts.map +0 -1
  236. package/dist/round/index.js.map +0 -1
  237. package/dist/round/policy.d.ts.map +0 -1
  238. package/dist/round/policy.js.map +0 -1
  239. package/dist/round/subagent.d.ts.map +0 -1
  240. package/dist/round/subagent.js.map +0 -1
  241. package/dist/round/turn.d.ts.map +0 -1
  242. package/dist/round/turn.js.map +0 -1
  243. package/dist/round/workflow.d.ts.map +0 -1
  244. package/dist/round/workflow.js.map +0 -1
  245. package/dist/runtime/index.d.ts.map +0 -1
  246. package/dist/runtime/index.js.map +0 -1
  247. package/dist/runtime/tool-families.d.ts.map +0 -1
  248. package/dist/runtime/tool-families.js.map +0 -1
  249. package/dist/subagent/fingerprint.d.ts.map +0 -1
  250. package/dist/subagent/fingerprint.js.map +0 -1
  251. package/dist/subagent/index.d.ts.map +0 -1
  252. package/dist/subagent/index.js.map +0 -1
  253. package/dist/subagent/prompt.d.ts.map +0 -1
  254. package/dist/subagent/prompt.js.map +0 -1
  255. package/dist/subagent/run.d.ts.map +0 -1
  256. package/dist/subagent/run.js.map +0 -1
  257. package/dist/subagent/workspace.d.ts.map +0 -1
  258. package/dist/subagent/workspace.js.map +0 -1
  259. package/dist/subtasks/catalog.d.ts.map +0 -1
  260. package/dist/subtasks/catalog.js.map +0 -1
  261. package/dist/subtasks/decomposition.d.ts.map +0 -1
  262. package/dist/subtasks/decomposition.js.map +0 -1
  263. package/dist/subtasks/delegate.d.ts.map +0 -1
  264. package/dist/subtasks/delegate.js.map +0 -1
  265. package/dist/subtasks/index.d.ts.map +0 -1
  266. package/dist/subtasks/index.js.map +0 -1
  267. package/dist/subtasks/scheduler.d.ts +0 -49
  268. package/dist/subtasks/scheduler.d.ts.map +0 -1
  269. package/dist/subtasks/scheduler.js +0 -48
  270. package/dist/subtasks/scheduler.js.map +0 -1
  271. package/dist/subtasks/subtask-types.d.ts.map +0 -1
  272. package/dist/subtasks/subtask-types.js.map +0 -1
  273. package/dist/subtasks/types.d.ts.map +0 -1
  274. package/dist/subtasks/types.js.map +0 -1
  275. package/dist/testing/auth.d.ts.map +0 -1
  276. package/dist/testing/auth.js.map +0 -1
  277. package/dist/testing/do.d.ts.map +0 -1
  278. package/dist/testing/do.js.map +0 -1
  279. package/dist/testing/fake-session.d.ts.map +0 -1
  280. package/dist/testing/fake-session.js.map +0 -1
  281. package/dist/testing/fixtures.d.ts.map +0 -1
  282. package/dist/testing/fixtures.js.map +0 -1
  283. package/dist/testing/harness.d.ts.map +0 -1
  284. package/dist/testing/harness.js.map +0 -1
  285. package/dist/testing/index.d.ts.map +0 -1
  286. package/dist/testing/index.js.map +0 -1
  287. package/dist/testing/mock-model.d.ts.map +0 -1
  288. package/dist/testing/mock-model.js.map +0 -1
  289. package/dist/testing/node.d.ts.map +0 -1
  290. package/dist/testing/node.js.map +0 -1
  291. package/dist/testing/vcr-global-setup.d.ts.map +0 -1
  292. package/dist/testing/vcr-global-setup.js.map +0 -1
  293. package/dist/testing/vcr-shared.d.ts.map +0 -1
  294. package/dist/testing/vcr-shared.js.map +0 -1
  295. package/dist/testing/vcr-spec.d.ts.map +0 -1
  296. package/dist/testing/vcr-spec.js.map +0 -1
  297. package/dist/testing/vcr-store.d.ts.map +0 -1
  298. package/dist/testing/vcr-store.js.map +0 -1
  299. package/dist/testing/vcr.d.ts.map +0 -1
  300. package/dist/testing/vcr.js.map +0 -1
  301. package/dist/worker/define-agent.d.ts.map +0 -1
  302. package/dist/worker/define-agent.js.map +0 -1
  303. package/dist/worker/index.d.ts.map +0 -1
  304. package/dist/worker/index.js.map +0 -1
@@ -0,0 +1,541 @@
1
+ /**
2
+ * The AI SDK prompt → Anthropic Messages request mapping.
3
+ *
4
+ * Split out from the model itself because it is pure: prompt in, request body
5
+ * plus warnings out, no client and no I/O. That is what makes the sharp edges
6
+ * below testable without a network or a cassette, and every one of them is a
7
+ * silent-corruption bug rather than a loud one.
8
+ *
9
+ * Three shape mismatches do the real damage if you get them wrong:
10
+ *
11
+ * 1. **There is no `tool` role in the Messages API.** The AI SDK models tool
12
+ * results as their own role; Anthropic carries them as `tool_result` blocks
13
+ * inside a **user** turn.
14
+ * 2. **`thinking` blocks are signed and must be replayed byte-for-byte.** Opus 5
15
+ * runs adaptive thinking by default and rejects a modified block. Core's loop
16
+ * is multi-turn tool use on every round, so it replays the previous assistant
17
+ * message every single time — drop or rebuild the signature and round two of
18
+ * every task 400s.
19
+ * 3. **`input` is a parsed value on the way in and a JSON string on the way
20
+ * out.** `LanguageModelV4ToolCallPart.input` (prompt side) is `unknown`;
21
+ * `LanguageModelV4ToolCall.input` (result side) is `string`. Mixing them up
22
+ * type-checks and corrupts silently.
23
+ */
24
+ /** Namespaced provider key for anything we stash on a part. */
25
+ export const ANTHROPIC_PROVIDER = "anthropic";
26
+ /** Anything Anthropic will not accept in a `tool_use.id`. */
27
+ const UNSAFE_TOOL_CALL_ID = /[^a-zA-Z0-9_-]/g;
28
+ /**
29
+ * A tool-call id Anthropic will accept, from one it might not.
30
+ *
31
+ * The API validates this field against `^[a-zA-Z0-9_-]+$` and answers a
32
+ * violation with a `400 invalid_request_error` naming the exact message index —
33
+ * `messages.7.content.1.tool_use.id`. That is a *deterministic* failure on
34
+ * replayed history, which makes it the worst kind: every retry and every
35
+ * fallback re-sends the same poisoned message list and fails identically, so the
36
+ * round burns its whole ladder without a single attempt that could have worked.
37
+ * It cost a production task exactly that way.
38
+ *
39
+ * Applied to both sides of the pair — the assistant's `tool_use.id` and the
40
+ * user turn's `tool_result.tool_use_id` — because the two must agree or the
41
+ * request is malformed in a second, more confusing way. Since the mapping is a
42
+ * pure function of the id, applying it independently at both call sites lands on
43
+ * the same answer without threading state between them.
44
+ *
45
+ * This is a **backstop**, not the fix. Core's own generators emit safe ids
46
+ * directly (see {@link file://../../subtasks/delegate.ts delegateToolCallId});
47
+ * this catches the next generator someone writes, and any id that reaches the
48
+ * adapter from outside core.
49
+ *
50
+ * Not injective, and deliberately not made so: two ids differing only in
51
+ * unsafe characters collapse together. Adding a hash to avoid that would change
52
+ * every id to guard against a collision that requires two synthetic calls in one
53
+ * request whose ids differ *only* in punctuation. Provider-legible ids are worth
54
+ * more than that.
55
+ */
56
+ export function providerSafeToolCallId(id) {
57
+ return id.replace(UNSAFE_TOOL_CALL_ID, "_");
58
+ }
59
+ /**
60
+ * Anthropic's cache marker.
61
+ *
62
+ * `{type:"ephemeral"}` is the *cache-on* switch and the only `cache_control`
63
+ * type there is — "ephemeral" describes the entry's lifetime, not its value.
64
+ * Omitting the field is what disables caching.
65
+ */
66
+ function cacheControl(ttl) {
67
+ return ttl === "1h"
68
+ ? { type: "ephemeral", ttl: "1h" }
69
+ : { type: "ephemeral" };
70
+ }
71
+ /**
72
+ * At most four `cache_control` breakpoints are allowed per request, and they are
73
+ * spent stability-first because the cache is a **prefix** match and the render
74
+ * order is `tools` → `system` → `messages`:
75
+ *
76
+ * - one on the last tool (frozen for the agent's lifetime)
77
+ * - one on the last system block (frozen)
78
+ * - up to two rolling ones in the conversation
79
+ *
80
+ * The second rolling breakpoint is not redundant. Each breakpoint walks back at
81
+ * most **20 content blocks** looking for a live entry, and one busy round of an
82
+ * agent that calls tools in a loop adds a `tool_use`/`tool_result` pair per
83
+ * call — well past 20 in a single turn. Without an anchor partway back, the next
84
+ * request finds nothing, silently misses, and re-bills the entire prefix with no
85
+ * error to notice.
86
+ */
87
+ const MESSAGE_BREAKPOINT_STRIDE = 15;
88
+ /** Which content blocks a `file` part can become, by media type. */
89
+ function fileBlock(data, mediaType, warnings) {
90
+ const unsupported = (details) => {
91
+ warnings.push({ type: "unsupported", feature: "file-part", details });
92
+ return undefined;
93
+ };
94
+ if (data.type === "reference")
95
+ return unsupported("provider file references are not supported");
96
+ if (data.type === "text")
97
+ return { type: "text", text: data.text };
98
+ const isImage = mediaType.startsWith("image/");
99
+ const isPdf = mediaType === "application/pdf";
100
+ if (!isImage && !isPdf)
101
+ return unsupported(`unsupported media type "${mediaType}"`);
102
+ if (data.type === "url") {
103
+ const url = data.url.toString();
104
+ return isImage
105
+ ? { type: "image", source: { type: "url", url } }
106
+ : { type: "document", source: { type: "url", url } };
107
+ }
108
+ // `data` is raw bytes or an already-base64 string. Anthropic wants base64.
109
+ const base64 = typeof data.data === "string" ? data.data : bytesToBase64(data.data);
110
+ return isImage
111
+ ? {
112
+ type: "image",
113
+ source: {
114
+ type: "base64",
115
+ media_type: mediaType,
116
+ data: base64
117
+ }
118
+ }
119
+ : {
120
+ type: "document",
121
+ source: { type: "base64", media_type: "application/pdf", data: base64 }
122
+ };
123
+ }
124
+ /**
125
+ * Base64 without `Buffer`.
126
+ *
127
+ * Core targets workerd, where `Buffer` only exists under `nodejs_compat` and
128
+ * core must not assume a consumer enabled it. Chunked so a large file cannot
129
+ * blow the argument limit of `String.fromCharCode`.
130
+ */
131
+ function bytesToBase64(bytes) {
132
+ let binary = "";
133
+ const chunk = 0x8000;
134
+ for (let i = 0; i < bytes.length; i += chunk) {
135
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
136
+ }
137
+ return btoa(binary);
138
+ }
139
+ /**
140
+ * Read a signature previously stashed on a reasoning part.
141
+ *
142
+ * Written by the response mapper into `providerMetadata.anthropic.signature`;
143
+ * the SDK hands it back as `providerOptions.anthropic.signature`. Both halves
144
+ * have to agree exactly — this is the single most load-bearing line in the file.
145
+ */
146
+ function signatureOf(providerOptions) {
147
+ const own = providerOptions?.[ANTHROPIC_PROVIDER];
148
+ const signature = own?.["signature"];
149
+ return typeof signature === "string" ? signature : undefined;
150
+ }
151
+ /** `redacted_thinking` round-trips as opaque data, same contract as a signature. */
152
+ function redactedDataOf(providerOptions) {
153
+ const own = providerOptions?.[ANTHROPIC_PROVIDER];
154
+ const data = own?.["redactedData"];
155
+ return typeof data === "string" ? data : undefined;
156
+ }
157
+ /** One AI SDK message → zero or more Anthropic messages (before merging). */
158
+ function mapMessage(message, warnings) {
159
+ switch (message.role) {
160
+ case "system":
161
+ // Handled by the caller — system text is a top-level field, not a turn.
162
+ return undefined;
163
+ case "user": {
164
+ const content = [];
165
+ for (const part of message.content) {
166
+ if (part.type === "text") {
167
+ if (part.text.length > 0)
168
+ content.push({ type: "text", text: part.text });
169
+ }
170
+ else {
171
+ const block = fileBlock(part.data, part.mediaType, warnings);
172
+ if (block)
173
+ content.push(block);
174
+ }
175
+ }
176
+ return content.length > 0 ? { role: "user", content } : undefined;
177
+ }
178
+ case "assistant": {
179
+ const content = [];
180
+ for (const part of message.content) {
181
+ switch (part.type) {
182
+ case "text":
183
+ // Anthropic rejects an empty text block; the SDK emits them freely.
184
+ if (part.text.length > 0)
185
+ content.push({ type: "text", text: part.text });
186
+ break;
187
+ case "reasoning": {
188
+ const redactedData = redactedDataOf(part.providerOptions);
189
+ if (redactedData !== undefined) {
190
+ content.push({ type: "redacted_thinking", data: redactedData });
191
+ break;
192
+ }
193
+ const signature = signatureOf(part.providerOptions);
194
+ // A thinking block without its signature is rejected outright, and
195
+ // inventing one is worse. Dropping it is the only safe move — but
196
+ // say so, because it means the response mapper lost the metadata.
197
+ if (signature === undefined) {
198
+ warnings.push({
199
+ type: "other",
200
+ message: "dropped a reasoning part with no Anthropic signature; " +
201
+ "thinking blocks cannot be replayed without one"
202
+ });
203
+ break;
204
+ }
205
+ content.push({ type: "thinking", thinking: part.text, signature });
206
+ break;
207
+ }
208
+ case "tool-call":
209
+ content.push({
210
+ type: "tool_use",
211
+ id: providerSafeToolCallId(part.toolCallId),
212
+ name: part.toolName,
213
+ // Prompt side: already a parsed value. Do NOT stringify.
214
+ input: part.input
215
+ });
216
+ break;
217
+ case "tool-result":
218
+ // Provider-executed results replayed on the assistant turn. Core
219
+ // never produces these; pass through rather than silently drop.
220
+ warnings.push({
221
+ type: "unsupported",
222
+ feature: "assistant tool-result part",
223
+ details: `tool "${part.toolName}" result on an assistant turn was dropped`
224
+ });
225
+ break;
226
+ case "file": {
227
+ const block = fileBlock(part.data, part.mediaType, warnings);
228
+ if (block)
229
+ content.push(block);
230
+ break;
231
+ }
232
+ default:
233
+ warnings.push({
234
+ type: "unsupported",
235
+ feature: `assistant part "${part.type}"`
236
+ });
237
+ }
238
+ }
239
+ return content.length > 0 ? { role: "assistant", content } : undefined;
240
+ }
241
+ case "tool": {
242
+ // The mapping that most often goes wrong: Anthropic has no `tool` role.
243
+ // Tool results ride in a *user* turn as `tool_result` blocks.
244
+ const content = [];
245
+ for (const part of message.content) {
246
+ if (part.type !== "tool-result") {
247
+ warnings.push({
248
+ type: "unsupported",
249
+ feature: `tool part "${part.type}"`
250
+ });
251
+ continue;
252
+ }
253
+ const { text, isError } = toolResultText(part.output);
254
+ content.push({
255
+ type: "tool_result",
256
+ // The same mapping as the `tool_use.id` above, and it has to be: a
257
+ // result whose id no longer matches its call is a malformed request.
258
+ tool_use_id: providerSafeToolCallId(part.toolCallId),
259
+ content: text,
260
+ ...(isError ? { is_error: true } : {})
261
+ });
262
+ }
263
+ return content.length > 0 ? { role: "user", content } : undefined;
264
+ }
265
+ }
266
+ }
267
+ /** Flatten a V4 tool result into the string Anthropic carries, plus its error flag. */
268
+ function toolResultText(output) {
269
+ switch (output.type) {
270
+ case "text":
271
+ return { text: output.value, isError: false };
272
+ case "error-text":
273
+ return { text: output.value, isError: true };
274
+ case "json":
275
+ return { text: JSON.stringify(output.value), isError: false };
276
+ case "error-json":
277
+ return { text: JSON.stringify(output.value), isError: true };
278
+ case "execution-denied":
279
+ return {
280
+ text: output.reason ?? "The tool call was denied.",
281
+ isError: true
282
+ };
283
+ case "content":
284
+ return {
285
+ text: output.value
286
+ .map((part) => (part.type === "text" ? part.text : `[${part.type}]`))
287
+ .join("\n"),
288
+ isError: false
289
+ };
290
+ }
291
+ }
292
+ /**
293
+ * Merge consecutive same-role turns.
294
+ *
295
+ * Cheap insurance rather than a hard requirement: the mapping already produces
296
+ * alternating turns for the shapes core generates (an assistant turn of
297
+ * `tool_use` followed by a `tool` message that becomes a user turn). But a
298
+ * caller is free to emit two user messages in a row, and merging is always
299
+ * valid, so the adapter never has to care which API version tightened the rule.
300
+ */
301
+ function mergeAdjacent(messages) {
302
+ const merged = [];
303
+ for (const message of messages) {
304
+ const previous = merged[merged.length - 1];
305
+ if (previous && previous.role === message.role) {
306
+ previous.content = hoistToolResults([
307
+ ...previous.content,
308
+ ...message.content
309
+ ]);
310
+ continue;
311
+ }
312
+ merged.push({
313
+ role: message.role,
314
+ content: [...message.content]
315
+ });
316
+ }
317
+ return merged;
318
+ }
319
+ /**
320
+ * `tool_result` blocks must lead the user turn that answers a `tool_use`.
321
+ *
322
+ * Only reachable via a merge — the mapping alone never mixes them with text —
323
+ * but a caller that puts a user message between an assistant's tool call and its
324
+ * results would otherwise produce `[text, tool_result]`, which the API rejects.
325
+ * Stable within each group, so the pairing with the preceding `tool_use` blocks
326
+ * is preserved.
327
+ */
328
+ function hoistToolResults(content) {
329
+ const results = content.filter((block) => block.type === "tool_result");
330
+ if (results.length === 0 || results.length === content.length)
331
+ return content;
332
+ return [
333
+ ...results,
334
+ ...content.filter((block) => block.type !== "tool_result")
335
+ ];
336
+ }
337
+ /**
338
+ * Give every `tool_use` a `tool_result`, inventing one where the transcript
339
+ * lost it.
340
+ *
341
+ * Anthropic rejects a request in which an assistant turn calls a tool and the
342
+ * next user turn does not answer it — every id, no exceptions. The mapping above
343
+ * can produce exactly that without anything looking wrong: a `tool` message
344
+ * whose parts were all unmappable maps to `undefined` and is dropped, taking the
345
+ * only `tool_result` for a still-present `tool_use` with it.
346
+ *
347
+ * Synthesising an error result is strictly better than the alternatives. Dropping
348
+ * the `tool_use` too would rewrite what the model actually did, and a `thinking`
349
+ * block signed over that turn would then no longer match. Saying "the result was
350
+ * lost" is true, is what the model would infer anyway, and keeps the turn
351
+ * replayable.
352
+ *
353
+ * **Scope: only a turn with something after it.** A conversation *ending* on an
354
+ * unanswered `tool_use` is a different shape — an assistant prefill — and core
355
+ * never produces one, since `generateText` appends results before the next call.
356
+ * Repairing it would mean appending a turn to a prompt the caller deliberately
357
+ * ended, on a guess about how the API treats it. Left alone until there is a
358
+ * reason to touch it.
359
+ */
360
+ function answerOrphanedToolUses(messages, warnings) {
361
+ const out = [];
362
+ for (let i = 0; i < messages.length; i += 1) {
363
+ const message = messages[i];
364
+ out.push(message);
365
+ if (message.role !== "assistant")
366
+ continue;
367
+ const blocks = message.content;
368
+ const ids = blocks
369
+ .filter((b) => b.type === "tool_use")
370
+ .map((b) => b.id);
371
+ if (ids.length === 0)
372
+ continue;
373
+ const next = messages[i + 1];
374
+ if (!next)
375
+ continue;
376
+ const answered = new Set(next.role === "user"
377
+ ? next.content
378
+ .filter((b) => b.type === "tool_result")
379
+ .map((b) => b.tool_use_id)
380
+ : []);
381
+ const missing = ids.filter((id) => !answered.has(id));
382
+ if (missing.length === 0)
383
+ continue;
384
+ warnings.push({
385
+ type: "other",
386
+ message: `${missing.length} tool call(s) had no result in the prompt; ` +
387
+ "an error result was synthesised so the request stays valid"
388
+ });
389
+ const repairs = missing.map((id) => ({
390
+ type: "tool_result",
391
+ tool_use_id: id,
392
+ content: "The result of this tool call was not preserved.",
393
+ is_error: true
394
+ }));
395
+ // Prepended, not appended: `tool_result` blocks must lead their turn, the
396
+ // same rule `hoistToolResults` enforces after a merge.
397
+ if (next.role === "user") {
398
+ next.content = [
399
+ ...repairs,
400
+ ...next.content
401
+ ];
402
+ }
403
+ else {
404
+ out.push({ role: "user", content: repairs });
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+ /**
410
+ * Place the rolling conversation breakpoints.
411
+ *
412
+ * Walks the flattened block sequence and marks the last block, plus one
413
+ * `MESSAGE_BREAKPOINT_STRIDE` back when the tail is long enough to outrun the
414
+ * 20-block lookback window. Mutates in place — the messages were already cloned
415
+ * by {@link mergeAdjacent}.
416
+ */
417
+ function placeMessageBreakpoints(messages, ttl) {
418
+ const blocks = [];
419
+ for (const message of messages) {
420
+ for (const block of message.content) {
421
+ blocks.push(block);
422
+ }
423
+ }
424
+ if (blocks.length === 0)
425
+ return;
426
+ const targets = new Set([blocks.length - 1]);
427
+ if (blocks.length > MESSAGE_BREAKPOINT_STRIDE + 1) {
428
+ targets.add(blocks.length - 1 - MESSAGE_BREAKPOINT_STRIDE);
429
+ }
430
+ for (const index of targets) {
431
+ const block = blocks[index];
432
+ // `thinking` and `redacted_thinking` reject cache_control; skip them rather
433
+ // than 400 the request, and let the neighbouring breakpoint carry the round.
434
+ if (!block ||
435
+ block.type === "thinking" ||
436
+ block.type === "redacted_thinking")
437
+ continue;
438
+ block.cache_control = cacheControl(ttl);
439
+ }
440
+ }
441
+ /** Map the prompt half of a call: system blocks + turns. */
442
+ export function mapPrompt(prompt, options) {
443
+ const warnings = [];
444
+ const ttl = options.cache === false ? undefined : (options.cache ?? "5m");
445
+ const systemText = prompt
446
+ .filter((message) => message.role === "system")
447
+ .map((message) => message.content)
448
+ .filter((text) => text.length > 0);
449
+ const system = systemText.length > 0
450
+ ? systemText.map((text, index) => ({
451
+ type: "text",
452
+ text,
453
+ // Frozen prefix: one breakpoint on the last block covers tools+system.
454
+ ...(ttl && index === systemText.length - 1
455
+ ? { cache_control: cacheControl(ttl) }
456
+ : {})
457
+ }))
458
+ : undefined;
459
+ const mapped = prompt
460
+ .map((message) => mapMessage(message, warnings))
461
+ .filter((message) => message !== undefined);
462
+ const messages = answerOrphanedToolUses(mergeAdjacent(mapped), warnings);
463
+ // Every mapping branch above drops a turn that maps to nothing, which is
464
+ // right per-message and wrong for the request: Anthropic requires the first
465
+ // turn to be `user`, so a leading user message whose only part was empty text
466
+ // leaves the conversation opening on `assistant` and 400s. Cheaper to notice
467
+ // here than in a log.
468
+ if (messages.length > 0 && messages[0].role !== "user") {
469
+ warnings.push({
470
+ type: "other",
471
+ message: "the first mapped turn was not a user turn; a placeholder was inserted " +
472
+ "because the Messages API requires the conversation to open on one"
473
+ });
474
+ messages.unshift({ role: "user", content: [{ type: "text", text: "…" }] });
475
+ }
476
+ if (ttl)
477
+ placeMessageBreakpoints(messages, ttl);
478
+ return { system, messages, warnings };
479
+ }
480
+ /** Map tool definitions, putting the frozen-prefix breakpoint on the last one. */
481
+ export function mapTools(tools, ttl, warnings) {
482
+ const functionTools = tools.filter((tool) => tool.type === "function");
483
+ for (const tool of tools) {
484
+ if (tool.type !== "function") {
485
+ warnings.push({
486
+ type: "unsupported",
487
+ feature: "provider-defined tool",
488
+ details: `tool "${tool.name}" was dropped; only function tools are mapped`
489
+ });
490
+ }
491
+ }
492
+ if (functionTools.length === 0)
493
+ return undefined;
494
+ return functionTools.map((tool, index) => ({
495
+ name: tool.name,
496
+ ...(tool.description ? { description: tool.description } : {}),
497
+ input_schema: tool.inputSchema,
498
+ ...(ttl && index === functionTools.length - 1
499
+ ? { cache_control: cacheControl(ttl) }
500
+ : {})
501
+ }));
502
+ }
503
+ /** Map tool choice. Note V4 uses `{type:"required"}`, not the bare string. */
504
+ export function mapToolChoice(toolChoice) {
505
+ if (!toolChoice)
506
+ return undefined;
507
+ switch (toolChoice.type) {
508
+ case "auto":
509
+ return { type: "auto" };
510
+ case "none":
511
+ return { type: "none" };
512
+ // Core sets this on every round: "you must call one of the control tools".
513
+ case "required":
514
+ return { type: "any" };
515
+ case "tool":
516
+ return { type: "tool", name: toolChoice.toolName };
517
+ }
518
+ }
519
+ /**
520
+ * Sampling parameters Claude Opus 5 and its siblings removed.
521
+ *
522
+ * Sending any of them is a 400, so they are dropped rather than forwarded.
523
+ * `warnings` is exactly the channel the spec provides for saying so — silently
524
+ * ignoring a caller's `temperature` would be the worse failure.
525
+ */
526
+ const REMOVED_SAMPLING_SETTINGS = [
527
+ "temperature",
528
+ "topP",
529
+ "topK",
530
+ "seed",
531
+ "presencePenalty",
532
+ "frequencyPenalty"
533
+ ];
534
+ export function collectSamplingWarnings(options) {
535
+ return REMOVED_SAMPLING_SETTINGS.filter((setting) => options[setting] !== undefined).map((setting) => ({
536
+ type: "unsupported",
537
+ feature: setting,
538
+ details: "Claude 4.7+ models removed sampling parameters; the value was dropped. " +
539
+ "Steer with the prompt, or with output_config.effort."
540
+ }));
541
+ }
@@ -0,0 +1,79 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type { ModelConfig } from "../../config.js";
3
+ import type { CredentialRejectedBy } from "../errors.js";
4
+ import type { ModelRuntime } from "../model.js";
5
+ import type { CacheTtl } from "./prompt.js";
6
+ /**
7
+ * A {@link ModelRuntime} backed by Claude, for agents whose work justifies it.
8
+ *
9
+ * `ModelRuntime` / `ModelPair` were already the right interface — the Workers AI
10
+ * implementation in {@link file://../model.ts model.ts} is one implementation of
11
+ * it, not the definition. This is a sibling, so an agent switches providers by
12
+ * overriding `modelRuntime()` and changes nothing else.
13
+ *
14
+ * Provider choice is per **agent**, not per deployment: the fleet keeps running
15
+ * on Workers AI and one tenant opts into Claude.
16
+ */
17
+ export interface AnthropicRuntimeDeps {
18
+ /**
19
+ * Base URL for the Messages API.
20
+ *
21
+ * A thunk because the useful value — `env.AI.gateway(id).getUrl("anthropic")`
22
+ * — is only obtainable once bindings exist. Routing through AI Gateway is what
23
+ * keeps Claude calls in the same logs as every Workers AI call; point it at
24
+ * `https://api.anthropic.com` to bypass the gateway.
25
+ */
26
+ baseUrl: () => string | Promise<string>;
27
+ /**
28
+ * The bearer credential, resolved lazily so a rotated secret is picked up by
29
+ * the next isolate without a redeploy.
30
+ *
31
+ * Async-capable, like {@link baseUrl}, because the credential is not always a
32
+ * stored secret. A deployment that puts an authenticated intermediary in
33
+ * front of Anthropic mints a short-lived token per request instead — signing
34
+ * is asynchronous, and a token with a lifetime measured in minutes cannot be
35
+ * captured once and reused, which is why the client is rebuilt per call
36
+ * rather than memoized. See the note above `clientFor`.
37
+ */
38
+ authToken: () => string | Promise<string>;
39
+ /**
40
+ * AI Gateway token, when the gateway has **Authenticated Gateway** enabled.
41
+ *
42
+ * Required in that case and only that case, which is why it is optional:
43
+ * pointing `baseUrl` at `api.anthropic.com`, or at a gateway with
44
+ * authentication off, must keep working with no token at all.
45
+ *
46
+ * Easy to miss, because the agents on Workers AI never need it — `env.AI.run()`
47
+ * reaches the gateway through the binding, which the platform authenticates.
48
+ * Only a client calling the **provider-native URL** presents credentials of
49
+ * its own, and a gateway with authentication on rejects it at the door: a
50
+ * `401` that never reaches Anthropic and never appears in the gateway's own
51
+ * call log, which makes it look like the model credential was refused.
52
+ */
53
+ gatewayToken?: () => string | undefined;
54
+ /** Model ids, gateway slug and output ceiling — the agent's resolved config. */
55
+ config: ModelConfig;
56
+ /**
57
+ * `output_config.effort`. Coding and agentic work wants `"xhigh"`; the API
58
+ * default is `"high"`. Core does not choose this — the agent does.
59
+ */
60
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
61
+ /**
62
+ * Prompt-cache TTL. `"1h"` costs 2x on write instead of 1.25x and is the right
63
+ * call for an agent whose rounds are separated by minutes of tool work — a
64
+ * container boot, an install, a test suite — because a 5-minute entry has
65
+ * expired by the next round and the whole prefix is re-billed at full price.
66
+ */
67
+ cache?: CacheTtl | false;
68
+ /**
69
+ * Recognise a deployment-specific authority in a rejected-credential body —
70
+ * notably the intermediary {@link gatewayToken}'s note describes, which fails
71
+ * with the same `401` as the gateway and the provider and has a completely
72
+ * different remedy. See
73
+ * {@link file://./language-model.ts AnthropicModelDeps.classifyAuthFailure}.
74
+ */
75
+ classifyAuthFailure?: (body: unknown) => CredentialRejectedBy | undefined;
76
+ /** Escape hatch for tests: use this instead of constructing a real client. */
77
+ clientOverride?: Anthropic;
78
+ }
79
+ export declare function createAnthropicModelRuntime(deps: AnthropicRuntimeDeps): ModelRuntime;