@codeany/open-agent-sdk 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/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/examples/01-simple-query.ts +43 -0
- package/examples/02-multi-tool.ts +44 -0
- package/examples/03-multi-turn.ts +39 -0
- package/examples/04-prompt-api.ts +29 -0
- package/examples/05-custom-system-prompt.ts +26 -0
- package/examples/06-mcp-server.ts +49 -0
- package/examples/07-custom-tools.ts +87 -0
- package/examples/08-official-api-compat.ts +38 -0
- package/examples/09-subagents.ts +48 -0
- package/examples/10-permissions.ts +40 -0
- package/examples/11-custom-mcp-tools.ts +101 -0
- package/examples/web/index.html +365 -0
- package/examples/web/server.ts +157 -0
- package/package.json +60 -0
- package/src/agent.ts +425 -0
- package/src/engine.ts +520 -0
- package/src/hooks.ts +261 -0
- package/src/index.ts +376 -0
- package/src/mcp/client.ts +150 -0
- package/src/sdk-mcp-server.ts +78 -0
- package/src/session.ts +227 -0
- package/src/tool-helper.ts +127 -0
- package/src/tools/agent-tool.ts +153 -0
- package/src/tools/ask-user.ts +79 -0
- package/src/tools/bash.ts +75 -0
- package/src/tools/config-tool.ts +89 -0
- package/src/tools/cron-tools.ts +153 -0
- package/src/tools/edit.ts +74 -0
- package/src/tools/glob.ts +77 -0
- package/src/tools/grep.ts +168 -0
- package/src/tools/index.ts +232 -0
- package/src/tools/lsp-tool.ts +163 -0
- package/src/tools/mcp-resource-tools.ts +125 -0
- package/src/tools/notebook-edit.ts +93 -0
- package/src/tools/plan-tools.ts +88 -0
- package/src/tools/read.ts +73 -0
- package/src/tools/send-message.ts +96 -0
- package/src/tools/task-tools.ts +290 -0
- package/src/tools/team-tools.ts +128 -0
- package/src/tools/todo-tool.ts +112 -0
- package/src/tools/tool-search.ts +87 -0
- package/src/tools/types.ts +62 -0
- package/src/tools/web-fetch.ts +66 -0
- package/src/tools/web-search.ts +86 -0
- package/src/tools/worktree-tools.ts +140 -0
- package/src/tools/write.ts +42 -0
- package/src/types.ts +459 -0
- package/src/utils/compact.ts +206 -0
- package/src/utils/context.ts +191 -0
- package/src/utils/fileCache.ts +148 -0
- package/src/utils/messages.ts +196 -0
- package/src/utils/retry.ts +140 -0
- package/src/utils/tokens.ts +122 -0
- package/tsconfig.json +19 -0
package/src/engine.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryEngine - Core agentic loop
|
|
3
|
+
*
|
|
4
|
+
* Manages the full conversation lifecycle:
|
|
5
|
+
* 1. Take user prompt
|
|
6
|
+
* 2. Build system prompt with context (git status, project context, tools)
|
|
7
|
+
* 3. Call LLM API with tools
|
|
8
|
+
* 4. Stream response
|
|
9
|
+
* 5. Execute tool calls (concurrent for read-only, serial for mutations)
|
|
10
|
+
* 6. Send results back, repeat until done
|
|
11
|
+
* 7. Auto-compact when context exceeds threshold
|
|
12
|
+
* 8. Retry with exponential backoff on transient errors
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
16
|
+
import type {
|
|
17
|
+
SDKMessage,
|
|
18
|
+
QueryEngineConfig,
|
|
19
|
+
ToolDefinition,
|
|
20
|
+
ToolResult,
|
|
21
|
+
ToolContext,
|
|
22
|
+
TokenUsage,
|
|
23
|
+
} from './types.js'
|
|
24
|
+
import { toApiTool } from './tools/types.js'
|
|
25
|
+
import {
|
|
26
|
+
estimateMessagesTokens,
|
|
27
|
+
estimateCost,
|
|
28
|
+
getAutoCompactThreshold,
|
|
29
|
+
} from './utils/tokens.js'
|
|
30
|
+
import {
|
|
31
|
+
shouldAutoCompact,
|
|
32
|
+
compactConversation,
|
|
33
|
+
microCompactMessages,
|
|
34
|
+
createAutoCompactState,
|
|
35
|
+
type AutoCompactState,
|
|
36
|
+
} from './utils/compact.js'
|
|
37
|
+
import {
|
|
38
|
+
withRetry,
|
|
39
|
+
isPromptTooLongError,
|
|
40
|
+
formatApiError,
|
|
41
|
+
} from './utils/retry.js'
|
|
42
|
+
import { getSystemContext, getUserContext } from './utils/context.js'
|
|
43
|
+
import { normalizeMessagesForAPI } from './utils/messages.js'
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// System Prompt Builder
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
async function buildSystemPrompt(config: QueryEngineConfig): Promise<string> {
|
|
50
|
+
if (config.systemPrompt) {
|
|
51
|
+
const base = config.systemPrompt
|
|
52
|
+
return config.appendSystemPrompt
|
|
53
|
+
? base + '\n\n' + config.appendSystemPrompt
|
|
54
|
+
: base
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parts: string[] = []
|
|
58
|
+
|
|
59
|
+
parts.push(
|
|
60
|
+
'You are an AI assistant with access to tools. Use the tools provided to help the user accomplish their tasks.',
|
|
61
|
+
'You should use tools when they would help you complete the task more accurately or efficiently.',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// List available tools with descriptions
|
|
65
|
+
parts.push('\n# Available Tools\n')
|
|
66
|
+
for (const tool of config.tools) {
|
|
67
|
+
parts.push(`- **${tool.name}**: ${tool.description}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add agent definitions
|
|
71
|
+
if (config.agents && Object.keys(config.agents).length > 0) {
|
|
72
|
+
parts.push('\n# Available Subagents\n')
|
|
73
|
+
for (const [name, def] of Object.entries(config.agents)) {
|
|
74
|
+
parts.push(`- **${name}**: ${def.description}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// System context (git status, etc.)
|
|
79
|
+
try {
|
|
80
|
+
const sysCtx = await getSystemContext(config.cwd)
|
|
81
|
+
if (sysCtx) {
|
|
82
|
+
parts.push('\n# Environment\n')
|
|
83
|
+
parts.push(sysCtx)
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Context is best-effort
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// User context (AGENT.md, date)
|
|
90
|
+
try {
|
|
91
|
+
const userCtx = await getUserContext(config.cwd)
|
|
92
|
+
if (userCtx) {
|
|
93
|
+
parts.push('\n# Project Context\n')
|
|
94
|
+
parts.push(userCtx)
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Context is best-effort
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Working directory
|
|
101
|
+
parts.push(`\n# Working Directory\n${config.cwd}`)
|
|
102
|
+
|
|
103
|
+
if (config.appendSystemPrompt) {
|
|
104
|
+
parts.push('\n' + config.appendSystemPrompt)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return parts.join('\n')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// QueryEngine
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export class QueryEngine {
|
|
115
|
+
private config: QueryEngineConfig
|
|
116
|
+
private client: Anthropic
|
|
117
|
+
public messages: Anthropic.MessageParam[] = []
|
|
118
|
+
private totalUsage: TokenUsage = { input_tokens: 0, output_tokens: 0 }
|
|
119
|
+
private totalCost = 0
|
|
120
|
+
private turnCount = 0
|
|
121
|
+
private compactState: AutoCompactState
|
|
122
|
+
private sessionId: string
|
|
123
|
+
private apiTimeMs = 0
|
|
124
|
+
|
|
125
|
+
constructor(config: QueryEngineConfig) {
|
|
126
|
+
this.config = config
|
|
127
|
+
this.client = new Anthropic({
|
|
128
|
+
apiKey: config.apiKey,
|
|
129
|
+
baseURL: config.baseURL,
|
|
130
|
+
})
|
|
131
|
+
this.compactState = createAutoCompactState()
|
|
132
|
+
this.sessionId = crypto.randomUUID()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Submit a user message and run the agentic loop.
|
|
137
|
+
* Yields SDKMessage events as the agent works.
|
|
138
|
+
*/
|
|
139
|
+
async *submitMessage(
|
|
140
|
+
prompt: string | Anthropic.ContentBlockParam[],
|
|
141
|
+
): AsyncGenerator<SDKMessage> {
|
|
142
|
+
// Add user message
|
|
143
|
+
this.messages.push({ role: 'user', content: prompt })
|
|
144
|
+
|
|
145
|
+
// Build tool definitions for API
|
|
146
|
+
const tools = this.config.tools.map(toApiTool)
|
|
147
|
+
|
|
148
|
+
// Build system prompt
|
|
149
|
+
const systemPrompt = await buildSystemPrompt(this.config)
|
|
150
|
+
|
|
151
|
+
// Emit init system message
|
|
152
|
+
yield {
|
|
153
|
+
type: 'system',
|
|
154
|
+
subtype: 'init',
|
|
155
|
+
session_id: this.sessionId,
|
|
156
|
+
tools: this.config.tools.map(t => t.name),
|
|
157
|
+
model: this.config.model,
|
|
158
|
+
cwd: this.config.cwd,
|
|
159
|
+
mcp_servers: [],
|
|
160
|
+
permission_mode: 'bypassPermissions',
|
|
161
|
+
} as SDKMessage
|
|
162
|
+
|
|
163
|
+
// Agentic loop
|
|
164
|
+
let turnsRemaining = this.config.maxTurns
|
|
165
|
+
let budgetExceeded = false
|
|
166
|
+
let maxOutputRecoveryAttempts = 0
|
|
167
|
+
const MAX_OUTPUT_RECOVERY = 3
|
|
168
|
+
|
|
169
|
+
while (turnsRemaining > 0) {
|
|
170
|
+
if (this.config.abortSignal?.aborted) break
|
|
171
|
+
|
|
172
|
+
// Check budget
|
|
173
|
+
if (this.config.maxBudgetUsd && this.totalCost >= this.config.maxBudgetUsd) {
|
|
174
|
+
budgetExceeded = true
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Auto-compact if context is too large
|
|
179
|
+
if (shouldAutoCompact(this.messages, this.config.model, this.compactState)) {
|
|
180
|
+
try {
|
|
181
|
+
const result = await compactConversation(
|
|
182
|
+
this.client,
|
|
183
|
+
this.config.model,
|
|
184
|
+
this.messages,
|
|
185
|
+
this.compactState,
|
|
186
|
+
)
|
|
187
|
+
this.messages = result.compactedMessages
|
|
188
|
+
this.compactState = result.state
|
|
189
|
+
} catch {
|
|
190
|
+
// Continue with uncompacted messages
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Micro-compact: truncate large tool results
|
|
195
|
+
const apiMessages = microCompactMessages(
|
|
196
|
+
normalizeMessagesForAPI(this.messages),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
this.turnCount++
|
|
200
|
+
turnsRemaining--
|
|
201
|
+
|
|
202
|
+
// Make API call with retry
|
|
203
|
+
let response: Anthropic.Message
|
|
204
|
+
const apiStart = performance.now()
|
|
205
|
+
try {
|
|
206
|
+
response = await withRetry(
|
|
207
|
+
async () => {
|
|
208
|
+
const requestParams: Anthropic.MessageCreateParamsNonStreaming = {
|
|
209
|
+
model: this.config.model,
|
|
210
|
+
max_tokens: this.config.maxTokens,
|
|
211
|
+
system: systemPrompt,
|
|
212
|
+
messages: apiMessages,
|
|
213
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add thinking if configured
|
|
217
|
+
if (
|
|
218
|
+
this.config.thinking?.type === 'enabled' &&
|
|
219
|
+
this.config.thinking.budgetTokens
|
|
220
|
+
) {
|
|
221
|
+
(requestParams as any).thinking = {
|
|
222
|
+
type: 'enabled',
|
|
223
|
+
budget_tokens: this.config.thinking.budgetTokens,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return this.client.messages.create(requestParams)
|
|
228
|
+
},
|
|
229
|
+
undefined,
|
|
230
|
+
this.config.abortSignal,
|
|
231
|
+
)
|
|
232
|
+
} catch (err: any) {
|
|
233
|
+
// Handle prompt-too-long by compacting
|
|
234
|
+
if (isPromptTooLongError(err) && !this.compactState.compacted) {
|
|
235
|
+
try {
|
|
236
|
+
const result = await compactConversation(
|
|
237
|
+
this.client,
|
|
238
|
+
this.config.model,
|
|
239
|
+
this.messages,
|
|
240
|
+
this.compactState,
|
|
241
|
+
)
|
|
242
|
+
this.messages = result.compactedMessages
|
|
243
|
+
this.compactState = result.state
|
|
244
|
+
turnsRemaining++ // Retry this turn
|
|
245
|
+
this.turnCount--
|
|
246
|
+
continue
|
|
247
|
+
} catch {
|
|
248
|
+
// Can't compact, give up
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
yield {
|
|
253
|
+
type: 'result',
|
|
254
|
+
subtype: 'error',
|
|
255
|
+
usage: this.totalUsage,
|
|
256
|
+
num_turns: this.turnCount,
|
|
257
|
+
cost: this.totalCost,
|
|
258
|
+
}
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Track API timing
|
|
263
|
+
this.apiTimeMs += performance.now() - apiStart
|
|
264
|
+
|
|
265
|
+
// Track usage
|
|
266
|
+
if (response.usage) {
|
|
267
|
+
this.totalUsage.input_tokens += response.usage.input_tokens
|
|
268
|
+
this.totalUsage.output_tokens += response.usage.output_tokens
|
|
269
|
+
if ('cache_creation_input_tokens' in response.usage) {
|
|
270
|
+
this.totalUsage.cache_creation_input_tokens =
|
|
271
|
+
(this.totalUsage.cache_creation_input_tokens || 0) +
|
|
272
|
+
((response.usage as any).cache_creation_input_tokens || 0)
|
|
273
|
+
}
|
|
274
|
+
if ('cache_read_input_tokens' in response.usage) {
|
|
275
|
+
this.totalUsage.cache_read_input_tokens =
|
|
276
|
+
(this.totalUsage.cache_read_input_tokens || 0) +
|
|
277
|
+
((response.usage as any).cache_read_input_tokens || 0)
|
|
278
|
+
}
|
|
279
|
+
this.totalCost += estimateCost(this.config.model, response.usage as TokenUsage)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add assistant message to conversation
|
|
283
|
+
this.messages.push({ role: 'assistant', content: response.content })
|
|
284
|
+
|
|
285
|
+
// Yield assistant message
|
|
286
|
+
yield {
|
|
287
|
+
type: 'assistant',
|
|
288
|
+
message: {
|
|
289
|
+
role: 'assistant',
|
|
290
|
+
content: response.content,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle max_output_tokens recovery
|
|
295
|
+
if (
|
|
296
|
+
response.stop_reason === 'max_tokens' &&
|
|
297
|
+
maxOutputRecoveryAttempts < MAX_OUTPUT_RECOVERY
|
|
298
|
+
) {
|
|
299
|
+
maxOutputRecoveryAttempts++
|
|
300
|
+
// Add continuation prompt
|
|
301
|
+
this.messages.push({
|
|
302
|
+
role: 'user',
|
|
303
|
+
content: 'Please continue from where you left off.',
|
|
304
|
+
})
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check for tool use
|
|
309
|
+
const toolUseBlocks = response.content.filter(
|
|
310
|
+
(block): block is Anthropic.ToolUseBlock => block.type === 'tool_use',
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if (toolUseBlocks.length === 0) {
|
|
314
|
+
break // No tool calls - agent is done
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Reset max_output recovery counter on successful tool use
|
|
318
|
+
maxOutputRecoveryAttempts = 0
|
|
319
|
+
|
|
320
|
+
// Execute tools (concurrent read-only, serial mutations)
|
|
321
|
+
const toolResults = await this.executeTools(toolUseBlocks)
|
|
322
|
+
|
|
323
|
+
// Yield tool results
|
|
324
|
+
for (const result of toolResults) {
|
|
325
|
+
yield {
|
|
326
|
+
type: 'tool_result',
|
|
327
|
+
result: {
|
|
328
|
+
tool_use_id: result.tool_use_id,
|
|
329
|
+
tool_name: result.tool_name || '',
|
|
330
|
+
output:
|
|
331
|
+
typeof result.content === 'string'
|
|
332
|
+
? result.content
|
|
333
|
+
: JSON.stringify(result.content),
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add tool results to conversation
|
|
339
|
+
this.messages.push({
|
|
340
|
+
role: 'user',
|
|
341
|
+
content: toolResults.map((r) => ({
|
|
342
|
+
type: 'tool_result' as const,
|
|
343
|
+
tool_use_id: r.tool_use_id,
|
|
344
|
+
content:
|
|
345
|
+
typeof r.content === 'string'
|
|
346
|
+
? r.content
|
|
347
|
+
: JSON.stringify(r.content),
|
|
348
|
+
is_error: r.is_error,
|
|
349
|
+
})),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
if (response.stop_reason === 'end_turn') break
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Yield enriched final result
|
|
356
|
+
const endSubtype = budgetExceeded
|
|
357
|
+
? 'error_max_budget_usd'
|
|
358
|
+
: turnsRemaining <= 0
|
|
359
|
+
? 'error_max_turns'
|
|
360
|
+
: 'success'
|
|
361
|
+
|
|
362
|
+
yield {
|
|
363
|
+
type: 'result',
|
|
364
|
+
subtype: endSubtype,
|
|
365
|
+
session_id: this.sessionId,
|
|
366
|
+
is_error: endSubtype !== 'success',
|
|
367
|
+
num_turns: this.turnCount,
|
|
368
|
+
total_cost_usd: this.totalCost,
|
|
369
|
+
duration_api_ms: Math.round(this.apiTimeMs),
|
|
370
|
+
usage: this.totalUsage,
|
|
371
|
+
model_usage: { [this.config.model]: { input_tokens: this.totalUsage.input_tokens, output_tokens: this.totalUsage.output_tokens } },
|
|
372
|
+
cost: this.totalCost,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Execute tool calls with concurrency control.
|
|
378
|
+
*
|
|
379
|
+
* Read-only tools run concurrently (up to 10 at a time).
|
|
380
|
+
* Mutation tools run sequentially.
|
|
381
|
+
*/
|
|
382
|
+
private async executeTools(
|
|
383
|
+
toolUseBlocks: Anthropic.ToolUseBlock[],
|
|
384
|
+
): Promise<(ToolResult & { tool_name?: string })[]> {
|
|
385
|
+
const context: ToolContext = {
|
|
386
|
+
cwd: this.config.cwd,
|
|
387
|
+
abortSignal: this.config.abortSignal,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const MAX_CONCURRENCY = parseInt(
|
|
391
|
+
process.env.AGENT_SDK_MAX_TOOL_CONCURRENCY || '10',
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
// Partition into read-only (concurrent) and mutation (serial)
|
|
395
|
+
const readOnly: Array<{ block: Anthropic.ToolUseBlock; tool?: ToolDefinition }> = []
|
|
396
|
+
const mutations: Array<{ block: Anthropic.ToolUseBlock; tool?: ToolDefinition }> = []
|
|
397
|
+
|
|
398
|
+
for (const block of toolUseBlocks) {
|
|
399
|
+
const tool = this.config.tools.find((t) => t.name === block.name)
|
|
400
|
+
if (tool?.isReadOnly?.()) {
|
|
401
|
+
readOnly.push({ block, tool })
|
|
402
|
+
} else {
|
|
403
|
+
mutations.push({ block, tool })
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const results: (ToolResult & { tool_name?: string })[] = []
|
|
408
|
+
|
|
409
|
+
// Execute read-only tools concurrently (batched by MAX_CONCURRENCY)
|
|
410
|
+
for (let i = 0; i < readOnly.length; i += MAX_CONCURRENCY) {
|
|
411
|
+
const batch = readOnly.slice(i, i + MAX_CONCURRENCY)
|
|
412
|
+
const batchResults = await Promise.all(
|
|
413
|
+
batch.map((item) =>
|
|
414
|
+
this.executeSingleTool(item.block, item.tool, context),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
results.push(...batchResults)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Execute mutation tools sequentially
|
|
421
|
+
for (const item of mutations) {
|
|
422
|
+
const result = await this.executeSingleTool(item.block, item.tool, context)
|
|
423
|
+
results.push(result)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return results
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Execute a single tool with permission checking.
|
|
431
|
+
*/
|
|
432
|
+
private async executeSingleTool(
|
|
433
|
+
block: Anthropic.ToolUseBlock,
|
|
434
|
+
tool: ToolDefinition | undefined,
|
|
435
|
+
context: ToolContext,
|
|
436
|
+
): Promise<ToolResult & { tool_name?: string }> {
|
|
437
|
+
if (!tool) {
|
|
438
|
+
return {
|
|
439
|
+
type: 'tool_result',
|
|
440
|
+
tool_use_id: block.id,
|
|
441
|
+
content: `Error: Unknown tool "${block.name}"`,
|
|
442
|
+
is_error: true,
|
|
443
|
+
tool_name: block.name,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check enabled
|
|
448
|
+
if (tool.isEnabled && !tool.isEnabled()) {
|
|
449
|
+
return {
|
|
450
|
+
type: 'tool_result',
|
|
451
|
+
tool_use_id: block.id,
|
|
452
|
+
content: `Error: Tool "${block.name}" is not enabled`,
|
|
453
|
+
is_error: true,
|
|
454
|
+
tool_name: block.name,
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Check permissions
|
|
459
|
+
if (this.config.canUseTool) {
|
|
460
|
+
try {
|
|
461
|
+
const permission = await this.config.canUseTool(tool, block.input)
|
|
462
|
+
if (permission.behavior === 'deny') {
|
|
463
|
+
return {
|
|
464
|
+
type: 'tool_result',
|
|
465
|
+
tool_use_id: block.id,
|
|
466
|
+
content: permission.message || `Permission denied for tool "${block.name}"`,
|
|
467
|
+
is_error: true,
|
|
468
|
+
tool_name: block.name,
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (permission.updatedInput !== undefined) {
|
|
472
|
+
block = { ...block, input: permission.updatedInput as Record<string, unknown> }
|
|
473
|
+
}
|
|
474
|
+
} catch (err: any) {
|
|
475
|
+
return {
|
|
476
|
+
type: 'tool_result',
|
|
477
|
+
tool_use_id: block.id,
|
|
478
|
+
content: `Permission check error: ${err.message}`,
|
|
479
|
+
is_error: true,
|
|
480
|
+
tool_name: block.name,
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Execute the tool
|
|
486
|
+
try {
|
|
487
|
+
const result = await tool.call(block.input, context)
|
|
488
|
+
return { ...result, tool_use_id: block.id, tool_name: block.name }
|
|
489
|
+
} catch (err: any) {
|
|
490
|
+
return {
|
|
491
|
+
type: 'tool_result',
|
|
492
|
+
tool_use_id: block.id,
|
|
493
|
+
content: `Tool execution error: ${err.message}`,
|
|
494
|
+
is_error: true,
|
|
495
|
+
tool_name: block.name,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get current messages for session persistence.
|
|
502
|
+
*/
|
|
503
|
+
getMessages(): Anthropic.MessageParam[] {
|
|
504
|
+
return [...this.messages]
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get total usage across all turns.
|
|
509
|
+
*/
|
|
510
|
+
getUsage(): TokenUsage {
|
|
511
|
+
return { ...this.totalUsage }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get total cost.
|
|
516
|
+
*/
|
|
517
|
+
getCost(): number {
|
|
518
|
+
return this.totalCost
|
|
519
|
+
}
|
|
520
|
+
}
|