@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.
Files changed (2) hide show
  1. package/package.json +2 -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.0",
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.17.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
- const conversation: Anthropic.MessageParam[] = messages.map(toAnthropicMessage)
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
- while (true) {
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) {