@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.
- package/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- 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
|
+
}
|