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