@belte/anthropic 0.1.0 → 0.1.1
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 +2 -2
- package/src/engine.ts +27 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@belte/anthropic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Anthropic Messages API engine for belte's agent() — a model-of-your-choosing assistant over your app's MCP surface",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
"@anthropic-ai/sdk": "^0.102.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@belte/belte": ">=0.
|
|
42
|
+
"@belte/belte": ">=0.19.2"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/engine.ts
CHANGED
|
@@ -26,8 +26,14 @@ type AnthropicConfig = {
|
|
|
26
26
|
apiKey: string
|
|
27
27
|
maxTokens?: number
|
|
28
28
|
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
|
|
29
|
+
// Hard cap on tool-loop turns so a model that never stops requesting tools can't
|
|
30
|
+
// spin the loop (and the open stream) forever. Defaults to MAX_STEPS.
|
|
31
|
+
maxSteps?: number
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
// Default tool-loop bound — generous enough for real multi-step tasks, finite enough to stop a runaway.
|
|
35
|
+
const MAX_STEPS = 24
|
|
36
|
+
|
|
31
37
|
// Provider stop_reason → the loop's neutral stop signal.
|
|
32
38
|
function mapStop(
|
|
33
39
|
stopReason: Anthropic.Message['stop_reason'],
|
|
@@ -98,11 +104,20 @@ function toolResultText(result: Record<string, unknown>): string {
|
|
|
98
104
|
|
|
99
105
|
export function engine(config: AnthropicConfig): AgentEngine {
|
|
100
106
|
const client = new Anthropic({ apiKey: config.apiKey })
|
|
107
|
+
const maxSteps = config.maxSteps ?? MAX_STEPS
|
|
101
108
|
return async function* ({ surface, messages }) {
|
|
102
|
-
|
|
109
|
+
/*
|
|
110
|
+
Drop turns that serialize to empty content — an assistant turn with no text
|
|
111
|
+
and no tool uses, or a tool turn with no results. The Messages API rejects an
|
|
112
|
+
empty content-block array, so a caller replaying such a turn would 400 the
|
|
113
|
+
whole request.
|
|
114
|
+
*/
|
|
115
|
+
const conversation: Anthropic.MessageParam[] = messages
|
|
116
|
+
.map(toAnthropicMessage)
|
|
117
|
+
.filter((message) => !(Array.isArray(message.content) && message.content.length === 0))
|
|
103
118
|
const tools = surface.tools.map(toAnthropicTool)
|
|
104
119
|
|
|
105
|
-
|
|
120
|
+
for (let step = 0; ; step += 1) {
|
|
106
121
|
const stream = client.messages.stream({
|
|
107
122
|
model: config.model,
|
|
108
123
|
max_tokens: config.maxTokens ?? 64000,
|
|
@@ -122,10 +137,20 @@ export function engine(config: AnthropicConfig): AgentEngine {
|
|
|
122
137
|
const final = await stream.finalMessage()
|
|
123
138
|
conversation.push({ role: 'assistant', content: final.content })
|
|
124
139
|
|
|
140
|
+
// Server paused a long-running turn: resume by re-requesting with the turn
|
|
141
|
+
// so far rather than ending and truncating the output. The step cap bounds it.
|
|
142
|
+
if (final.stop_reason === 'pause_turn' && step + 1 < maxSteps) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
125
145
|
if (final.stop_reason !== 'tool_use') {
|
|
126
146
|
yield { type: 'done', stop: mapStop(final.stop_reason) }
|
|
127
147
|
return
|
|
128
148
|
}
|
|
149
|
+
if (step + 1 >= maxSteps) {
|
|
150
|
+
// Tool-loop cap hit: stop instead of dispatching another round.
|
|
151
|
+
yield { type: 'done', stop: 'error' }
|
|
152
|
+
return
|
|
153
|
+
}
|
|
129
154
|
|
|
130
155
|
const results: Anthropic.ToolResultBlockParam[] = []
|
|
131
156
|
for (const block of final.content) {
|