@belte/anthropic 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 (2) hide show
  1. package/package.json +44 -0
  2. package/src/engine.ts +152 -0
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@belte/anthropic",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Anthropic Messages API engine for belte's agent() — a model-of-your-choosing assistant over your app's MCP surface",
6
+ "license": "MIT",
7
+ "author": "Brian Cray",
8
+ "homepage": "https://github.com/briancray/belte#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/briancray/belte.git",
12
+ "directory": "packages/anthropic"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/briancray/belte/issues"
16
+ },
17
+ "keywords": [
18
+ "belte",
19
+ "anthropic",
20
+ "claude",
21
+ "agent",
22
+ "mcp",
23
+ "llm"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.3.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "exports": {
33
+ ".": "./src/engine.ts"
34
+ },
35
+ "files": [
36
+ "src"
37
+ ],
38
+ "dependencies": {
39
+ "@anthropic-ai/sdk": "^0.102.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@belte/belte": ">=0.17.0"
43
+ }
44
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,152 @@
1
+ import Anthropic from '@anthropic-ai/sdk'
2
+ import type { AgentEngine, AgentSurface, NeutralMessage } from '@belte/belte/server/agent'
3
+
4
+ /*
5
+ The Anthropic engine for belte's `agent()`. `engine(config)` returns an
6
+ AgentEngine: a manual tool loop over the Messages API that advertises the
7
+ app's gated tool surface, streams text frames live, dispatches tool calls
8
+ back through `surface.call`, and loops until the model stops asking for
9
+ tools.
10
+
11
+ // src/server/rpc/chat.ts
12
+ import { agent } from '@belte/belte/server/agent'
13
+ import { jsonl } from '@belte/belte/server/jsonl'
14
+ import { engine } from '@belte/anthropic'
15
+ const chatEngine = engine({ model: 'claude-opus-4-8', apiKey: config.ANTHROPIC_API_KEY })
16
+ export const chat = POST(({ messages }) => jsonl(agent(chatEngine, messages)), { inputSchema })
17
+
18
+ Adaptive thinking only, no sampling params — Opus 4.8/4.7 reject
19
+ temperature/top_p/budget_tokens. The app's tools are the only tools; the
20
+ surface is already gated by each verb's clients.mcp declaration, so there
21
+ are no provider built-ins to fence here.
22
+ */
23
+
24
+ type AnthropicConfig = {
25
+ model: string
26
+ apiKey: string
27
+ maxTokens?: number
28
+ effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
29
+ }
30
+
31
+ // Provider stop_reason → the loop's neutral stop signal.
32
+ function mapStop(
33
+ stopReason: Anthropic.Message['stop_reason'],
34
+ ): 'end' | 'tool_use' | 'max_tokens' | 'refusal' {
35
+ switch (stopReason) {
36
+ case 'tool_use':
37
+ return 'tool_use'
38
+ case 'max_tokens':
39
+ return 'max_tokens'
40
+ case 'refusal':
41
+ return 'refusal'
42
+ default:
43
+ return 'end'
44
+ }
45
+ }
46
+
47
+ // Neutral conversation turn → Anthropic wire shape. System is handled separately (top-level), not here.
48
+ function toAnthropicMessage(message: NeutralMessage): Anthropic.MessageParam {
49
+ if (message.role === 'user') {
50
+ return { role: 'user', content: message.text }
51
+ }
52
+ if (message.role === 'assistant') {
53
+ const content: Anthropic.ContentBlockParam[] = []
54
+ if (message.text) {
55
+ content.push({ type: 'text', text: message.text })
56
+ }
57
+ for (const toolUse of message.toolUses ?? []) {
58
+ content.push({
59
+ type: 'tool_use',
60
+ id: toolUse.id,
61
+ name: toolUse.name,
62
+ input: toolUse.input as Record<string, unknown>,
63
+ })
64
+ }
65
+ return { role: 'assistant', content }
66
+ }
67
+ return {
68
+ role: 'user',
69
+ content: message.results.map((result) => ({
70
+ type: 'tool_result',
71
+ tool_use_id: result.id,
72
+ content: result.content,
73
+ is_error: result.isError ?? false,
74
+ })),
75
+ }
76
+ }
77
+
78
+ function toAnthropicTool(tool: AgentSurface['tools'][number]): Anthropic.Tool {
79
+ return {
80
+ name: tool.name,
81
+ description: tool.description,
82
+ input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
83
+ }
84
+ }
85
+
86
+ // Flattens an MCP tool result (text content blocks / structuredContent) to the string Anthropic expects.
87
+ function toolResultText(result: Record<string, unknown>): string {
88
+ const content = result.content
89
+ if (Array.isArray(content)) {
90
+ return content
91
+ .map((block) =>
92
+ block && typeof block === 'object' && 'text' in block ? String(block.text) : '',
93
+ )
94
+ .join('')
95
+ }
96
+ return JSON.stringify(result.structuredContent ?? result)
97
+ }
98
+
99
+ export function engine(config: AnthropicConfig): AgentEngine {
100
+ const client = new Anthropic({ apiKey: config.apiKey })
101
+ return async function* ({ surface, messages }) {
102
+ const conversation: Anthropic.MessageParam[] = messages.map(toAnthropicMessage)
103
+ const tools = surface.tools.map(toAnthropicTool)
104
+
105
+ while (true) {
106
+ const stream = client.messages.stream({
107
+ model: config.model,
108
+ max_tokens: config.maxTokens ?? 64000,
109
+ thinking: { type: 'adaptive' },
110
+ ...(config.effort ? { output_config: { effort: config.effort } } : {}),
111
+ tools,
112
+ messages: conversation,
113
+ })
114
+
115
+ // Stream text live; defer tool inputs to the final message (already JSON-parsed there).
116
+ for await (const event of stream) {
117
+ if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
118
+ yield { type: 'text', delta: event.delta.text }
119
+ }
120
+ }
121
+
122
+ const final = await stream.finalMessage()
123
+ conversation.push({ role: 'assistant', content: final.content })
124
+
125
+ if (final.stop_reason !== 'tool_use') {
126
+ yield { type: 'done', stop: mapStop(final.stop_reason) }
127
+ return
128
+ }
129
+
130
+ const results: Anthropic.ToolResultBlockParam[] = []
131
+ for (const block of final.content) {
132
+ if (block.type !== 'tool_use') {
133
+ continue
134
+ }
135
+ yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
136
+ const result = await surface.call(
137
+ block.name,
138
+ block.input as Record<string, unknown>,
139
+ )
140
+ const isError = result.isError === true
141
+ yield { type: 'tool_result', id: block.id, name: block.name, ok: !isError }
142
+ results.push({
143
+ type: 'tool_result',
144
+ tool_use_id: block.id,
145
+ content: toolResultText(result),
146
+ is_error: isError,
147
+ })
148
+ }
149
+ conversation.push({ role: 'user', content: results })
150
+ }
151
+ }
152
+ }