@visual-z/xeno 0.1.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 (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * The Anthropic adapter, and the interface every adapter implements.
3
+ *
4
+ * Everything the core produces is provider-neutral by construction, so the
5
+ * mapping lives here and nowhere else.
6
+ *
7
+ * `Provider` is an interface rather than a base class on purpose. Two adapters
8
+ * share a contract, not an implementation -- there is nothing for a base class
9
+ * to hold. Worse, a base class that constructs an Anthropic client drags the
10
+ * SDK into every subclass, including test fakes that never make a request,
11
+ * which is how an SDK dependency ends up in code that has nothing to do with
12
+ * Anthropic.
13
+ *
14
+ * Three rules this file exists to honour, all of which would otherwise be
15
+ * discovered as production failures:
16
+ *
17
+ * THINKING BLOCKS REPLAY VERBATIM. The API rejects blocks whose content was
18
+ * modified, and a block whose text is empty still has to go back unchanged.
19
+ * The core therefore carries the raw block opaquely and this file passes it
20
+ * straight through -- it never reconstructs one from text.
21
+ *
22
+ * THE FROZEN PREFIX CARRIES THE CACHE BREAKPOINT. System and tools render at
23
+ * the front of the prompt, so one explicit breakpoint on the system block
24
+ * gives the expensive shared part a read point that survives whatever
25
+ * happens later in messages. Top-level auto-caching then handles the growing
26
+ * tail.
27
+ *
28
+ * EVERY RESPONSE IS MEASURED. Usage comes back on each call and is returned
29
+ * alongside the message, because a cache that silently stopped working costs
30
+ * money without ever raising an error.
31
+ */
32
+
33
+ import Anthropic from "@anthropic-ai/sdk";
34
+ import type { Message, ContentBlock, FrozenPrefix } from "../core/project.ts";
35
+ import type { AssistantBlock } from "../core/events.ts";
36
+ import { assertNever } from "../core/exhaustive.ts";
37
+ import type { UsageSample } from "./usage.ts";
38
+ import { type Effort, type ModelSpec, CLAUDE_OPUS_5 } from "./model.ts";
39
+ import { withRetry, DEFAULT_RETRY, type RetryPolicy } from "./retry.ts";
40
+ import { anthropicOAuthHeaders } from "./oauth.ts";
41
+
42
+ export interface ToolSchema {
43
+ name: string;
44
+ description: string;
45
+ input_schema: {
46
+ type: "object";
47
+ properties: Record<string, unknown>;
48
+ required: string[];
49
+ };
50
+ }
51
+
52
+ export interface ProviderConfig {
53
+ /** Which model, and everything the runtime needs to know about it. */
54
+ spec: ModelSpec;
55
+ maxTokens: number;
56
+ /** Controls thinking depth and spend. */
57
+ effort: Effort;
58
+ /**
59
+ * Summarized thinking is opt-in: the default returns thinking blocks with
60
+ * empty text, which to a user looks like a long pause before any output.
61
+ */
62
+ thinkingDisplay: "summarized" | "omitted";
63
+ }
64
+
65
+ export const DEFAULT_PROVIDER: ProviderConfig = {
66
+ spec: CLAUDE_OPUS_5,
67
+ maxTokens: 16_000,
68
+ effort: "high",
69
+ thinkingDisplay: "summarized",
70
+ };
71
+
72
+ /**
73
+ * What the loop needs from a model, and the entire surface an adapter has to
74
+ * implement. Anything wider would be an Anthropic concept leaking into the
75
+ * contract; anything narrower could not report usage, which the whole design
76
+ * rests on measuring.
77
+ */
78
+ /**
79
+ * A display increment. Adapters emit these through `onDelta`; the loop
80
+ * turns them into UI events. They never become log events -- a turn that
81
+ * landed N deltas in the log would project as N assistant messages and
82
+ * break prefix stability (P7).
83
+ *
84
+ * Shared by both adapters so the loop never sees an Anthropic or OpenAI
85
+ * wire name. Discard is the same channel: a retry or a refusal has to
86
+ * wipe what the user already saw, and inventing a second callback would
87
+ * let one path forget.
88
+ */
89
+ export type StreamDelta =
90
+ | { kind: "text"; text: string }
91
+ | { kind: "thinking"; text: string }
92
+ | { kind: "discard" };
93
+
94
+ export type OnDelta = (delta: StreamDelta) => void;
95
+
96
+ export interface Provider {
97
+ readonly config: ProviderConfig;
98
+ send(
99
+ frozen: FrozenPrefix,
100
+ messages: readonly Message[],
101
+ node: number,
102
+ signal?: AbortSignal,
103
+ onDelta?: OnDelta,
104
+ ): Promise<Turn>;
105
+ }
106
+
107
+ /**
108
+ * The SDK methods this adapter actually calls.
109
+ *
110
+ * Structural rather than `Anthropic` so a test can inject a fake without
111
+ * constructing a client -- that would pull credentials and the SDK retry
112
+ * loop, which is the opposite of what a fake is for. Tests cannot import
113
+ * the SDK package (the boundary checker forbids package imports outside
114
+ * runtime/apps).
115
+ */
116
+ /** The complete-message shape, also what a non-streaming test fake returns. */
117
+ export interface AnthropicMessage {
118
+ stop_reason: string | null;
119
+ stop_details?: { category?: string | null; explanation?: string | null } | null;
120
+ content: Anthropic.ContentBlock[];
121
+ usage: {
122
+ input_tokens: number;
123
+ output_tokens: number;
124
+ cache_creation_input_tokens?: number | null;
125
+ cache_read_input_tokens?: number | null;
126
+ };
127
+ }
128
+
129
+ export interface AnthropicTransport {
130
+ messages: {
131
+ create(
132
+ params: Anthropic.MessageCreateParamsNonStreaming | Anthropic.MessageCreateParamsStreaming,
133
+ options?: { signal?: AbortSignal | null },
134
+ ): Promise<AnthropicMessage | AsyncIterable<unknown>>;
135
+ };
136
+ }
137
+
138
+ export interface Turn {
139
+ blocks: AssistantBlock[];
140
+ stopReason: string | null;
141
+ usage: UsageSample;
142
+ }
143
+
144
+ /** Thrown for a refusal, which arrives as HTTP 200 rather than an error. */
145
+ export class RefusalError extends Error {
146
+ readonly category: string | null;
147
+ readonly explanation: string | null;
148
+
149
+ constructor(category: string | null, explanation: string | null) {
150
+ super(`model refused the request${category ? ` (${category})` : ""}`);
151
+ this.name = "RefusalError";
152
+ this.category = category;
153
+ this.explanation = explanation;
154
+ }
155
+ }
156
+
157
+ function buildAnthropicClient(
158
+ apiKey: string | undefined,
159
+ options: { fetch?: typeof fetch; oauth?: boolean },
160
+ ): AnthropicTransport {
161
+ if (options.oauth === true && apiKey) {
162
+ // authToken is Bearer. Passing apiKey here would send x-api-key, which
163
+ // an OAuth token is not accepted as.
164
+ return new Anthropic({
165
+ maxRetries: 0,
166
+ authToken: apiKey,
167
+ defaultHeaders: anthropicOAuthHeaders(),
168
+ ...(options.fetch ? { fetch: options.fetch } : {}),
169
+ });
170
+ }
171
+ return new Anthropic({
172
+ maxRetries: 0,
173
+ ...(apiKey ? { apiKey } : {}),
174
+ ...(options.fetch ? { fetch: options.fetch } : {}),
175
+ });
176
+ }
177
+
178
+ export class AnthropicProvider implements Provider {
179
+ readonly config: ProviderConfig;
180
+ private readonly client: AnthropicTransport;
181
+ private readonly retry: RetryPolicy;
182
+ private readonly onRetry: ((attempt: number, delayMs: number, error: unknown) => void) | undefined;
183
+
184
+ /**
185
+ * `apiKey` comes from the resolution chain when there is one. Omitting it
186
+ * falls back to the SDK's own lookup (environment, `ant auth login`
187
+ * profile), which is what makes a machine already set up for Claude work
188
+ * with no configuration at all.
189
+ */
190
+ constructor(
191
+ config: ProviderConfig = DEFAULT_PROVIDER,
192
+ apiKey?: string,
193
+ options: {
194
+ client?: AnthropicTransport;
195
+ retry?: RetryPolicy;
196
+ onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
197
+ fetch?: typeof fetch;
198
+ /** OAuth access token: Bearer + oauth-2025-04-20, never x-api-key (D18). */
199
+ oauth?: boolean;
200
+ } = {},
201
+ ) {
202
+ this.config = config;
203
+ // maxRetries: 0 because this adapter retries. Leaving the SDK's own on
204
+ // would multiply the two -- four attempts becoming sixteen -- and the
205
+ // outer layer could neither see how long the inner one slept nor count
206
+ // it, so the usage report would quietly stop adding up. The SDK timer
207
+ // also ignores AbortSignal, so an outer wait is the only one Escape
208
+ // can actually cancel.
209
+ this.client = options.client ?? buildAnthropicClient(apiKey, options);
210
+ this.retry = options.retry ?? DEFAULT_RETRY;
211
+ this.onRetry = options.onRetry;
212
+ }
213
+
214
+ send(
215
+ frozen: FrozenPrefix,
216
+ messages: readonly Message[],
217
+ node: number,
218
+ signal?: AbortSignal,
219
+ onDelta?: OnDelta,
220
+ ): Promise<Turn> {
221
+ return withRetry(
222
+ () => this.attempt(frozen, messages, node, signal, onDelta),
223
+ this.retry,
224
+ undefined,
225
+ (attempt, delayMs, error) => {
226
+ // The next attempt starts from empty. Without this the UI concatenates
227
+ // two openings of the same sentence and has no sign a retry happened.
228
+ onDelta?.({ kind: "discard" });
229
+ this.onRetry?.(attempt, delayMs, error);
230
+ },
231
+ signal,
232
+ );
233
+ }
234
+
235
+ private async attempt(
236
+ frozen: FrozenPrefix,
237
+ messages: readonly Message[],
238
+ node: number,
239
+ signal?: AbortSignal,
240
+ onDelta?: OnDelta,
241
+ ): Promise<Turn> {
242
+ const started = Date.now();
243
+
244
+ const response = await this.client.messages.create(
245
+ {
246
+ model: this.config.spec.id,
247
+ max_tokens: this.config.maxTokens,
248
+ stream: true,
249
+ // One explicit breakpoint on the frozen prefix. It renders before
250
+ // messages, so this read point holds regardless of how the tail grows.
251
+ system: [
252
+ {
253
+ type: "text",
254
+ text: frozen.system,
255
+ cache_control: { type: "ephemeral" },
256
+ },
257
+ ],
258
+ // Auto-caching walks the breakpoint forward over the growing tail.
259
+ cache_control: { type: "ephemeral" },
260
+ tools: frozen.tools as Anthropic.Tool[],
261
+ // Only a reasoning model understands these. An older Claude, or an
262
+ // Anthropic-compatible endpoint serving some other model, answers
263
+ // them with a 400 or silently ignores them.
264
+ ...(this.config.spec.reasoning
265
+ ? { thinking: { type: "adaptive" as const, display: this.config.thinkingDisplay },
266
+ output_config: { effort: this.config.effort } }
267
+ : {}),
268
+ messages: messages.map(toWireMessage),
269
+ },
270
+ signal ? { signal } : undefined,
271
+ );
272
+
273
+ const assembled = isAsyncIterable(response)
274
+ ? await consumeAnthropicStream(response, onDelta)
275
+ : {
276
+ blocks: response.content.map(fromWireBlock),
277
+ stopReason: response.stop_reason,
278
+ stopDetails: response.stop_details ?? null,
279
+ usage: {
280
+ input_tokens: response.usage.input_tokens,
281
+ output_tokens: response.usage.output_tokens,
282
+ cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
283
+ cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
284
+ },
285
+ };
286
+
287
+ // A refusal is HTTP 200 with stop_reason "refusal", so content must not be
288
+ // trusted after it. Anything already painted has to go with it.
289
+ if (assembled.stopReason === "refusal") {
290
+ onDelta?.({ kind: "discard" });
291
+ const details = assembled.stopDetails;
292
+ throw new RefusalError(details?.category ?? null, details?.explanation ?? null);
293
+ }
294
+
295
+ return {
296
+ blocks: assembled.blocks,
297
+ stopReason: assembled.stopReason,
298
+ usage: {
299
+ ...assembled.usage,
300
+ node,
301
+ ms: Date.now() - started,
302
+ },
303
+ };
304
+ }
305
+ }
306
+
307
+ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
308
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
309
+ }
310
+
311
+ interface AssembledTurn {
312
+ blocks: AssistantBlock[];
313
+ stopReason: string | null;
314
+ stopDetails: { category?: string | null; explanation?: string | null } | null;
315
+ usage: {
316
+ input_tokens: number;
317
+ output_tokens: number;
318
+ cache_creation_input_tokens: number;
319
+ cache_read_input_tokens: number;
320
+ };
321
+ }
322
+
323
+ interface UsageFields {
324
+ input_tokens?: number | null;
325
+ output_tokens?: number | null;
326
+ cache_creation_input_tokens?: number | null;
327
+ cache_read_input_tokens?: number | null;
328
+ }
329
+
330
+ function takeUsage(
331
+ into: AssembledTurn["usage"],
332
+ raw: UsageFields | undefined,
333
+ ): void {
334
+ if (raw === undefined) return;
335
+ if (raw.input_tokens != null) into.input_tokens = raw.input_tokens;
336
+ if (raw.output_tokens != null) into.output_tokens = raw.output_tokens;
337
+ if (raw.cache_creation_input_tokens != null) {
338
+ into.cache_creation_input_tokens = raw.cache_creation_input_tokens;
339
+ }
340
+ if (raw.cache_read_input_tokens != null) {
341
+ into.cache_read_input_tokens = raw.cache_read_input_tokens;
342
+ }
343
+ }
344
+
345
+ function parseToolInput(raw: string): unknown {
346
+ if (raw.length === 0) return {};
347
+ try {
348
+ return JSON.parse(raw);
349
+ } catch {
350
+ return { __unparsed_arguments: raw };
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Walks the SDK stream (or a test fake of the same events) into a Turn.
356
+ *
357
+ * Usage is the sum of message_start (input / cache) and message_delta
358
+ * (output, and any field a proxy omitted at start). Deltas go to onDelta
359
+ * as they arrive so the loop can paint before this function returns.
360
+ */
361
+ async function consumeAnthropicStream(
362
+ stream: AsyncIterable<unknown>,
363
+ onDelta: OnDelta | undefined,
364
+ ): Promise<AssembledTurn> {
365
+ const content: Anthropic.ContentBlock[] = [];
366
+ const jsonBuf = new Map<number, string>();
367
+ const assembled: AssembledTurn = {
368
+ blocks: [],
369
+ stopReason: null,
370
+ stopDetails: null,
371
+ usage: {
372
+ input_tokens: 0,
373
+ output_tokens: 0,
374
+ cache_creation_input_tokens: 0,
375
+ cache_read_input_tokens: 0,
376
+ },
377
+ };
378
+
379
+ for await (const raw of stream) {
380
+ const event = raw as {
381
+ type?: string;
382
+ index?: number;
383
+ message?: { usage?: UsageFields };
384
+ content_block?: Anthropic.ContentBlock;
385
+ delta?: {
386
+ type?: string;
387
+ text?: string;
388
+ thinking?: string;
389
+ signature?: string;
390
+ partial_json?: string;
391
+ stop_reason?: string | null;
392
+ stop_details?: { category?: string | null; explanation?: string | null } | null;
393
+ };
394
+ usage?: UsageFields;
395
+ };
396
+
397
+ switch (event.type) {
398
+ case "message_start":
399
+ takeUsage(assembled.usage, event.message?.usage);
400
+ break;
401
+ case "content_block_start": {
402
+ const index = event.index ?? content.length;
403
+ if (event.content_block !== undefined) {
404
+ content[index] = { ...event.content_block } as Anthropic.ContentBlock;
405
+ if (event.content_block.type === "tool_use") jsonBuf.set(index, "");
406
+ }
407
+ break;
408
+ }
409
+ case "content_block_delta": {
410
+ const index = event.index ?? 0;
411
+ const delta = event.delta;
412
+ const block = content[index];
413
+ if (delta === undefined || block === undefined) break;
414
+ if (delta.type === "text_delta" && delta.text) {
415
+ if (block.type === "text") block.text += delta.text;
416
+ onDelta?.({ kind: "text", text: delta.text });
417
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
418
+ if (block.type === "thinking") block.thinking += delta.thinking;
419
+ onDelta?.({ kind: "thinking", text: delta.thinking });
420
+ } else if (delta.type === "signature_delta" && delta.signature && block.type === "thinking") {
421
+ block.signature = (block.signature ?? "") + delta.signature;
422
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
423
+ jsonBuf.set(index, (jsonBuf.get(index) ?? "") + delta.partial_json);
424
+ }
425
+ break;
426
+ }
427
+ case "content_block_stop": {
428
+ const index = event.index ?? 0;
429
+ const block = content[index];
430
+ if (block?.type === "tool_use") {
431
+ block.input = parseToolInput(jsonBuf.get(index) ?? "");
432
+ }
433
+ break;
434
+ }
435
+ case "message_delta": {
436
+ if (event.delta?.stop_reason) assembled.stopReason = event.delta.stop_reason;
437
+ if (event.delta?.stop_details !== undefined) {
438
+ assembled.stopDetails = event.delta.stop_details;
439
+ }
440
+ takeUsage(assembled.usage, event.usage);
441
+ break;
442
+ }
443
+ default:
444
+ break;
445
+ }
446
+ }
447
+
448
+ assembled.blocks = content.filter((b) => b !== undefined).map(fromWireBlock);
449
+ return assembled;
450
+ }
451
+
452
+ function toWireMessage(m: Message): Anthropic.MessageParam {
453
+ return {
454
+ role: m.role,
455
+ content: m.content.map(toWireBlock) as Anthropic.ContentBlockParam[],
456
+ };
457
+ }
458
+
459
+ function toWireBlock(b: ContentBlock): unknown {
460
+ switch (b.type) {
461
+ case "text":
462
+ return { type: "text", text: b.text };
463
+ case "thinking":
464
+ // Verbatim. Reconstructing the block would drop its signature and the
465
+ // next request would be rejected.
466
+ return b.raw;
467
+ case "tool_use":
468
+ return { type: "tool_use", id: b.id, name: b.name, input: b.input };
469
+ case "tool_result":
470
+ return {
471
+ type: "tool_result",
472
+ tool_use_id: b.tool_use_id,
473
+ content: b.content,
474
+ ...(b.is_error ? { is_error: true } : {}),
475
+ };
476
+ default:
477
+ return assertNever(b, "toWireBlock");
478
+ }
479
+ }
480
+
481
+ function fromWireBlock(b: Anthropic.ContentBlock): AssistantBlock {
482
+ switch (b.type) {
483
+ case "text":
484
+ return { type: "text", text: b.text };
485
+ case "thinking":
486
+ // `raw` is what goes back to the API; `text` is only for display.
487
+ return { type: "thinking", text: b.thinking, raw: b };
488
+ case "redacted_thinking":
489
+ return { type: "thinking", text: "", raw: b };
490
+ case "tool_use":
491
+ return { type: "tool_use", call_id: b.id, name: b.name, input: b.input };
492
+ default:
493
+ // Server-tool blocks land here. They are kept verbatim as thinking-like
494
+ // opaque content so a replay stays byte-identical, rather than being
495
+ // dropped and breaking the turn.
496
+ return { type: "thinking", text: "", raw: b };
497
+ }
498
+ }