@codeany/open-agent-sdk 0.2.1 → 0.2.3

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 (57) hide show
  1. package/dist/engine.d.ts.map +1 -1
  2. package/dist/engine.js +2 -0
  3. package/dist/engine.js.map +1 -1
  4. package/package.json +5 -6
  5. package/src/agent.ts +0 -516
  6. package/src/engine.ts +0 -630
  7. package/src/hooks.ts +0 -261
  8. package/src/index.ts +0 -426
  9. package/src/mcp/client.ts +0 -150
  10. package/src/providers/anthropic.ts +0 -60
  11. package/src/providers/index.ts +0 -34
  12. package/src/providers/openai.ts +0 -315
  13. package/src/providers/types.ts +0 -85
  14. package/src/sdk-mcp-server.ts +0 -78
  15. package/src/session.ts +0 -227
  16. package/src/skills/bundled/commit.ts +0 -38
  17. package/src/skills/bundled/debug.ts +0 -48
  18. package/src/skills/bundled/index.ts +0 -28
  19. package/src/skills/bundled/review.ts +0 -41
  20. package/src/skills/bundled/simplify.ts +0 -51
  21. package/src/skills/bundled/test.ts +0 -43
  22. package/src/skills/index.ts +0 -25
  23. package/src/skills/registry.ts +0 -133
  24. package/src/skills/types.ts +0 -99
  25. package/src/tool-helper.ts +0 -127
  26. package/src/tools/agent-tool.ts +0 -164
  27. package/src/tools/ask-user.ts +0 -79
  28. package/src/tools/bash.ts +0 -75
  29. package/src/tools/config-tool.ts +0 -89
  30. package/src/tools/cron-tools.ts +0 -153
  31. package/src/tools/edit.ts +0 -74
  32. package/src/tools/glob.ts +0 -77
  33. package/src/tools/grep.ts +0 -168
  34. package/src/tools/index.ts +0 -240
  35. package/src/tools/lsp-tool.ts +0 -163
  36. package/src/tools/mcp-resource-tools.ts +0 -125
  37. package/src/tools/notebook-edit.ts +0 -93
  38. package/src/tools/plan-tools.ts +0 -88
  39. package/src/tools/read.ts +0 -73
  40. package/src/tools/send-message.ts +0 -96
  41. package/src/tools/skill-tool.ts +0 -133
  42. package/src/tools/task-tools.ts +0 -290
  43. package/src/tools/team-tools.ts +0 -128
  44. package/src/tools/todo-tool.ts +0 -112
  45. package/src/tools/tool-search.ts +0 -87
  46. package/src/tools/types.ts +0 -66
  47. package/src/tools/web-fetch.ts +0 -66
  48. package/src/tools/web-search.ts +0 -86
  49. package/src/tools/worktree-tools.ts +0 -140
  50. package/src/tools/write.ts +0 -42
  51. package/src/types.ts +0 -486
  52. package/src/utils/compact.ts +0 -207
  53. package/src/utils/context.ts +0 -191
  54. package/src/utils/fileCache.ts +0 -148
  55. package/src/utils/messages.ts +0 -195
  56. package/src/utils/retry.ts +0 -140
  57. package/src/utils/tokens.ts +0 -145
