@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.
- package/package.json +44 -0
- 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
|
+
}
|