@visual-z/xeno 0.8.4 → 0.9.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/CHANGELOG.md +8 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/runtime/provider.ts
DELETED
|
@@ -1,580 +0,0 @@
|
|
|
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 { imageData } from "./images.ts";
|
|
36
|
-
import type { AssistantBlock } from "../core/events.ts";
|
|
37
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
38
|
-
import { isToolStopWithoutCall } from "../core/response-recovery.ts";
|
|
39
|
-
import type { UsageSample } from "./usage.ts";
|
|
40
|
-
import { type Effort, type ModelSpec, CLAUDE_OPUS_5 } from "./model.ts";
|
|
41
|
-
import { withRetry, retryLabel, DEFAULT_RETRY, type RetryPolicy } from "./retry.ts";
|
|
42
|
-
import { ConnectionReuse } from "./connection-reuse.ts";
|
|
43
|
-
import { watchIdle, type IdleWatch } from "./stream-idle.ts";
|
|
44
|
-
|
|
45
|
-
import { anthropicOAuthHeaders } from "./oauth.ts";
|
|
46
|
-
import { XENO_VERSION } from "./version.ts";
|
|
47
|
-
import type { ProviderSendOptions } from "./provider-tool-policy.ts";
|
|
48
|
-
|
|
49
|
-
export interface ToolSchema {
|
|
50
|
-
name: string;
|
|
51
|
-
description: string;
|
|
52
|
-
input_schema: {
|
|
53
|
-
/** Standard JSON Schema keywords from MCP must survive the frozen declaration. */
|
|
54
|
-
[keyword: string]: unknown;
|
|
55
|
-
type: "object";
|
|
56
|
-
properties: Record<string, unknown>;
|
|
57
|
-
required: string[];
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ProviderConfig {
|
|
62
|
-
/** Which model, and everything the runtime needs to know about it. */
|
|
63
|
-
spec: ModelSpec;
|
|
64
|
-
maxTokens: number;
|
|
65
|
-
/** Controls thinking depth and spend. */
|
|
66
|
-
effort: Effort;
|
|
67
|
-
/**
|
|
68
|
-
* Summarized thinking is opt-in: the default returns thinking blocks with
|
|
69
|
-
* empty text, which to a user looks like a long pause before any output.
|
|
70
|
-
*/
|
|
71
|
-
thinkingDisplay: "summarized" | "omitted";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const DEFAULT_PROVIDER: ProviderConfig = {
|
|
75
|
-
spec: CLAUDE_OPUS_5,
|
|
76
|
-
maxTokens: 16_000,
|
|
77
|
-
effort: "high",
|
|
78
|
-
thinkingDisplay: "summarized",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* What the loop needs from a model, and the entire surface an adapter has to
|
|
83
|
-
* implement. Anything wider would be an Anthropic concept leaking into the
|
|
84
|
-
* contract; anything narrower could not report usage, which the whole design
|
|
85
|
-
* rests on measuring.
|
|
86
|
-
*/
|
|
87
|
-
/**
|
|
88
|
-
* A display increment. Adapters emit these through `onDelta`; the loop
|
|
89
|
-
* turns them into UI events. They never become log events -- a turn that
|
|
90
|
-
* landed N deltas in the log would project as N assistant messages and
|
|
91
|
-
* break prefix stability (P7).
|
|
92
|
-
*
|
|
93
|
-
* Shared by both adapters so the loop never sees an Anthropic or OpenAI
|
|
94
|
-
* wire name. Discard is the same channel: a retry or a refusal has to
|
|
95
|
-
* wipe what the user already saw, and inventing a second callback would
|
|
96
|
-
* let one path forget.
|
|
97
|
-
*/
|
|
98
|
-
export type StreamDelta =
|
|
99
|
-
| { kind: "text"; text: string }
|
|
100
|
-
| { kind: "thinking"; text: string }
|
|
101
|
-
| { kind: "tool"; id: string; name: string; arguments: string }
|
|
102
|
-
// `max` is the policy's budget and `reason` a few words from retryLabel;
|
|
103
|
-
// optional so a view written against the older shape still renders.
|
|
104
|
-
| { kind: "retry"; attempt: number; delayMs: number; max?: number; reason?: string }
|
|
105
|
-
| { kind: "discard" };
|
|
106
|
-
|
|
107
|
-
export type OnDelta = (delta: StreamDelta) => void;
|
|
108
|
-
|
|
109
|
-
export interface Provider {
|
|
110
|
-
readonly config: ProviderConfig;
|
|
111
|
-
send(
|
|
112
|
-
frozen: FrozenPrefix,
|
|
113
|
-
messages: readonly Message[],
|
|
114
|
-
node: number,
|
|
115
|
-
signal?: AbortSignal,
|
|
116
|
-
onDelta?: OnDelta,
|
|
117
|
-
options?: ProviderSendOptions,
|
|
118
|
-
): Promise<Turn>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* The SDK methods this adapter actually calls.
|
|
123
|
-
*
|
|
124
|
-
* Structural rather than `Anthropic` so a test can inject a fake without
|
|
125
|
-
* constructing a client -- that would pull credentials and the SDK retry
|
|
126
|
-
* loop, which is the opposite of what a fake is for. Tests cannot import
|
|
127
|
-
* the SDK package (the boundary checker forbids package imports outside
|
|
128
|
-
* runtime/apps).
|
|
129
|
-
*/
|
|
130
|
-
/** The complete-message shape, also what a non-streaming test fake returns. */
|
|
131
|
-
export interface AnthropicMessage {
|
|
132
|
-
stop_reason: string | null;
|
|
133
|
-
stop_details?: { category?: string | null; explanation?: string | null } | null;
|
|
134
|
-
content: Anthropic.ContentBlock[];
|
|
135
|
-
usage: {
|
|
136
|
-
input_tokens: number;
|
|
137
|
-
output_tokens: number;
|
|
138
|
-
cache_creation_input_tokens?: number | null;
|
|
139
|
-
cache_read_input_tokens?: number | null;
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export interface AnthropicTransport {
|
|
144
|
-
messages: {
|
|
145
|
-
create(
|
|
146
|
-
params: Anthropic.MessageCreateParamsNonStreaming | Anthropic.MessageCreateParamsStreaming,
|
|
147
|
-
options?: { signal?: AbortSignal | null },
|
|
148
|
-
): Promise<AnthropicMessage | AsyncIterable<unknown>>;
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface Turn {
|
|
153
|
-
blocks: AssistantBlock[];
|
|
154
|
-
stopReason: string | null;
|
|
155
|
-
usage: UsageSample;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** D11: a rejected response can still prove usage; arbitrary errors cannot prove a billable receipt. */
|
|
159
|
-
export class FailedResponseError extends Error {
|
|
160
|
-
readonly usage: UsageSample | undefined;
|
|
161
|
-
|
|
162
|
-
constructor(message: string, usage?: UsageSample) {
|
|
163
|
-
super(message);
|
|
164
|
-
this.name = "FailedResponseError";
|
|
165
|
-
this.usage = usage === undefined ? undefined : { ...usage };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* A 200 response that contradicts itself, such as a tool stop with no tool
|
|
171
|
-
* call. retry.ts treats it like a transient stream failure: a fresh sample is
|
|
172
|
-
* the remedy, and exhausting retries must surface as a provider error rather
|
|
173
|
-
* than reach the loop as a silent final answer. The output token count tells
|
|
174
|
-
* "generated but lost in transit" apart from "nothing generated".
|
|
175
|
-
*/
|
|
176
|
-
export class MalformedResponseError extends FailedResponseError {
|
|
177
|
-
constructor(detail: string, usage?: UsageSample) {
|
|
178
|
-
super(`malformed provider response: ${detail}${usage ? ` (${usage.output_tokens} output tokens)` : ""}`, usage);
|
|
179
|
-
this.name = "MalformedResponseError";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Every adapter returns through this, so no dialect can hand the loop a contradictory turn. */
|
|
184
|
-
export function coherentTurn(turn: Turn, usageKnown = true): Turn {
|
|
185
|
-
if (isToolStopWithoutCall(turn.stopReason, turn.blocks))
|
|
186
|
-
throw new MalformedResponseError(`stop_reason ${JSON.stringify(turn.stopReason)} without a tool call`, usageKnown ? turn.usage : undefined);
|
|
187
|
-
return turn;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Thrown for a refusal, which arrives as HTTP 200 rather than an error. */
|
|
191
|
-
export class RefusalError extends FailedResponseError {
|
|
192
|
-
readonly category: string | null;
|
|
193
|
-
readonly explanation: string | null;
|
|
194
|
-
|
|
195
|
-
constructor(category: string | null, explanation: string | null, usage?: UsageSample) {
|
|
196
|
-
super(`model refused the request${category ? ` (${category})` : ""}`, usage);
|
|
197
|
-
this.name = "RefusalError";
|
|
198
|
-
this.category = category;
|
|
199
|
-
this.explanation = explanation;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function buildAnthropicClient(
|
|
204
|
-
apiKey: string | undefined,
|
|
205
|
-
options: { fetch?: typeof fetch; oauth?: boolean; baseUrl?: string },
|
|
206
|
-
): AnthropicTransport {
|
|
207
|
-
// D18: the User-Agent, not SDK headers, identifies xeno on the wire. Left alone
|
|
208
|
-
// the SDK sends `Anthropic/JS <version>`, which model gateways fold into "custom
|
|
209
|
-
// client" attribution; provider-request.ts already stamps xeno's UA on the Pi
|
|
210
|
-
// path. Constructor defaultHeaders merge after the SDK's built-in UA, so this
|
|
211
|
-
// entry wins without touching per-request headers.
|
|
212
|
-
const identity = { "User-Agent": `xeno/${XENO_VERSION}` };
|
|
213
|
-
if (options.oauth === true && apiKey) {
|
|
214
|
-
// authToken is Bearer. Passing apiKey here would send x-api-key, which
|
|
215
|
-
// an OAuth token is not accepted as.
|
|
216
|
-
return new Anthropic({
|
|
217
|
-
maxRetries: 0,
|
|
218
|
-
...(options.baseUrl ? { baseURL: options.baseUrl } : {}),
|
|
219
|
-
authToken: apiKey,
|
|
220
|
-
defaultHeaders: { ...anthropicOAuthHeaders(), ...identity },
|
|
221
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
return new Anthropic({
|
|
225
|
-
maxRetries: 0,
|
|
226
|
-
...(options.baseUrl ? { baseURL: options.baseUrl } : {}),
|
|
227
|
-
...(apiKey ? { apiKey } : {}),
|
|
228
|
-
defaultHeaders: identity,
|
|
229
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export class AnthropicProvider implements Provider {
|
|
234
|
-
readonly config: ProviderConfig;
|
|
235
|
-
private readonly client: AnthropicTransport;
|
|
236
|
-
private readonly connections = new ConnectionReuse();
|
|
237
|
-
private readonly streamIdleMs: number | undefined;
|
|
238
|
-
private readonly retry: RetryPolicy;
|
|
239
|
-
private readonly onRetry: ((attempt: number, delayMs: number, error: unknown) => void) | undefined;
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* `apiKey` comes from the resolution chain when there is one. Omitting it
|
|
243
|
-
* falls back to the SDK's own lookup (environment, `ant auth login`
|
|
244
|
-
* profile), which is what makes a machine already set up for Claude work
|
|
245
|
-
* with no configuration at all.
|
|
246
|
-
*/
|
|
247
|
-
constructor(
|
|
248
|
-
config: ProviderConfig = DEFAULT_PROVIDER,
|
|
249
|
-
apiKey?: string,
|
|
250
|
-
options: {
|
|
251
|
-
client?: AnthropicTransport;
|
|
252
|
-
retry?: RetryPolicy;
|
|
253
|
-
onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
|
|
254
|
-
fetch?: typeof fetch;
|
|
255
|
-
/** OAuth access token: Bearer + oauth-2025-04-20, never x-api-key (D18). */
|
|
256
|
-
oauth?: boolean;
|
|
257
|
-
/** Silence after which one attempt is abandoned; tests shorten it (stream-idle.ts). */
|
|
258
|
-
streamIdleMs?: number;
|
|
259
|
-
} = {},
|
|
260
|
-
) {
|
|
261
|
-
this.config = config;
|
|
262
|
-
// maxRetries: 0 because this adapter retries. Leaving the SDK's own on
|
|
263
|
-
// would multiply the two -- four attempts becoming sixteen -- and the
|
|
264
|
-
// outer layer could neither see how long the inner one slept nor count
|
|
265
|
-
// it, so the usage report would quietly stop adding up. The SDK timer
|
|
266
|
-
// also ignores AbortSignal, so an outer wait is the only one Escape
|
|
267
|
-
// can actually cancel.
|
|
268
|
-
this.client = options.client ?? buildAnthropicClient(apiKey, { ...options, fetch: this.connections.wrap(options.fetch ?? fetch),
|
|
269
|
-
...(config.spec.baseUrl ? { baseUrl: config.spec.baseUrl } : {}) });
|
|
270
|
-
this.retry = options.retry ?? DEFAULT_RETRY;
|
|
271
|
-
this.onRetry = options.onRetry;
|
|
272
|
-
this.streamIdleMs = options.streamIdleMs;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
send(
|
|
276
|
-
frozen: FrozenPrefix,
|
|
277
|
-
messages: readonly Message[],
|
|
278
|
-
node: number,
|
|
279
|
-
signal?: AbortSignal,
|
|
280
|
-
onDelta?: OnDelta,
|
|
281
|
-
options?: ProviderSendOptions,
|
|
282
|
-
): Promise<Turn> {
|
|
283
|
-
return withRetry(
|
|
284
|
-
() => watchIdle(signal, this.streamIdleMs, idle => this.attempt(frozen, messages, node, idle, onDelta, options)).catch((error: unknown) => { this.connections.note(error); throw error; }),
|
|
285
|
-
this.retry,
|
|
286
|
-
undefined,
|
|
287
|
-
(attempt, delayMs, error) => {
|
|
288
|
-
// The next attempt starts from empty. Without this the UI concatenates
|
|
289
|
-
// two openings of the same sentence and has no sign a retry happened.
|
|
290
|
-
onDelta?.({ kind: "discard" });
|
|
291
|
-
onDelta?.({ kind: "retry", attempt, delayMs, max: this.retry.maxRetries, reason: retryLabel(error) });
|
|
292
|
-
this.onRetry?.(attempt, delayMs, error);
|
|
293
|
-
},
|
|
294
|
-
signal,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private async attempt(
|
|
299
|
-
frozen: FrozenPrefix,
|
|
300
|
-
messages: readonly Message[],
|
|
301
|
-
node: number,
|
|
302
|
-
idle: IdleWatch,
|
|
303
|
-
onDelta?: OnDelta,
|
|
304
|
-
options?: ProviderSendOptions,
|
|
305
|
-
): Promise<Turn> {
|
|
306
|
-
const started = Date.now();
|
|
307
|
-
|
|
308
|
-
const response = await this.client.messages.create(
|
|
309
|
-
{
|
|
310
|
-
model: this.config.spec.id,
|
|
311
|
-
max_tokens: this.config.maxTokens,
|
|
312
|
-
stream: true,
|
|
313
|
-
// One explicit breakpoint on the frozen prefix. It renders before
|
|
314
|
-
// messages, so this read point holds regardless of how the tail grows.
|
|
315
|
-
system: [
|
|
316
|
-
{
|
|
317
|
-
type: "text",
|
|
318
|
-
text: frozen.system,
|
|
319
|
-
cache_control: { type: "ephemeral" },
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
// Auto-caching walks the breakpoint forward over the growing tail.
|
|
323
|
-
cache_control: { type: "ephemeral" },
|
|
324
|
-
tools: frozen.tools as Anthropic.Tool[],
|
|
325
|
-
...(options?.toolUse === "disabled" ? { tool_choice: { type: "none" as const } } : {}),
|
|
326
|
-
// Only a reasoning model understands these. An older Claude, or an
|
|
327
|
-
// Anthropic-compatible endpoint serving some other model, answers
|
|
328
|
-
// them with a 400 or silently ignores them.
|
|
329
|
-
...(this.config.spec.reasoning
|
|
330
|
-
? this.config.effort === "off" ? { thinking: { type: "disabled" as const } }
|
|
331
|
-
: { thinking: { type: "adaptive" as const, display: this.config.thinkingDisplay },
|
|
332
|
-
output_config: { effort: anthropicEffort(this.config) } }
|
|
333
|
-
: {}),
|
|
334
|
-
messages: messages.map(toWireMessage),
|
|
335
|
-
},
|
|
336
|
-
{ signal: idle.signal },
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
const assembled = isAsyncIterable(response)
|
|
340
|
-
? await consumeAnthropicStream(idle.events(response), onDelta)
|
|
341
|
-
: {
|
|
342
|
-
blocks: response.content.map(fromWireBlock),
|
|
343
|
-
stopReason: response.stop_reason,
|
|
344
|
-
stopDetails: response.stop_details ?? null,
|
|
345
|
-
usageKnown: true,
|
|
346
|
-
usage: {
|
|
347
|
-
input_tokens: response.usage.input_tokens,
|
|
348
|
-
output_tokens: response.usage.output_tokens,
|
|
349
|
-
cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
|
|
350
|
-
cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
const usage = { ...assembled.usage, node, ms: Date.now() - started };
|
|
354
|
-
|
|
355
|
-
// A refusal is HTTP 200 with stop_reason "refusal", so content must not be
|
|
356
|
-
// trusted after it. Anything already painted has to go with it.
|
|
357
|
-
if (assembled.stopReason === "refusal") {
|
|
358
|
-
onDelta?.({ kind: "discard" });
|
|
359
|
-
const details = assembled.stopDetails;
|
|
360
|
-
throw new RefusalError(details?.category ?? null, details?.explanation ?? null, assembled.usageKnown ? usage : undefined);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return coherentTurn({
|
|
364
|
-
blocks: assembled.blocks,
|
|
365
|
-
stopReason: assembled.stopReason,
|
|
366
|
-
usage,
|
|
367
|
-
}, assembled.usageKnown);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** Pi maps minimal to low for adaptive thinking; off is a thinking switch, never an effort string. */
|
|
372
|
-
function anthropicEffort(config: ProviderConfig): "low" | "medium" | "high" | "xhigh" | "max" {
|
|
373
|
-
const level = config.spec.thinkingLevelMap?.[config.effort] ?? config.effort;
|
|
374
|
-
return level === "minimal" || level === "off" ? "low" :
|
|
375
|
-
level === "low" || level === "medium" || level === "high" || level === "xhigh" || level === "max" ? level : "high";
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
379
|
-
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
interface AssembledTurn {
|
|
383
|
-
blocks: AssistantBlock[];
|
|
384
|
-
stopReason: string | null;
|
|
385
|
-
stopDetails: { category?: string | null; explanation?: string | null } | null;
|
|
386
|
-
/** Provider receipt presence distinguishes a measured zero from absent usage (D11). */
|
|
387
|
-
usageKnown: boolean;
|
|
388
|
-
usage: {
|
|
389
|
-
input_tokens: number;
|
|
390
|
-
output_tokens: number;
|
|
391
|
-
cache_creation_input_tokens: number;
|
|
392
|
-
cache_read_input_tokens: number;
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
interface UsageFields {
|
|
397
|
-
input_tokens?: number | null;
|
|
398
|
-
output_tokens?: number | null;
|
|
399
|
-
cache_creation_input_tokens?: number | null;
|
|
400
|
-
cache_read_input_tokens?: number | null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function takeUsage(
|
|
404
|
-
into: AssembledTurn["usage"],
|
|
405
|
-
raw: UsageFields | undefined,
|
|
406
|
-
): void {
|
|
407
|
-
if (raw === undefined) return;
|
|
408
|
-
if (raw.input_tokens != null) into.input_tokens = raw.input_tokens;
|
|
409
|
-
if (raw.output_tokens != null) into.output_tokens = raw.output_tokens;
|
|
410
|
-
if (raw.cache_creation_input_tokens != null) {
|
|
411
|
-
into.cache_creation_input_tokens = raw.cache_creation_input_tokens;
|
|
412
|
-
}
|
|
413
|
-
if (raw.cache_read_input_tokens != null) {
|
|
414
|
-
into.cache_read_input_tokens = raw.cache_read_input_tokens;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function parseToolInput(raw: string): unknown {
|
|
419
|
-
if (raw.length === 0) return {};
|
|
420
|
-
try {
|
|
421
|
-
return JSON.parse(raw);
|
|
422
|
-
} catch {
|
|
423
|
-
return { __unparsed_arguments: raw };
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Walks the SDK stream (or a test fake of the same events) into a Turn.
|
|
429
|
-
*
|
|
430
|
-
* Usage is the sum of message_start (input / cache) and message_delta
|
|
431
|
-
* (output, and any field a proxy omitted at start). Deltas go to onDelta
|
|
432
|
-
* as they arrive so the loop can paint before this function returns.
|
|
433
|
-
*/
|
|
434
|
-
async function consumeAnthropicStream(
|
|
435
|
-
stream: AsyncIterable<unknown>,
|
|
436
|
-
onDelta: OnDelta | undefined,
|
|
437
|
-
): Promise<AssembledTurn> {
|
|
438
|
-
const content: Anthropic.ContentBlock[] = [];
|
|
439
|
-
const jsonBuf = new Map<number, string>();
|
|
440
|
-
const assembled: AssembledTurn = {
|
|
441
|
-
blocks: [],
|
|
442
|
-
stopReason: null,
|
|
443
|
-
stopDetails: null,
|
|
444
|
-
usageKnown: false,
|
|
445
|
-
usage: {
|
|
446
|
-
input_tokens: 0,
|
|
447
|
-
output_tokens: 0,
|
|
448
|
-
cache_creation_input_tokens: 0,
|
|
449
|
-
cache_read_input_tokens: 0,
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
for await (const raw of stream) {
|
|
454
|
-
const event = raw as {
|
|
455
|
-
type?: string;
|
|
456
|
-
index?: number;
|
|
457
|
-
message?: { usage?: UsageFields };
|
|
458
|
-
content_block?: Anthropic.ContentBlock;
|
|
459
|
-
delta?: {
|
|
460
|
-
type?: string;
|
|
461
|
-
text?: string;
|
|
462
|
-
thinking?: string;
|
|
463
|
-
signature?: string;
|
|
464
|
-
partial_json?: string;
|
|
465
|
-
stop_reason?: string | null;
|
|
466
|
-
stop_details?: { category?: string | null; explanation?: string | null } | null;
|
|
467
|
-
};
|
|
468
|
-
usage?: UsageFields;
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
switch (event.type) {
|
|
472
|
-
case "message_start":
|
|
473
|
-
takeUsage(assembled.usage, event.message?.usage);
|
|
474
|
-
if (event.message?.usage !== undefined) assembled.usageKnown = true;
|
|
475
|
-
break;
|
|
476
|
-
case "content_block_start": {
|
|
477
|
-
const index = event.index ?? content.length;
|
|
478
|
-
if (event.content_block !== undefined) {
|
|
479
|
-
content[index] = { ...event.content_block } as Anthropic.ContentBlock;
|
|
480
|
-
if (event.content_block.type === "tool_use") {
|
|
481
|
-
jsonBuf.set(index, "");
|
|
482
|
-
onDelta?.({ kind: "tool", id: event.content_block.id, name: event.content_block.name, arguments: "" });
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
case "content_block_delta": {
|
|
488
|
-
const index = event.index ?? 0;
|
|
489
|
-
const delta = event.delta;
|
|
490
|
-
const block = content[index];
|
|
491
|
-
if (delta === undefined || block === undefined) break;
|
|
492
|
-
if (delta.type === "text_delta" && delta.text) {
|
|
493
|
-
if (block.type === "text") block.text += delta.text;
|
|
494
|
-
onDelta?.({ kind: "text", text: delta.text });
|
|
495
|
-
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
496
|
-
if (block.type === "thinking") block.thinking += delta.thinking;
|
|
497
|
-
onDelta?.({ kind: "thinking", text: delta.thinking });
|
|
498
|
-
} else if (delta.type === "signature_delta" && delta.signature && block.type === "thinking") {
|
|
499
|
-
block.signature = (block.signature ?? "") + delta.signature;
|
|
500
|
-
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
501
|
-
jsonBuf.set(index, (jsonBuf.get(index) ?? "") + delta.partial_json);
|
|
502
|
-
if (block.type === "tool_use") onDelta?.({ kind: "tool", id: block.id, name: block.name, arguments: jsonBuf.get(index)! });
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
case "content_block_stop": {
|
|
507
|
-
const index = event.index ?? 0;
|
|
508
|
-
const block = content[index];
|
|
509
|
-
if (block?.type === "tool_use") {
|
|
510
|
-
block.input = parseToolInput(jsonBuf.get(index) ?? "");
|
|
511
|
-
}
|
|
512
|
-
break;
|
|
513
|
-
}
|
|
514
|
-
case "message_delta": {
|
|
515
|
-
if (event.delta?.stop_reason) assembled.stopReason = event.delta.stop_reason;
|
|
516
|
-
if (event.delta?.stop_details !== undefined) {
|
|
517
|
-
assembled.stopDetails = event.delta.stop_details;
|
|
518
|
-
}
|
|
519
|
-
takeUsage(assembled.usage, event.usage);
|
|
520
|
-
if (event.usage !== undefined) assembled.usageKnown = true;
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
default:
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
assembled.blocks = content.filter((b) => b !== undefined).map(fromWireBlock);
|
|
529
|
-
return assembled;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function toWireMessage(m: Message): Anthropic.MessageParam {
|
|
533
|
-
return {
|
|
534
|
-
role: m.role,
|
|
535
|
-
// Anthropic requires all parallel tool results before any text or image in the user message.
|
|
536
|
-
content: [...m.content.filter(b => b.type === "tool_result"), ...m.content.filter(b => b.type !== "tool_result")].map(toWireBlock) as Anthropic.ContentBlockParam[],
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function toWireBlock(b: ContentBlock): unknown {
|
|
541
|
-
switch (b.type) {
|
|
542
|
-
case "image": return { type: "image", source: { type: "base64", media_type: b.image.mimeType, data: imageData(b) } };
|
|
543
|
-
case "text":
|
|
544
|
-
return { type: "text", text: b.text };
|
|
545
|
-
case "thinking":
|
|
546
|
-
// Verbatim. Reconstructing the block would drop its signature and the
|
|
547
|
-
// next request would be rejected.
|
|
548
|
-
return b.raw;
|
|
549
|
-
case "tool_use":
|
|
550
|
-
return { type: "tool_use", id: b.id, name: b.name, input: b.input };
|
|
551
|
-
case "tool_result":
|
|
552
|
-
return {
|
|
553
|
-
type: "tool_result",
|
|
554
|
-
tool_use_id: b.tool_use_id,
|
|
555
|
-
content: b.content,
|
|
556
|
-
...(b.is_error ? { is_error: true } : {}),
|
|
557
|
-
};
|
|
558
|
-
default:
|
|
559
|
-
return assertNever(b, "toWireBlock");
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function fromWireBlock(b: Anthropic.ContentBlock): AssistantBlock {
|
|
564
|
-
switch (b.type) {
|
|
565
|
-
case "text":
|
|
566
|
-
return { type: "text", text: b.text };
|
|
567
|
-
case "thinking":
|
|
568
|
-
// `raw` is what goes back to the API; `text` is only for display.
|
|
569
|
-
return { type: "thinking", text: b.thinking, raw: b };
|
|
570
|
-
case "redacted_thinking":
|
|
571
|
-
return { type: "thinking", text: "", raw: b };
|
|
572
|
-
case "tool_use":
|
|
573
|
-
return { type: "tool_use", call_id: b.id, name: b.name, input: b.input };
|
|
574
|
-
default:
|
|
575
|
-
// Server-tool blocks land here. They are kept verbatim as thinking-like
|
|
576
|
-
// opaque content so a replay stays byte-identical, rather than being
|
|
577
|
-
// dropped and breaking the turn.
|
|
578
|
-
return { type: "thinking", text: "", raw: b };
|
|
579
|
-
}
|
|
580
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/** R13: UI and protocol clients share one active-turn question adapter. */
|
|
2
|
-
import { parseQuestions, questionRequest, validateQuestionReply, MAX_QUESTIONS, MAX_QUESTION_CHOICES,
|
|
3
|
-
type QuestionRequest, type QuestionReply } from "../core/question.ts";
|
|
4
|
-
import { failureDiagnostic, NOT_STARTED } from "../core/tool-failure.ts";
|
|
5
|
-
import { executionOutcome } from "../core/outcome.ts";
|
|
6
|
-
import type { JsonValue, ToolDiagnostic } from "../core/tool-contract.ts";
|
|
7
|
-
import type { ToolContext, ToolDefinition, ToolResult } from "./tools.ts";
|
|
8
|
-
|
|
9
|
-
export type QuestionAdapter = (request: QuestionRequest, signal: AbortSignal) => Promise<unknown | undefined>;
|
|
10
|
-
export interface QuestionSlot {
|
|
11
|
-
/** The Session/app owner changes this at user-turn boundaries, never model input. */
|
|
12
|
-
activeTurn?: string;
|
|
13
|
-
current?: QuestionAdapter;
|
|
14
|
-
}
|
|
15
|
-
function failure(code: ToolDiagnostic["code"], message: string): ToolResult {
|
|
16
|
-
return { ok: false, content: message, diagnostic: failureDiagnostic(code, message, NOT_STARTED),
|
|
17
|
-
outcome: executionOutcome(code === "cancelled" ? "cancelled" : "tool_failed") };
|
|
18
|
-
}
|
|
19
|
-
function plainReply(reply: QuestionReply): JsonValue {
|
|
20
|
-
return { request_id: reply.request_id, turn_id: reply.turn_id,
|
|
21
|
-
answers: reply.answers.map(answer => ({ id: answer.id, choices: [...answer.choices], ...(answer.text !== undefined ? { text: answer.text } : {}) })) };
|
|
22
|
-
}
|
|
23
|
-
async function ask(slot: QuestionSlot, ctx: ToolContext, input: Record<string, unknown>): Promise<ToolResult> {
|
|
24
|
-
let questions;
|
|
25
|
-
try { questions = parseQuestions(input); }
|
|
26
|
-
catch (error) { return failure("invalid_input", error instanceof Error ? error.message : "Invalid question"); }
|
|
27
|
-
const turn = slot.activeTurn, call = ctx.callId, adapter = slot.current;
|
|
28
|
-
if (!turn || !call) return failure("permission_denied", "question requires an active turn and tool invocation");
|
|
29
|
-
if (!adapter) return failure("environment_missing", "question requires an interactive terminal or a connected client that can answer questions");
|
|
30
|
-
const signal = ctx.signal ?? new AbortController().signal;
|
|
31
|
-
if (signal.aborted) return failure("cancelled", "Question cancelled");
|
|
32
|
-
const request = questionRequest(turn, call, questions);
|
|
33
|
-
let stop = () => {};
|
|
34
|
-
const cancelled = new Promise<undefined>(resolve => { stop = () => resolve(undefined); signal.addEventListener("abort", stop, { once: true }); });
|
|
35
|
-
try {
|
|
36
|
-
// Late adapter rejection is observed by race; an adapter ignoring its
|
|
37
|
-
// signal cannot keep the original tool exchange unpaired (D5).
|
|
38
|
-
const answer = await Promise.race([Promise.resolve().then(() => adapter(structuredClone(request), signal)), cancelled]);
|
|
39
|
-
if (signal.aborted || answer === undefined || slot.activeTurn !== turn) return failure("cancelled", "Question cancelled or its turn is no longer active");
|
|
40
|
-
let reply;
|
|
41
|
-
try { reply = validateQuestionReply(request, answer); }
|
|
42
|
-
catch { return failure("invalid_input", "Question client returned an invalid answer or stale invocation identity"); }
|
|
43
|
-
const data = plainReply(reply);
|
|
44
|
-
return { ok: true, content: JSON.stringify(data), data, outcome: executionOutcome("tool_completed") };
|
|
45
|
-
} catch { return failure("execution_failed", "Question client failed before returning a validated answer"); }
|
|
46
|
-
finally { signal.removeEventListener("abort", stop); }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createQuestionTool(slot: QuestionSlot): ToolDefinition {
|
|
50
|
-
return { name: "question", access: "state", execution: "exclusive", branch: "none",
|
|
51
|
-
description: "Ask 1..3 concise questions for missing requirements or material decisions; await the user's answer in this turn. Use stable question and option ids. Set multiple=true for multiple choices; custom text is enabled by default. An absent/cancelled answer is not approval. This tool does not approve tool execution or change workflow mode.",
|
|
52
|
-
input_schema: { type: "object", required: ["questions"], properties: {
|
|
53
|
-
questions: { type: "array", minItems: 1, maxItems: MAX_QUESTIONS, items: { type: "object", additionalProperties: false,
|
|
54
|
-
required: ["id", "header", "question"], properties: {
|
|
55
|
-
id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,63}$" }, header: { type: "string", minLength: 1, maxLength: 40 },
|
|
56
|
-
question: { type: "string", minLength: 1, maxLength: 2000 }, multiple: { type: "boolean" }, custom: { type: "boolean" },
|
|
57
|
-
options: { type: "array", maxItems: MAX_QUESTION_CHOICES, items: { type: "object", additionalProperties: false,
|
|
58
|
-
required: ["id", "label"], properties: { id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,63}$" },
|
|
59
|
-
label: { type: "string", minLength: 1, maxLength: 120 }, description: { type: "string", minLength: 1, maxLength: 600 } } } },
|
|
60
|
-
} } },
|
|
61
|
-
} }, run: (ctx, input) => ask(slot, ctx, input) };
|
|
62
|
-
}
|