package/src/engine.ts DELETED
@@ -1,630 +0,0 @@
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 (via provider abstraction)
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 type {
16
- SDKMessage,
17
- QueryEngineConfig,
18
- ToolDefinition,
19
- ToolResult,
20
- ToolContext,
21
- TokenUsage,
22
- } from './types.js'
23
- import type {
24
- LLMProvider,
25
- CreateMessageResponse,
26
- NormalizedMessageParam,
27
- NormalizedTool,
28
- } from './providers/types.js'
29
- import {
30
- estimateMessagesTokens,
31
- estimateCost,
32
- getAutoCompactThreshold,
33
- } from './utils/tokens.js'
34
- import {
35
- shouldAutoCompact,
36
- compactConversation,
37
- microCompactMessages,
38
- createAutoCompactState,
39
- type AutoCompactState,
40
- } from './utils/compact.js'
41
- import {
42
- withRetry,
43
- isPromptTooLongError,
44
- } from './utils/retry.js'
45
- import { getSystemContext, getUserContext } from './utils/context.js'
46
- import { normalizeMessagesForAPI } from './utils/messages.js'
47
- import type { HookRegistry, HookInput, HookOutput } from './hooks.js'
48
-
49
- // ============================================================================
50
- // Tool format conversion
51
- // ============================================================================
52
-
53
- /** Convert a ToolDefinition to the normalized provider tool format. */
54
- function toProviderTool(tool: ToolDefinition): NormalizedTool {
55
- return {
56
- name: tool.name,
57
- description: tool.description,
58
- input_schema: tool.inputSchema,
59
- }
60
- }
61
-
62
- // ============================================================================
63
- // ToolUseBlock (internal type for extracted tool_use blocks)
64
- // ============================================================================
65
-
66
- interface ToolUseBlock {
67
- type: 'tool_use'
68
- id: string
69
- name: string
70
- input: any
71
- }
72
-
73
- // ============================================================================
74
- // System Prompt Builder
75
- // ============================================================================
76
-
77
- async function buildSystemPrompt(config: QueryEngineConfig): Promise<string> {
78
- if (config.systemPrompt) {
79
- const base = config.systemPrompt
80
- return config.appendSystemPrompt
81
- ? base + '\n\n' + config.appendSystemPrompt
82
- : base
83
- }
84
-
85
- const parts: string[] = []
86
-
87
- parts.push(
88
- 'You are an AI assistant with access to tools. Use the tools provided to help the user accomplish their tasks.',
89
- 'You should use tools when they would help you complete the task more accurately or efficiently.',
90
- )
91
-
92
- // List available tools with descriptions
93
- parts.push('\n# Available Tools\n')
94
- for (const tool of config.tools) {
95
- parts.push(`- **${tool.name}**: ${tool.description}`)
96
- }
97
-
98
- // Add agent definitions
99
- if (config.agents && Object.keys(config.agents).length > 0) {
100
- parts.push('\n# Available Subagents\n')
101
- for (const [name, def] of Object.entries(config.agents)) {
102
- parts.push(`- **${name}**: ${def.description}`)
103
- }
104
- }
105
-
106
- // System context (git status, etc.)
107
- try {
108
- const sysCtx = await getSystemContext(config.cwd)
109
- if (sysCtx) {
110
- parts.push('\n# Environment\n')
111
- parts.push(sysCtx)
112
- }
113
- } catch {
114
- // Context is best-effort
115
- }
116
-
117
- // User context (AGENT.md, date)
118
- try {
119
- const userCtx = await getUserContext(config.cwd)
120
- if (userCtx) {
121
- parts.push('\n# Project Context\n')
122
- parts.push(userCtx)
123
- }
124
- } catch {
125
- // Context is best-effort
126
- }
127
-
128
- // Working directory
129
- parts.push(`\n# Working Directory\n${config.cwd}`)
130
-
131
- if (config.appendSystemPrompt) {
132
- parts.push('\n' + config.appendSystemPrompt)
133
- }
134
-
135
- return parts.join('\n')
136
- }
137
-
138
- // ============================================================================
139
- // QueryEngine
140
- // ============================================================================
141
-
142
- export class QueryEngine {
143
- private config: QueryEngineConfig
144
- private provider: LLMProvider
145
- public messages: NormalizedMessageParam[] = []
146
- private totalUsage: TokenUsage = { input_tokens: 0, output_tokens: 0 }
147
- private totalCost = 0
148
- private turnCount = 0
149
- private compactState: AutoCompactState
150
- private sessionId: string
151
- private apiTimeMs = 0
152
- private hookRegistry?: HookRegistry
153
-
154
- constructor(config: QueryEngineConfig) {
155
- this.config = config
156
- this.provider = config.provider
157
- this.compactState = createAutoCompactState()
158
- this.sessionId = config.sessionId || crypto.randomUUID()
159
- this.hookRegistry = config.hookRegistry
160
- }
161
-
162
- /**
163
- * Execute hooks for a lifecycle event.
164
- * Returns hook outputs; never throws.
165
- */
166
- private async executeHooks(
167
- event: import('./hooks.js').HookEvent,
168
- extra?: Partial<HookInput>,
169
- ): Promise<HookOutput[]> {
170
- if (!this.hookRegistry?.hasHooks(event)) return []
171
- try {
172
- return await this.hookRegistry.execute(event, {
173
- event,
174
- sessionId: this.sessionId,
175
- cwd: this.config.cwd,
176
- ...extra,
177
- })
178
- } catch {
179
- return []
180
- }
181
- }
182
-
183
- /**
184
- * Submit a user message and run the agentic loop.
185
- * Yields SDKMessage events as the agent works.
186
- */
187
- async *submitMessage(
188
- prompt: string | any[],
189
- ): AsyncGenerator<SDKMessage> {
190
- // Hook: SessionStart
191
- await this.executeHooks('SessionStart')
192
-
193
- // Hook: UserPromptSubmit
194
- const userHookResults = await this.executeHooks('UserPromptSubmit', {
195
- toolInput: prompt,
196
- })
197
- // Check if any hook blocks the submission
198
- if (userHookResults.some((r) => r.block)) {
199
- yield {
200
- type: 'result',
201
- subtype: 'error_during_execution',
202
- is_error: true,
203
- usage: this.totalUsage,
204
- num_turns: 0,
205
- cost: 0,
206
- errors: ['Blocked by UserPromptSubmit hook'],
207
- }
208
- return
209
- }
210
-
211
- // Add user message
212
- this.messages.push({ role: 'user', content: prompt as any })
213
-
214
- // Build tool definitions for provider
215
- const tools = this.config.tools.map(toProviderTool)
216
-
217
- // Build system prompt
218
- const systemPrompt = await buildSystemPrompt(this.config)
219
-
220
- // Emit init system message
221
- yield {
222
- type: 'system',
223
- subtype: 'init',
224
- session_id: this.sessionId,
225
- tools: this.config.tools.map(t => t.name),
226
- model: this.config.model,
227
- cwd: this.config.cwd,
228
- mcp_servers: [],
229
- permission_mode: 'bypassPermissions',
230
- } as SDKMessage
231
-
232
- // Agentic loop
233
- let turnsRemaining = this.config.maxTurns
234
- let budgetExceeded = false
235
- let maxOutputRecoveryAttempts = 0
236
- const MAX_OUTPUT_RECOVERY = 3
237
-
238
- while (turnsRemaining > 0) {
239
- if (this.config.abortSignal?.aborted) break
240
-
241
- // Check budget
242
- if (this.config.maxBudgetUsd && this.totalCost >= this.config.maxBudgetUsd) {
243
- budgetExceeded = true
244
- break
245
- }
246
-
247
- // Auto-compact if context is too large
248
- if (shouldAutoCompact(this.messages as any[], this.config.model, this.compactState)) {
249
- await this.executeHooks('PreCompact')
250
- try {
251
- const result = await compactConversation(
252
- this.provider,
253
- this.config.model,
254
- this.messages as any[],
255
- this.compactState,
256
- )
257
- this.messages = result.compactedMessages as NormalizedMessageParam[]
258
- this.compactState = result.state
259
- await this.executeHooks('PostCompact')
260
- } catch {
261
- // Continue with uncompacted messages
262
- }
263
- }
264
-
265
- // Micro-compact: truncate large tool results
266
- const apiMessages = microCompactMessages(
267
- normalizeMessagesForAPI(this.messages as any[]),
268
- ) as NormalizedMessageParam[]
269
-
270
- this.turnCount++
271
- turnsRemaining--
272
-
273
- // Make API call with retry via provider
274
- let response: CreateMessageResponse
275
- const apiStart = performance.now()
276
- try {
277
- response = await withRetry(
278
- async () => {
279
- return this.provider.createMessage({
280
- model: this.config.model,
281
- maxTokens: this.config.maxTokens,
282
- system: systemPrompt,
283
- messages: apiMessages,
284
- tools: tools.length > 0 ? tools : undefined,
285
- thinking:
286
- this.config.thinking?.type === 'enabled' &&
287
- this.config.thinking.budgetTokens
288
- ? {
289
- type: 'enabled',
290
- budget_tokens: this.config.thinking.budgetTokens,
291
- }
292
- : undefined,
293
- })
294
- },
295
- undefined,
296
- this.config.abortSignal,
297
- )
298
- } catch (err: any) {
299
- // Handle prompt-too-long by compacting
300
- if (isPromptTooLongError(err) && !this.compactState.compacted) {
301
- try {
302
- const result = await compactConversation(
303
- this.provider,
304
- this.config.model,
305
- this.messages as any[],
306
- this.compactState,
307
- )
308
- this.messages = result.compactedMessages as NormalizedMessageParam[]
309
- this.compactState = result.state
310
- turnsRemaining++ // Retry this turn
311
- this.turnCount--
312
- continue
313
- } catch {
314
- // Can't compact, give up
315
- }
316
- }
317
-
318
- yield {
319
- type: 'result',
320
- subtype: 'error',
321
- usage: this.totalUsage,
322
- num_turns: this.turnCount,
323
- cost: this.totalCost,
324
- }
325
- return
326
- }
327
-
328
- // Track API timing
329
- this.apiTimeMs += performance.now() - apiStart
330
-
331
- // Track usage (normalized by provider)
332
- if (response.usage) {
333
- this.totalUsage.input_tokens += response.usage.input_tokens
334
- this.totalUsage.output_tokens += response.usage.output_tokens
335
- if (response.usage.cache_creation_input_tokens) {
336
- this.totalUsage.cache_creation_input_tokens =
337
- (this.totalUsage.cache_creation_input_tokens || 0) +
338
- response.usage.cache_creation_input_tokens
339
- }
340
- if (response.usage.cache_read_input_tokens) {
341
- this.totalUsage.cache_read_input_tokens =
342
- (this.totalUsage.cache_read_input_tokens || 0) +
343
- response.usage.cache_read_input_tokens
344
- }
345
- this.totalCost += estimateCost(this.config.model, response.usage)
346
- }
347
-
348
- // Add assistant message to conversation
349
- this.messages.push({ role: 'assistant', content: response.content as any })
350
-
351
- // Yield assistant message
352
- yield {
353
- type: 'assistant',
354
- message: {
355
- role: 'assistant',
356
- content: response.content as any,
357
- },
358
- }
359
-
360
- // Handle max_output_tokens recovery
361
- if (
362
- response.stopReason === 'max_tokens' &&
363
- maxOutputRecoveryAttempts < MAX_OUTPUT_RECOVERY
364
- ) {
365
- maxOutputRecoveryAttempts++
366
- // Add continuation prompt
367
- this.messages.push({
368
- role: 'user',
369
- content: 'Please continue from where you left off.',
370
- })
371
- continue
372
- }
373
-
374
- // Check for tool use
375
- const toolUseBlocks = response.content.filter(
376
- (block): block is ToolUseBlock => block.type === 'tool_use',
377
- )
378
-
379
- if (toolUseBlocks.length === 0) {
380
- break // No tool calls - agent is done
381
- }
382
-
383
- // Reset max_output recovery counter on successful tool use
384
- maxOutputRecoveryAttempts = 0
385
-
386
- // Execute tools (concurrent read-only, serial mutations)
387
- const toolResults = await this.executeTools(toolUseBlocks)
388
-
389
- // Yield tool results
390
- for (const result of toolResults) {
391
- yield {
392
- type: 'tool_result',
393
- result: {
394
- tool_use_id: result.tool_use_id,
395
- tool_name: result.tool_name || '',
396
- output:
397
- typeof result.content === 'string'
398
- ? result.content
399
- : JSON.stringify(result.content),
400
- },
401
- }
402
- }
403
-
404
- // Add tool results to conversation
405
- this.messages.push({
406
- role: 'user',
407
- content: toolResults.map((r) => ({
408
- type: 'tool_result' as const,
409
- tool_use_id: r.tool_use_id,
410
- content:
411
- typeof r.content === 'string'
412
- ? r.content
413
- : JSON.stringify(r.content),
414
- is_error: r.is_error,
415
- })),
416
- })
417
-
418
- if (response.stopReason === 'end_turn') break
419
- }
420
-
421
- // Hook: Stop (end of agentic loop)
422
- await this.executeHooks('Stop')
423
-
424
- // Hook: SessionEnd
425
- await this.executeHooks('SessionEnd')
426
-
427
- // Yield enriched final result
428
- const endSubtype = budgetExceeded
429
- ? 'error_max_budget_usd'
430
- : turnsRemaining <= 0
431
- ? 'error_max_turns'
432
- : 'success'
433
-
434
- yield {
435
- type: 'result',
436
- subtype: endSubtype,
437
- session_id: this.sessionId,
438
- is_error: endSubtype !== 'success',
439
- num_turns: this.turnCount,
440
- total_cost_usd: this.totalCost,
441
- duration_api_ms: Math.round(this.apiTimeMs),
442
- usage: this.totalUsage,
443
- model_usage: { [this.config.model]: { input_tokens: this.totalUsage.input_tokens, output_tokens: this.totalUsage.output_tokens } },
444
- cost: this.totalCost,
445
- }
446
- }
447
-
448
- /**
449
- * Execute tool calls with concurrency control.
450
- *
451
- * Read-only tools run concurrently (up to 10 at a time).
452
- * Mutation tools run sequentially.
453
- */
454
- private async executeTools(
455
- toolUseBlocks: ToolUseBlock[],
456
- ): Promise<(ToolResult & { tool_name?: string })[]> {
457
- const context: ToolContext = {
458
- cwd: this.config.cwd,
459
- abortSignal: this.config.abortSignal,
460
- provider: this.provider,
461
- model: this.config.model,
462
- apiType: this.provider.apiType,
463
- }
464
-
465
- const MAX_CONCURRENCY = parseInt(
466
- process.env.AGENT_SDK_MAX_TOOL_CONCURRENCY || '10',
467
- )
468
-
469
- // Partition into read-only (concurrent) and mutation (serial)
470
- const readOnly: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
471
- const mutations: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
472
-
473
- for (const block of toolUseBlocks) {
474
- const tool = this.config.tools.find((t) => t.name === block.name)
475
- if (tool?.isReadOnly?.()) {
476
- readOnly.push({ block, tool })
477
- } else {
478
- mutations.push({ block, tool })
479
- }
480
- }
481
-
482
- const results: (ToolResult & { tool_name?: string })[] = []
483
-
484
- // Execute read-only tools concurrently (batched by MAX_CONCURRENCY)
485
- for (let i = 0; i < readOnly.length; i += MAX_CONCURRENCY) {
486
- const batch = readOnly.slice(i, i + MAX_CONCURRENCY)
487
- const batchResults = await Promise.all(
488
- batch.map((item) =>
489
- this.executeSingleTool(item.block, item.tool, context),
490
- ),
491
- )
492
- results.push(...batchResults)
493
- }
494
-
495
- // Execute mutation tools sequentially
496
- for (const item of mutations) {
497
- const result = await this.executeSingleTool(item.block, item.tool, context)
498
- results.push(result)
499
- }
500
-
501
- return results
502
- }
503
-
504
- /**
505
- * Execute a single tool with permission checking.
506
- */
507
- private async executeSingleTool(
508
- block: ToolUseBlock,
509
- tool: ToolDefinition | undefined,
510
- context: ToolContext,
511
- ): Promise<ToolResult & { tool_name?: string }> {
512
- if (!tool) {
513
- return {
514
- type: 'tool_result',
515
- tool_use_id: block.id,
516
- content: `Error: Unknown tool "${block.name}"`,
517
- is_error: true,
518
- tool_name: block.name,
519
- }
520
- }
521
-
522
- // Check enabled
523
- if (tool.isEnabled && !tool.isEnabled()) {
524
- return {
525
- type: 'tool_result',
526
- tool_use_id: block.id,
527
- content: `Error: Tool "${block.name}" is not enabled`,
528
- is_error: true,
529
- tool_name: block.name,
530
- }
531
- }
532
-
533
- // Check permissions
534
- if (this.config.canUseTool) {
535
- try {
536
- const permission = await this.config.canUseTool(tool, block.input)
537
- if (permission.behavior === 'deny') {
538
- return {
539
- type: 'tool_result',
540
- tool_use_id: block.id,
541
- content: permission.message || `Permission denied for tool "${block.name}"`,
542
- is_error: true,
543
- tool_name: block.name,
544
- }
545
- }
546
- if (permission.updatedInput !== undefined) {
547
- block = { ...block, input: permission.updatedInput }
548
- }
549
- } catch (err: any) {
550
- return {
551
- type: 'tool_result',
552
- tool_use_id: block.id,
553
- content: `Permission check error: ${err.message}`,
554
- is_error: true,
555
- tool_name: block.name,
556
- }
557
- }
558
- }
559
-
560
- // Hook: PreToolUse
561
- const preHookResults = await this.executeHooks('PreToolUse', {
562
- toolName: block.name,
563
- toolInput: block.input,
564
- toolUseId: block.id,
565
- })
566
- // Check if any hook blocks this tool
567
- if (preHookResults.some((r) => r.block)) {
568
- const msg = preHookResults.find((r) => r.message)?.message || 'Blocked by PreToolUse hook'
569
- return {
570
- type: 'tool_result',
571
- tool_use_id: block.id,
572
- content: msg,
573
- is_error: true,
574
- tool_name: block.name,
575
- }
576
- }
577
-
578
- // Execute the tool
579
- try {
580
- const result = await tool.call(block.input, context)
581
-
582
- // Hook: PostToolUse
583
- await this.executeHooks('PostToolUse', {
584
- toolName: block.name,
585
- toolInput: block.input,
586
- toolOutput: typeof result.content === 'string' ? result.content : JSON.stringify(result.content),
587
- toolUseId: block.id,
588
- })
589
-
590
- return { ...result, tool_use_id: block.id, tool_name: block.name }
591
- } catch (err: any) {
592
- // Hook: PostToolUseFailure
593
- await this.executeHooks('PostToolUseFailure', {
594
- toolName: block.name,
595
- toolInput: block.input,
596
- toolUseId: block.id,
597
- error: err.message,
598
- })
599
-
600
- return {
601
- type: 'tool_result',
602
- tool_use_id: block.id,
603
- content: `Tool execution error: ${err.message}`,
604
- is_error: true,
605
- tool_name: block.name,
606
- }
607
- }
608
- }
609
-
610
- /**
611
- * Get current messages for session persistence.
612
- */
613
- getMessages(): NormalizedMessageParam[] {
614
- return [...this.messages]
615
- }
616
-
617
- /**
618
- * Get total usage across all turns.
619
- */
620
- getUsage(): TokenUsage {
621
- return { ...this.totalUsage }
622
- }
623
-
624
- /**
625
- * Get total cost.
626
- */
627
- getCost(): number {
628
- return this.totalCost
629
- }
630
- }