@codeany/open-agent-sdk 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.
- package/README.md +144 -31
- package/examples/12-skills.ts +88 -0
- package/examples/13-hooks.ts +88 -0
- package/examples/14-openai-compat.ts +71 -0
- package/package.json +1 -1
- package/src/agent.ts +106 -15
- package/src/engine.ts +169 -59
- package/src/index.ts +51 -1
- package/src/providers/anthropic.ts +60 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/openai.ts +315 -0
- package/src/providers/types.ts +85 -0
- package/src/session.ts +5 -5
- package/src/skills/bundled/commit.ts +38 -0
- package/src/skills/bundled/debug.ts +48 -0
- package/src/skills/bundled/index.ts +28 -0
- package/src/skills/bundled/review.ts +41 -0
- package/src/skills/bundled/simplify.ts +51 -0
- package/src/skills/bundled/test.ts +43 -0
- package/src/skills/index.ts +25 -0
- package/src/skills/registry.ts +133 -0
- package/src/skills/types.ts +99 -0
- package/src/tools/agent-tool.ts +13 -2
- package/src/tools/index.ts +8 -0
- package/src/tools/skill-tool.ts +133 -0
- package/src/tools/types.ts +7 -3
- package/src/types.ts +35 -8
- package/src/utils/compact.ts +18 -17
- package/src/utils/messages.ts +12 -13
- package/src/utils/tokens.ts +29 -6
package/src/agent.ts
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* import { createAgent } from 'open-agent-sdk'
|
|
9
9
|
* const agent = createAgent({ model: 'claude-sonnet-4-6' })
|
|
10
10
|
* for await (const event of agent.query('Hello')) { ... }
|
|
11
|
+
*
|
|
12
|
+
* // OpenAI-compatible models
|
|
13
|
+
* const agent = createAgent({
|
|
14
|
+
* apiType: 'openai-completions',
|
|
15
|
+
* model: 'gpt-4o',
|
|
16
|
+
* apiKey: 'sk-...',
|
|
17
|
+
* baseURL: 'https://api.openai.com/v1',
|
|
18
|
+
* })
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import type {
|
|
@@ -17,7 +25,6 @@ import type {
|
|
|
17
25
|
ToolDefinition,
|
|
18
26
|
CanUseToolFn,
|
|
19
27
|
Message,
|
|
20
|
-
TokenUsage,
|
|
21
28
|
PermissionMode,
|
|
22
29
|
} from './types.js'
|
|
23
30
|
import { QueryEngine } from './engine.js'
|
|
@@ -29,7 +36,10 @@ import {
|
|
|
29
36
|
saveSession,
|
|
30
37
|
loadSession,
|
|
31
38
|
} from './session.js'
|
|
32
|
-
import type
|
|
39
|
+
import { createHookRegistry, type HookRegistry } from './hooks.js'
|
|
40
|
+
import { initBundledSkills } from './skills/index.js'
|
|
41
|
+
import { createProvider, type LLMProvider, type ApiType } from './providers/index.js'
|
|
42
|
+
import type { NormalizedMessageParam } from './providers/types.js'
|
|
33
43
|
|
|
34
44
|
// --------------------------------------------------------------------------
|
|
35
45
|
// Agent class
|
|
@@ -39,14 +49,17 @@ export class Agent {
|
|
|
39
49
|
private cfg: AgentOptions
|
|
40
50
|
private toolPool: ToolDefinition[]
|
|
41
51
|
private modelId: string
|
|
52
|
+
private apiType: ApiType
|
|
42
53
|
private apiCredentials: { key?: string; baseUrl?: string }
|
|
54
|
+
private provider: LLMProvider
|
|
43
55
|
private mcpLinks: MCPConnection[] = []
|
|
44
|
-
private history:
|
|
56
|
+
private history: NormalizedMessageParam[] = []
|
|
45
57
|
private messageLog: Message[] = []
|
|
46
58
|
private setupDone: Promise<void>
|
|
47
59
|
private sid: string
|
|
48
60
|
private abortCtrl: AbortController | null = null
|
|
49
61
|
private currentEngine: QueryEngine | null = null
|
|
62
|
+
private hookRegistry: HookRegistry
|
|
50
63
|
|
|
51
64
|
constructor(options: AgentOptions = {}) {
|
|
52
65
|
// Shallow copy to avoid mutating caller's object
|
|
@@ -57,13 +70,38 @@ export class Agent {
|
|
|
57
70
|
this.modelId = this.cfg.model ?? this.readEnv('CODEANY_MODEL') ?? 'claude-sonnet-4-6'
|
|
58
71
|
this.sid = this.cfg.sessionId ?? crypto.randomUUID()
|
|
59
72
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
// Resolve API type
|
|
74
|
+
this.apiType = this.resolveApiType()
|
|
75
|
+
|
|
76
|
+
// Create LLM provider
|
|
77
|
+
this.provider = createProvider(this.apiType, {
|
|
78
|
+
apiKey: this.apiCredentials.key,
|
|
79
|
+
baseURL: this.apiCredentials.baseUrl,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Initialize bundled skills
|
|
83
|
+
initBundledSkills()
|
|
84
|
+
|
|
85
|
+
// Build hook registry from options
|
|
86
|
+
this.hookRegistry = createHookRegistry()
|
|
87
|
+
if (this.cfg.hooks) {
|
|
88
|
+
// Convert AgentOptions hooks format to HookConfig
|
|
89
|
+
for (const [event, defs] of Object.entries(this.cfg.hooks)) {
|
|
90
|
+
for (const def of defs) {
|
|
91
|
+
for (const handler of def.hooks) {
|
|
92
|
+
this.hookRegistry.register(event as any, {
|
|
93
|
+
matcher: def.matcher,
|
|
94
|
+
timeout: def.timeout,
|
|
95
|
+
handler: async (input) => {
|
|
96
|
+
const result = await handler(input, input.toolUseId || '', {
|
|
97
|
+
signal: this.abortCtrl?.signal || new AbortController().signal,
|
|
98
|
+
})
|
|
99
|
+
return result || undefined
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
67
105
|
}
|
|
68
106
|
|
|
69
107
|
// Build tool pool from options (supports ToolDefinition[], string[], or preset)
|
|
@@ -73,6 +111,41 @@ export class Agent {
|
|
|
73
111
|
this.setupDone = this.setup()
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Resolve API type from options, env, or model name heuristic.
|
|
116
|
+
*/
|
|
117
|
+
private resolveApiType(): ApiType {
|
|
118
|
+
// Explicit option
|
|
119
|
+
if (this.cfg.apiType) return this.cfg.apiType
|
|
120
|
+
|
|
121
|
+
// Env var
|
|
122
|
+
const envType =
|
|
123
|
+
this.cfg.env?.CODEANY_API_TYPE ??
|
|
124
|
+
this.readEnv('CODEANY_API_TYPE')
|
|
125
|
+
if (envType === 'openai-completions' || envType === 'anthropic-messages') {
|
|
126
|
+
return envType
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Heuristic from model name
|
|
130
|
+
const model = this.modelId.toLowerCase()
|
|
131
|
+
if (
|
|
132
|
+
model.includes('gpt-') ||
|
|
133
|
+
model.includes('o1') ||
|
|
134
|
+
model.includes('o3') ||
|
|
135
|
+
model.includes('o4') ||
|
|
136
|
+
model.includes('deepseek') ||
|
|
137
|
+
model.includes('qwen') ||
|
|
138
|
+
model.includes('yi-') ||
|
|
139
|
+
model.includes('glm') ||
|
|
140
|
+
model.includes('mistral') ||
|
|
141
|
+
model.includes('gemma')
|
|
142
|
+
) {
|
|
143
|
+
return 'openai-completions'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return 'anthropic-messages'
|
|
147
|
+
}
|
|
148
|
+
|
|
76
149
|
/** Pick API key and base URL from options or CODEANY_* env vars. */
|
|
77
150
|
private pickCredentials(): { key?: string; baseUrl?: string } {
|
|
78
151
|
const envMap = this.cfg.env
|
|
@@ -208,12 +281,21 @@ export class Agent {
|
|
|
208
281
|
}
|
|
209
282
|
}
|
|
210
283
|
|
|
284
|
+
// Recreate provider if overrides change credentials or apiType
|
|
285
|
+
let provider = this.provider
|
|
286
|
+
if (overrides?.apiType || overrides?.apiKey || overrides?.baseURL) {
|
|
287
|
+
const resolvedApiType = overrides.apiType ?? this.apiType
|
|
288
|
+
provider = createProvider(resolvedApiType, {
|
|
289
|
+
apiKey: overrides.apiKey ?? this.apiCredentials.key,
|
|
290
|
+
baseURL: overrides.baseURL ?? this.apiCredentials.baseUrl,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
211
294
|
// Create query engine with current conversation state
|
|
212
295
|
const engine = new QueryEngine({
|
|
213
296
|
cwd,
|
|
214
297
|
model: opts.model || this.modelId,
|
|
215
|
-
|
|
216
|
-
baseURL: this.apiCredentials.baseUrl,
|
|
298
|
+
provider,
|
|
217
299
|
tools,
|
|
218
300
|
systemPrompt,
|
|
219
301
|
appendSystemPrompt,
|
|
@@ -226,6 +308,8 @@ export class Agent {
|
|
|
226
308
|
includePartialMessages: opts.includePartialMessages ?? false,
|
|
227
309
|
abortSignal: this.abortCtrl.signal,
|
|
228
310
|
agents: opts.agents,
|
|
311
|
+
hookRegistry: this.hookRegistry,
|
|
312
|
+
sessionId: this.sid,
|
|
229
313
|
})
|
|
230
314
|
this.currentEngine = engine
|
|
231
315
|
|
|
@@ -279,9 +363,9 @@ export class Agent {
|
|
|
279
363
|
switch (ev.type) {
|
|
280
364
|
case 'assistant': {
|
|
281
365
|
// Extract the last assistant text (multi-turn: only final answer matters)
|
|
282
|
-
const fragments = ev.message.content
|
|
283
|
-
.filter((c
|
|
284
|
-
.map((c) => c.text)
|
|
366
|
+
const fragments = (ev.message.content as any[])
|
|
367
|
+
.filter((c: any) => c.type === 'text')
|
|
368
|
+
.map((c: any) => c.text)
|
|
285
369
|
if (fragments.length) collected.text = fragments.join('')
|
|
286
370
|
break
|
|
287
371
|
}
|
|
@@ -359,6 +443,13 @@ export class Agent {
|
|
|
359
443
|
return this.sid
|
|
360
444
|
}
|
|
361
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Get the current API type.
|
|
448
|
+
*/
|
|
449
|
+
getApiType(): ApiType {
|
|
450
|
+
return this.apiType
|
|
451
|
+
}
|
|
452
|
+
|
|
362
453
|
/**
|
|
363
454
|
* Stop a background task.
|
|
364
455
|
*/
|
package/src/engine.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages the full conversation lifecycle:
|
|
5
5
|
* 1. Take user prompt
|
|
6
6
|
* 2. Build system prompt with context (git status, project context, tools)
|
|
7
|
-
* 3. Call LLM API with tools
|
|
7
|
+
* 3. Call LLM API with tools (via provider abstraction)
|
|
8
8
|
* 4. Stream response
|
|
9
9
|
* 5. Execute tool calls (concurrent for read-only, serial for mutations)
|
|
10
10
|
* 6. Send results back, repeat until done
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* 8. Retry with exponential backoff on transient errors
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import Anthropic from '@anthropic-ai/sdk'
|
|
16
15
|
import type {
|
|
17
16
|
SDKMessage,
|
|
18
17
|
QueryEngineConfig,
|
|
@@ -21,7 +20,12 @@ import type {
|
|
|
21
20
|
ToolContext,
|
|
22
21
|
TokenUsage,
|
|
23
22
|
} from './types.js'
|
|
24
|
-
import {
|
|
23
|
+
import type {
|
|
24
|
+
LLMProvider,
|
|
25
|
+
CreateMessageResponse,
|
|
26
|
+
NormalizedMessageParam,
|
|
27
|
+
NormalizedTool,
|
|
28
|
+
} from './providers/types.js'
|
|
25
29
|
import {
|
|
26
30
|
estimateMessagesTokens,
|
|
27
31
|
estimateCost,
|
|
@@ -37,10 +41,34 @@ import {
|
|
|
37
41
|
import {
|
|
38
42
|
withRetry,
|
|
39
43
|
isPromptTooLongError,
|
|
40
|
-
formatApiError,
|
|
41
44
|
} from './utils/retry.js'
|
|
42
45
|
import { getSystemContext, getUserContext } from './utils/context.js'
|
|
43
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
|
+
}
|
|
44
72
|
|
|
45
73
|
// ============================================================================
|
|
46
74
|
// System Prompt Builder
|
|
@@ -113,23 +141,43 @@ async function buildSystemPrompt(config: QueryEngineConfig): Promise<string> {
|
|
|
113
141
|
|
|
114
142
|
export class QueryEngine {
|
|
115
143
|
private config: QueryEngineConfig
|
|
116
|
-
private
|
|
117
|
-
public messages:
|
|
144
|
+
private provider: LLMProvider
|
|
145
|
+
public messages: NormalizedMessageParam[] = []
|
|
118
146
|
private totalUsage: TokenUsage = { input_tokens: 0, output_tokens: 0 }
|
|
119
147
|
private totalCost = 0
|
|
120
148
|
private turnCount = 0
|
|
121
149
|
private compactState: AutoCompactState
|
|
122
150
|
private sessionId: string
|
|
123
151
|
private apiTimeMs = 0
|
|
152
|
+
private hookRegistry?: HookRegistry
|
|
124
153
|
|
|
125
154
|
constructor(config: QueryEngineConfig) {
|
|
126
155
|
this.config = config
|
|
127
|
-
this.
|
|
128
|
-
apiKey: config.apiKey,
|
|
129
|
-
baseURL: config.baseURL,
|
|
130
|
-
})
|
|
156
|
+
this.provider = config.provider
|
|
131
157
|
this.compactState = createAutoCompactState()
|
|
132
|
-
this.sessionId = crypto.randomUUID()
|
|
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
|
+
}
|
|
133
181
|
}
|
|
134
182
|
|
|
135
183
|
/**
|
|
@@ -137,13 +185,34 @@ export class QueryEngine {
|
|
|
137
185
|
* Yields SDKMessage events as the agent works.
|
|
138
186
|
*/
|
|
139
187
|
async *submitMessage(
|
|
140
|
-
prompt: string |
|
|
188
|
+
prompt: string | any[],
|
|
141
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
|
+
|
|
142
211
|
// Add user message
|
|
143
|
-
this.messages.push({ role: 'user', content: prompt })
|
|
212
|
+
this.messages.push({ role: 'user', content: prompt as any })
|
|
144
213
|
|
|
145
|
-
// Build tool definitions for
|
|
146
|
-
const tools = this.config.tools.map(
|
|
214
|
+
// Build tool definitions for provider
|
|
215
|
+
const tools = this.config.tools.map(toProviderTool)
|
|
147
216
|
|
|
148
217
|
// Build system prompt
|
|
149
218
|
const systemPrompt = await buildSystemPrompt(this.config)
|
|
@@ -176,16 +245,18 @@ export class QueryEngine {
|
|
|
176
245
|
}
|
|
177
246
|
|
|
178
247
|
// Auto-compact if context is too large
|
|
179
|
-
if (shouldAutoCompact(this.messages, this.config.model, this.compactState)) {
|
|
248
|
+
if (shouldAutoCompact(this.messages as any[], this.config.model, this.compactState)) {
|
|
249
|
+
await this.executeHooks('PreCompact')
|
|
180
250
|
try {
|
|
181
251
|
const result = await compactConversation(
|
|
182
|
-
this.
|
|
252
|
+
this.provider,
|
|
183
253
|
this.config.model,
|
|
184
|
-
this.messages,
|
|
254
|
+
this.messages as any[],
|
|
185
255
|
this.compactState,
|
|
186
256
|
)
|
|
187
|
-
this.messages = result.compactedMessages
|
|
257
|
+
this.messages = result.compactedMessages as NormalizedMessageParam[]
|
|
188
258
|
this.compactState = result.state
|
|
259
|
+
await this.executeHooks('PostCompact')
|
|
189
260
|
} catch {
|
|
190
261
|
// Continue with uncompacted messages
|
|
191
262
|
}
|
|
@@ -193,38 +264,33 @@ export class QueryEngine {
|
|
|
193
264
|
|
|
194
265
|
// Micro-compact: truncate large tool results
|
|
195
266
|
const apiMessages = microCompactMessages(
|
|
196
|
-
normalizeMessagesForAPI(this.messages),
|
|
197
|
-
)
|
|
267
|
+
normalizeMessagesForAPI(this.messages as any[]),
|
|
268
|
+
) as NormalizedMessageParam[]
|
|
198
269
|
|
|
199
270
|
this.turnCount++
|
|
200
271
|
turnsRemaining--
|
|
201
272
|
|
|
202
|
-
// Make API call with retry
|
|
203
|
-
let response:
|
|
273
|
+
// Make API call with retry via provider
|
|
274
|
+
let response: CreateMessageResponse
|
|
204
275
|
const apiStart = performance.now()
|
|
205
276
|
try {
|
|
206
277
|
response = await withRetry(
|
|
207
278
|
async () => {
|
|
208
|
-
|
|
279
|
+
return this.provider.createMessage({
|
|
209
280
|
model: this.config.model,
|
|
210
|
-
|
|
281
|
+
maxTokens: this.config.maxTokens,
|
|
211
282
|
system: systemPrompt,
|
|
212
283
|
messages: apiMessages,
|
|
213
284
|
tools: tools.length > 0 ? tools : undefined,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
budget_tokens: this.config.thinking.budgetTokens,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return this.client.messages.create(requestParams)
|
|
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
|
+
})
|
|
228
294
|
},
|
|
229
295
|
undefined,
|
|
230
296
|
this.config.abortSignal,
|
|
@@ -234,12 +300,12 @@ export class QueryEngine {
|
|
|
234
300
|
if (isPromptTooLongError(err) && !this.compactState.compacted) {
|
|
235
301
|
try {
|
|
236
302
|
const result = await compactConversation(
|
|
237
|
-
this.
|
|
303
|
+
this.provider,
|
|
238
304
|
this.config.model,
|
|
239
|
-
this.messages,
|
|
305
|
+
this.messages as any[],
|
|
240
306
|
this.compactState,
|
|
241
307
|
)
|
|
242
|
-
this.messages = result.compactedMessages
|
|
308
|
+
this.messages = result.compactedMessages as NormalizedMessageParam[]
|
|
243
309
|
this.compactState = result.state
|
|
244
310
|
turnsRemaining++ // Retry this turn
|
|
245
311
|
this.turnCount--
|
|
@@ -262,38 +328,38 @@ export class QueryEngine {
|
|
|
262
328
|
// Track API timing
|
|
263
329
|
this.apiTimeMs += performance.now() - apiStart
|
|
264
330
|
|
|
265
|
-
// Track usage
|
|
331
|
+
// Track usage (normalized by provider)
|
|
266
332
|
if (response.usage) {
|
|
267
333
|
this.totalUsage.input_tokens += response.usage.input_tokens
|
|
268
334
|
this.totalUsage.output_tokens += response.usage.output_tokens
|
|
269
|
-
if (
|
|
335
|
+
if (response.usage.cache_creation_input_tokens) {
|
|
270
336
|
this.totalUsage.cache_creation_input_tokens =
|
|
271
337
|
(this.totalUsage.cache_creation_input_tokens || 0) +
|
|
272
|
-
|
|
338
|
+
response.usage.cache_creation_input_tokens
|
|
273
339
|
}
|
|
274
|
-
if (
|
|
340
|
+
if (response.usage.cache_read_input_tokens) {
|
|
275
341
|
this.totalUsage.cache_read_input_tokens =
|
|
276
342
|
(this.totalUsage.cache_read_input_tokens || 0) +
|
|
277
|
-
|
|
343
|
+
response.usage.cache_read_input_tokens
|
|
278
344
|
}
|
|
279
|
-
this.totalCost += estimateCost(this.config.model, response.usage
|
|
345
|
+
this.totalCost += estimateCost(this.config.model, response.usage)
|
|
280
346
|
}
|
|
281
347
|
|
|
282
348
|
// Add assistant message to conversation
|
|
283
|
-
this.messages.push({ role: 'assistant', content: response.content })
|
|
349
|
+
this.messages.push({ role: 'assistant', content: response.content as any })
|
|
284
350
|
|
|
285
351
|
// Yield assistant message
|
|
286
352
|
yield {
|
|
287
353
|
type: 'assistant',
|
|
288
354
|
message: {
|
|
289
355
|
role: 'assistant',
|
|
290
|
-
content: response.content,
|
|
356
|
+
content: response.content as any,
|
|
291
357
|
},
|
|
292
358
|
}
|
|
293
359
|
|
|
294
360
|
// Handle max_output_tokens recovery
|
|
295
361
|
if (
|
|
296
|
-
response.
|
|
362
|
+
response.stopReason === 'max_tokens' &&
|
|
297
363
|
maxOutputRecoveryAttempts < MAX_OUTPUT_RECOVERY
|
|
298
364
|
) {
|
|
299
365
|
maxOutputRecoveryAttempts++
|
|
@@ -307,7 +373,7 @@ export class QueryEngine {
|
|
|
307
373
|
|
|
308
374
|
// Check for tool use
|
|
309
375
|
const toolUseBlocks = response.content.filter(
|
|
310
|
-
(block): block is
|
|
376
|
+
(block): block is ToolUseBlock => block.type === 'tool_use',
|
|
311
377
|
)
|
|
312
378
|
|
|
313
379
|
if (toolUseBlocks.length === 0) {
|
|
@@ -349,9 +415,15 @@ export class QueryEngine {
|
|
|
349
415
|
})),
|
|
350
416
|
})
|
|
351
417
|
|
|
352
|
-
if (response.
|
|
418
|
+
if (response.stopReason === 'end_turn') break
|
|
353
419
|
}
|
|
354
420
|
|
|
421
|
+
// Hook: Stop (end of agentic loop)
|
|
422
|
+
await this.executeHooks('Stop')
|
|
423
|
+
|
|
424
|
+
// Hook: SessionEnd
|
|
425
|
+
await this.executeHooks('SessionEnd')
|
|
426
|
+
|
|
355
427
|
// Yield enriched final result
|
|
356
428
|
const endSubtype = budgetExceeded
|
|
357
429
|
? 'error_max_budget_usd'
|
|
@@ -380,11 +452,14 @@ export class QueryEngine {
|
|
|
380
452
|
* Mutation tools run sequentially.
|
|
381
453
|
*/
|
|
382
454
|
private async executeTools(
|
|
383
|
-
toolUseBlocks:
|
|
455
|
+
toolUseBlocks: ToolUseBlock[],
|
|
384
456
|
): Promise<(ToolResult & { tool_name?: string })[]> {
|
|
385
457
|
const context: ToolContext = {
|
|
386
458
|
cwd: this.config.cwd,
|
|
387
459
|
abortSignal: this.config.abortSignal,
|
|
460
|
+
provider: this.provider,
|
|
461
|
+
model: this.config.model,
|
|
462
|
+
apiType: this.provider.apiType,
|
|
388
463
|
}
|
|
389
464
|
|
|
390
465
|
const MAX_CONCURRENCY = parseInt(
|
|
@@ -392,8 +467,8 @@ export class QueryEngine {
|
|
|
392
467
|
)
|
|
393
468
|
|
|
394
469
|
// Partition into read-only (concurrent) and mutation (serial)
|
|
395
|
-
const readOnly: Array<{ block:
|
|
396
|
-
const mutations: Array<{ block:
|
|
470
|
+
const readOnly: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
|
|
471
|
+
const mutations: Array<{ block: ToolUseBlock; tool?: ToolDefinition }> = []
|
|
397
472
|
|
|
398
473
|
for (const block of toolUseBlocks) {
|
|
399
474
|
const tool = this.config.tools.find((t) => t.name === block.name)
|
|
@@ -430,7 +505,7 @@ export class QueryEngine {
|
|
|
430
505
|
* Execute a single tool with permission checking.
|
|
431
506
|
*/
|
|
432
507
|
private async executeSingleTool(
|
|
433
|
-
block:
|
|
508
|
+
block: ToolUseBlock,
|
|
434
509
|
tool: ToolDefinition | undefined,
|
|
435
510
|
context: ToolContext,
|
|
436
511
|
): Promise<ToolResult & { tool_name?: string }> {
|
|
@@ -469,7 +544,7 @@ export class QueryEngine {
|
|
|
469
544
|
}
|
|
470
545
|
}
|
|
471
546
|
if (permission.updatedInput !== undefined) {
|
|
472
|
-
block = { ...block, input: permission.updatedInput
|
|
547
|
+
block = { ...block, input: permission.updatedInput }
|
|
473
548
|
}
|
|
474
549
|
} catch (err: any) {
|
|
475
550
|
return {
|
|
@@ -482,11 +557,46 @@ export class QueryEngine {
|
|
|
482
557
|
}
|
|
483
558
|
}
|
|
484
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
|
+
|
|
485
578
|
// Execute the tool
|
|
486
579
|
try {
|
|
487
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
|
+
|
|
488
590
|
return { ...result, tool_use_id: block.id, tool_name: block.name }
|
|
489
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
|
+
|
|
490
600
|
return {
|
|
491
601
|
type: 'tool_result',
|
|
492
602
|
tool_use_id: block.id,
|
|
@@ -500,7 +610,7 @@ export class QueryEngine {
|
|
|
500
610
|
/**
|
|
501
611
|
* Get current messages for session persistence.
|
|
502
612
|
*/
|
|
503
|
-
getMessages():
|
|
613
|
+
getMessages(): NormalizedMessageParam[] {
|
|
504
614
|
return [...this.messages]
|
|
505
615
|
}
|
|
506
616
|
|