@claudebuddy/claudebuddy-agent-sdk 0.4.0 → 0.5.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/CHANGELOG.md +36 -0
- package/README.md +75 -3
- package/dist/agent.d.ts +12 -22
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +209 -224
- package/dist/agent.js.map +1 -1
- package/dist/engine.d.ts +7 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +234 -224
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +3 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +13 -4
- package/dist/mcp/client.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +31 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/openai.d.ts +3 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +54 -3
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/sse.d.ts +3 -0
- package/dist/providers/sse.d.ts.map +1 -0
- package/dist/providers/sse.js +36 -0
- package/dist/providers/sse.js.map +1 -0
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/session.d.ts +10 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +98 -4
- package/dist/session.js.map +1 -1
- package/dist/tools/agent-tool.d.ts.map +1 -1
- package/dist/tools/agent-tool.js +32 -9
- package/dist/tools/agent-tool.js.map +1 -1
- package/dist/tools/ask-user.d.ts +3 -2
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +16 -15
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash.d.ts +0 -3
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +79 -59
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/config-tool.d.ts +4 -3
- package/dist/tools/config-tool.d.ts.map +1 -1
- package/dist/tools/config-tool.js +20 -16
- package/dist/tools/config-tool.js.map +1 -1
- package/dist/tools/cron-tools.d.ts +3 -2
- package/dist/tools/cron-tools.d.ts.map +1 -1
- package/dist/tools/cron-tools.js +33 -29
- package/dist/tools/cron-tools.js.map +1 -1
- package/dist/tools/index.js +55 -53
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lsp-tool.js +2 -2
- package/dist/tools/lsp-tool.js.map +1 -1
- package/dist/tools/mcp-resource-tools.d.ts +2 -1
- package/dist/tools/mcp-resource-tools.d.ts.map +1 -1
- package/dist/tools/mcp-resource-tools.js +44 -27
- package/dist/tools/mcp-resource-tools.js.map +1 -1
- package/dist/tools/plan-tools.d.ts +3 -2
- package/dist/tools/plan-tools.d.ts.map +1 -1
- package/dist/tools/plan-tools.js +24 -21
- package/dist/tools/plan-tools.js.map +1 -1
- package/dist/tools/send-message.d.ts +5 -4
- package/dist/tools/send-message.d.ts.map +1 -1
- package/dist/tools/send-message.js +24 -21
- package/dist/tools/send-message.js.map +1 -1
- package/dist/tools/session-state.d.ts +4 -0
- package/dist/tools/session-state.d.ts.map +1 -0
- package/dist/tools/session-state.js +9 -0
- package/dist/tools/session-state.js.map +1 -0
- package/dist/tools/task-tools.d.ts +19 -4
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +119 -31
- package/dist/tools/task-tools.js.map +1 -1
- package/dist/tools/team-tools.d.ts +7 -3
- package/dist/tools/team-tools.d.ts.map +1 -1
- package/dist/tools/team-tools.js +26 -22
- package/dist/tools/team-tools.js.map +1 -1
- package/dist/tools/todo-tool.d.ts +3 -2
- package/dist/tools/todo-tool.d.ts.map +1 -1
- package/dist/tools/todo-tool.js +24 -21
- package/dist/tools/todo-tool.js.map +1 -1
- package/dist/tools/tool-search.d.ts +2 -1
- package/dist/tools/tool-search.d.ts.map +1 -1
- package/dist/tools/tool-search.js +14 -13
- package/dist/tools/tool-search.js.map +1 -1
- package/dist/tools/update-goal.d.ts +9 -7
- package/dist/tools/update-goal.d.ts.map +1 -1
- package/dist/tools/update-goal.js +20 -21
- package/dist/tools/update-goal.js.map +1 -1
- package/dist/tools/worktree-tools.d.ts +0 -6
- package/dist/tools/worktree-tools.d.ts.map +1 -1
- package/dist/tools/worktree-tools.js +14 -6
- package/dist/tools/worktree-tools.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/compact.d.ts +5 -1
- package/dist/utils/compact.d.ts.map +1 -1
- package/dist/utils/compact.js +11 -1
- package/dist/utils/compact.js.map +1 -1
- package/dist/utils/permissions.d.ts +7 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +39 -0
- package/dist/utils/permissions.js.map +1 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +17 -2
- package/dist/utils/retry.js.map +1 -1
- package/docs/background-tasks.md +62 -0
- package/docs/runtime-progress.md +63 -0
- package/package.json +9 -5
package/dist/engine.d.ts
CHANGED
|
@@ -17,11 +17,14 @@ export declare class QueryEngine {
|
|
|
17
17
|
private config;
|
|
18
18
|
private provider;
|
|
19
19
|
messages: NormalizedMessageParam[];
|
|
20
|
-
private
|
|
21
|
-
private
|
|
20
|
+
private ledger;
|
|
21
|
+
private initialUsage;
|
|
22
|
+
private initialCost;
|
|
23
|
+
private initialModelUsage;
|
|
22
24
|
private turnCount;
|
|
23
25
|
private compactState;
|
|
24
26
|
private sessionId;
|
|
27
|
+
private taskGroup;
|
|
25
28
|
private apiTimeMs;
|
|
26
29
|
private hookRegistry?;
|
|
27
30
|
private spillStore?;
|
|
@@ -36,10 +39,11 @@ export declare class QueryEngine {
|
|
|
36
39
|
* Yields SDKMessage events as the agent works.
|
|
37
40
|
*/
|
|
38
41
|
submitMessage(prompt: string | any[]): AsyncGenerator<SDKMessage>;
|
|
42
|
+
private chargeUsage;
|
|
39
43
|
/**
|
|
40
44
|
* Execute tool calls with concurrency control.
|
|
41
45
|
*
|
|
42
|
-
*
|
|
46
|
+
* Adjacent explicitly concurrency-safe read-only tools run concurrently (up to 10).
|
|
43
47
|
* Mutation tools run sequentially.
|
|
44
48
|
*/
|
|
45
49
|
private executeTools;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAIjB,UAAU,EAEX,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAGV,sBAAsB,EAEvB,MAAM,sBAAsB,CAAA;AAqH7B,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAa;IACtB,QAAQ,EAAE,sBAAsB,EAAE,CAAK;IAC9C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,iBAAiB,CAAiC;IAC1D,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,YAAY,CAAC,CAAc;IACnC,OAAO,CAAC,UAAU,CAAC,CAAY;gBAEnB,MAAM,EAAE,iBAAiB;IAgBrC;;;OAGG;YACW,YAAY;IAiB1B;;;OAGG;IACI,aAAa,CAClB,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE,GACrB,cAAc,CAAC,UAAU,CAAC;IAoJ7B,OAAO,CAAC,WAAW;IAYnB;;;;;OAKG;YACW,YAAY;IAoD1B;;;;;OAKG;YACW,mBAAmB;IAiDjC;;OAEG;YACW,iBAAiB;IA6G/B;;OAEG;IACH,WAAW,IAAI,sBAAsB,EAAE;IAIvC;;OAEG;IACH,QAAQ,IAAI,UAAU;IAQtB;;OAEG;IACH,OAAO,IAAI,MAAM;CAGlB"}
|
package/dist/engine.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 7. Auto-compact when context exceeds threshold
|
|
12
12
|
* 8. Retry with exponential backoff on transient errors
|
|
13
13
|
*/
|
|
14
|
+
import { settleTaskGroup } from './tools/task-tools.js';
|
|
14
15
|
import { estimateCost, } from './utils/tokens.js';
|
|
15
16
|
import { shouldAutoCompact, compactConversation, microCompactMessages, createAutoCompactState, } from './utils/compact.js';
|
|
16
17
|
import { withRetry, isPromptTooLongError, } from './utils/retry.js';
|
|
@@ -88,16 +89,24 @@ export class QueryEngine {
|
|
|
88
89
|
config;
|
|
89
90
|
provider;
|
|
90
91
|
messages = [];
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
ledger;
|
|
93
|
+
initialUsage;
|
|
94
|
+
initialCost;
|
|
95
|
+
initialModelUsage = {};
|
|
93
96
|
turnCount = 0;
|
|
94
97
|
compactState;
|
|
95
98
|
sessionId;
|
|
99
|
+
taskGroup = new Set();
|
|
96
100
|
apiTimeMs = 0;
|
|
97
101
|
hookRegistry;
|
|
98
102
|
spillStore;
|
|
99
103
|
constructor(config) {
|
|
104
|
+
config.tools = config.tools.filter(tool => tool.isEnabled?.() !== false);
|
|
100
105
|
this.config = config;
|
|
106
|
+
this.ledger = config.executionBudget ?? { cost: 0, usage: { input_tokens: 0, output_tokens: 0 } };
|
|
107
|
+
this.initialUsage = { ...this.ledger.usage };
|
|
108
|
+
this.initialCost = this.ledger.cost;
|
|
109
|
+
config.sessionState ??= new Map();
|
|
101
110
|
this.provider = config.provider;
|
|
102
111
|
this.compactState = createAutoCompactState();
|
|
103
112
|
this.sessionId = config.sessionId || crypto.randomUUID();
|
|
@@ -130,258 +139,246 @@ export class QueryEngine {
|
|
|
130
139
|
* Yields SDKMessage events as the agent works.
|
|
131
140
|
*/
|
|
132
141
|
async *submitMessage(prompt) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
this.initialUsage = { ...this.ledger.usage };
|
|
143
|
+
this.initialCost = this.ledger.cost;
|
|
144
|
+
this.initialModelUsage = Object.fromEntries(Object.entries(this.ledger.modelUsage ?? {}).map(([model, usage]) => [model, { ...usage }]));
|
|
145
|
+
this.turnCount = 0;
|
|
146
|
+
this.apiTimeMs = 0;
|
|
147
|
+
this.taskGroup.clear();
|
|
148
|
+
let backgroundTasks = [];
|
|
149
|
+
let status = 'error_max_turns';
|
|
150
|
+
let errors;
|
|
151
|
+
let pendingTools = [];
|
|
152
|
+
const atBudget = () => this.config.maxBudgetUsd !== undefined && this.ledger.cost >= this.config.maxBudgetUsd;
|
|
153
|
+
const compact = async () => {
|
|
154
|
+
this.config.abortSignal?.throwIfAborted();
|
|
155
|
+
await this.executeHooks('PreCompact');
|
|
156
|
+
this.config.abortSignal?.throwIfAborted();
|
|
157
|
+
const result = await compactConversation(this.provider, this.config.model, this.messages, this.compactState, {
|
|
158
|
+
signal: this.config.abortSignal,
|
|
159
|
+
onUsage: usage => this.chargeUsage(usage),
|
|
160
|
+
});
|
|
161
|
+
this.messages = result.compactedMessages;
|
|
162
|
+
this.compactState = result.state;
|
|
163
|
+
if (result.success)
|
|
164
|
+
await this.executeHooks('PostCompact');
|
|
165
|
+
return result.success;
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
await this.executeHooks('SessionStart');
|
|
169
|
+
this.config.abortSignal?.throwIfAborted();
|
|
170
|
+
const userHookResults = await this.executeHooks('UserPromptSubmit', { toolInput: prompt });
|
|
171
|
+
if (userHookResults.some(r => r.block))
|
|
172
|
+
throw new Error('Blocked by UserPromptSubmit hook');
|
|
173
|
+
this.messages.push({ role: 'user', content: prompt });
|
|
174
|
+
const tools = this.config.tools.map(toProviderTool);
|
|
175
|
+
const systemPrompt = await buildSystemPrompt(this.config);
|
|
141
176
|
yield {
|
|
142
|
-
type: '
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
usage: this.totalUsage,
|
|
146
|
-
num_turns: 0,
|
|
147
|
-
cost: 0,
|
|
148
|
-
errors: ['Blocked by UserPromptSubmit hook'],
|
|
177
|
+
type: 'system', subtype: 'init', session_id: this.sessionId,
|
|
178
|
+
tools: this.config.tools.map(t => t.name), model: this.config.model,
|
|
179
|
+
cwd: this.config.cwd, mcp_servers: [], permission_mode: this.config.permissionMode ?? 'bypassPermissions',
|
|
149
180
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const systemPrompt = await buildSystemPrompt(this.config);
|
|
158
|
-
// Emit init system message
|
|
159
|
-
yield {
|
|
160
|
-
type: 'system',
|
|
161
|
-
subtype: 'init',
|
|
162
|
-
session_id: this.sessionId,
|
|
163
|
-
tools: this.config.tools.map(t => t.name),
|
|
164
|
-
model: this.config.model,
|
|
165
|
-
cwd: this.config.cwd,
|
|
166
|
-
mcp_servers: [],
|
|
167
|
-
permission_mode: 'bypassPermissions',
|
|
168
|
-
};
|
|
169
|
-
// Agentic loop
|
|
170
|
-
let turnsRemaining = this.config.maxTurns;
|
|
171
|
-
let budgetExceeded = false;
|
|
172
|
-
let maxOutputRecoveryAttempts = 0;
|
|
173
|
-
const MAX_OUTPUT_RECOVERY = 3;
|
|
174
|
-
while (turnsRemaining > 0) {
|
|
175
|
-
if (this.config.abortSignal?.aborted)
|
|
176
|
-
break;
|
|
177
|
-
// Check budget
|
|
178
|
-
if (this.config.maxBudgetUsd && this.totalCost >= this.config.maxBudgetUsd) {
|
|
179
|
-
budgetExceeded = true;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
// Auto-compact if context is too large
|
|
183
|
-
if (shouldAutoCompact(this.messages, this.config.model, this.compactState, this.config.contextWindowSize)) {
|
|
184
|
-
await this.executeHooks('PreCompact');
|
|
185
|
-
try {
|
|
186
|
-
const result = await compactConversation(this.provider, this.config.model, this.messages, this.compactState);
|
|
187
|
-
this.messages = result.compactedMessages;
|
|
188
|
-
this.compactState = result.state;
|
|
189
|
-
await this.executeHooks('PostCompact');
|
|
181
|
+
let recoveryAttempts = 0;
|
|
182
|
+
let outputRecoveryAttempts = 0;
|
|
183
|
+
while (this.turnCount < this.config.maxTurns) {
|
|
184
|
+
this.config.abortSignal?.throwIfAborted();
|
|
185
|
+
if (atBudget()) {
|
|
186
|
+
status = 'error_max_budget_usd';
|
|
187
|
+
break;
|
|
190
188
|
}
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
if (shouldAutoCompact(this.messages, this.config.model, this.compactState, this.config.contextWindowSize)) {
|
|
190
|
+
await compact();
|
|
193
191
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
messages: apiMessages,
|
|
209
|
-
tools: tools.length
|
|
210
|
-
thinking: this.config.thinking?.type === 'enabled' &&
|
|
211
|
-
this.config.thinking.budgetTokens
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
192
|
+
this.config.abortSignal?.throwIfAborted();
|
|
193
|
+
if (atBudget()) {
|
|
194
|
+
status = 'error_max_budget_usd';
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const apiMessages = microCompactMessages(normalizeMessagesForAPI(this.messages));
|
|
198
|
+
this.turnCount++;
|
|
199
|
+
let response;
|
|
200
|
+
let emittedPartial = false;
|
|
201
|
+
const apiStart = performance.now();
|
|
202
|
+
try {
|
|
203
|
+
const request = {
|
|
204
|
+
signal: this.config.abortSignal,
|
|
205
|
+
model: this.config.model, maxTokens: this.config.maxTokens,
|
|
206
|
+
system: systemPrompt, messages: apiMessages,
|
|
207
|
+
tools: tools.length ? tools : undefined,
|
|
208
|
+
thinking: this.config.thinking?.type === 'enabled' && this.config.thinking.budgetTokens
|
|
209
|
+
? { type: 'enabled', budget_tokens: this.config.thinking.budgetTokens } : undefined,
|
|
210
|
+
};
|
|
211
|
+
if (this.config.includePartialMessages && this.provider.streamMessage) {
|
|
212
|
+
let complete;
|
|
213
|
+
for await (const event of this.provider.streamMessage(request)) {
|
|
214
|
+
this.config.abortSignal?.throwIfAborted();
|
|
215
|
+
if (event.type === 'response')
|
|
216
|
+
complete = event.response;
|
|
217
|
+
else {
|
|
218
|
+
emittedPartial = true;
|
|
219
|
+
yield { type: 'partial_message', partial: event };
|
|
215
220
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
catch (err) {
|
|
221
|
-
// Handle prompt-too-long by compacting
|
|
222
|
-
if (isPromptTooLongError(err) && !this.compactState.compacted) {
|
|
223
|
-
try {
|
|
224
|
-
const result = await compactConversation(this.provider, this.config.model, this.messages, this.compactState);
|
|
225
|
-
this.messages = result.compactedMessages;
|
|
226
|
-
this.compactState = result.state;
|
|
227
|
-
turnsRemaining++; // Retry this turn
|
|
228
|
-
this.turnCount--;
|
|
229
|
-
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!complete)
|
|
223
|
+
throw new Error('Stream ended without a complete response');
|
|
224
|
+
response = complete;
|
|
230
225
|
}
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
else {
|
|
227
|
+
response = await withRetry(() => this.provider.createMessage(request), undefined, this.config.abortSignal);
|
|
233
228
|
}
|
|
234
229
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
// Track API timing
|
|
247
|
-
this.apiTimeMs += performance.now() - apiStart;
|
|
248
|
-
// Track usage (normalized by provider)
|
|
249
|
-
if (response.usage) {
|
|
250
|
-
this.totalUsage.input_tokens += response.usage.input_tokens;
|
|
251
|
-
this.totalUsage.output_tokens += response.usage.output_tokens;
|
|
252
|
-
if (response.usage.cache_creation_input_tokens) {
|
|
253
|
-
this.totalUsage.cache_creation_input_tokens =
|
|
254
|
-
(this.totalUsage.cache_creation_input_tokens || 0) +
|
|
255
|
-
response.usage.cache_creation_input_tokens;
|
|
230
|
+
catch (err) {
|
|
231
|
+
this.config.abortSignal?.throwIfAborted();
|
|
232
|
+
if (!emittedPartial && isPromptTooLongError(err) && recoveryAttempts < 1 && !atBudget()) {
|
|
233
|
+
recoveryAttempts++;
|
|
234
|
+
if (await compact()) {
|
|
235
|
+
this.turnCount--;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw err;
|
|
256
240
|
}
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
|
|
241
|
+
finally {
|
|
242
|
+
this.apiTimeMs += performance.now() - apiStart;
|
|
243
|
+
}
|
|
244
|
+
if (response.usage)
|
|
245
|
+
this.chargeUsage(response.usage);
|
|
246
|
+
this.config.abortSignal?.throwIfAborted();
|
|
247
|
+
this.messages.push({ role: 'assistant', content: response.content });
|
|
248
|
+
pendingTools = response.content.filter((block) => block.type === 'tool_use');
|
|
249
|
+
yield { type: 'assistant', message: { role: 'assistant', content: response.content } };
|
|
250
|
+
this.config.abortSignal?.throwIfAborted();
|
|
251
|
+
if (pendingTools.length) {
|
|
252
|
+
if (response.stopReason === 'max_tokens')
|
|
253
|
+
throw new Error('Tool response was truncated before completion');
|
|
254
|
+
outputRecoveryAttempts = 0;
|
|
255
|
+
const results = await this.executeTools(pendingTools);
|
|
256
|
+
const spilled = this.spillStore ? await this.applySpillToResults(results) : results;
|
|
257
|
+
// Persist all results before yielding so an early consumer exit preserves valid history.
|
|
258
|
+
this.messages.push({ role: 'user', content: spilled.map(r => ({
|
|
259
|
+
type: 'tool_result', tool_use_id: r.tool_use_id,
|
|
260
|
+
content: typeof r.content === 'string' ? r.content : JSON.stringify(r.content), is_error: r.is_error,
|
|
261
|
+
})) });
|
|
262
|
+
pendingTools = [];
|
|
263
|
+
for (const r of spilled)
|
|
264
|
+
yield { type: 'tool_result', result: {
|
|
265
|
+
tool_use_id: r.tool_use_id, tool_name: r.tool_name ?? '',
|
|
266
|
+
output: typeof r.content === 'string' ? r.content : JSON.stringify(r.content),
|
|
267
|
+
} };
|
|
268
|
+
this.config.abortSignal?.throwIfAborted();
|
|
269
|
+
}
|
|
270
|
+
else if (response.stopReason === 'max_tokens') {
|
|
271
|
+
if (outputRecoveryAttempts++ >= 3) {
|
|
272
|
+
status = 'error_during_execution';
|
|
273
|
+
errors = ['Maximum output recovery attempts exceeded'];
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
this.messages.push({ role: 'user', content: 'Please continue from where you left off.' });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
status = 'success';
|
|
280
|
+
break;
|
|
261
281
|
}
|
|
262
|
-
this.totalCost += estimateCost(this.config.model, response.usage, this.config.pricingPerMillion);
|
|
263
|
-
}
|
|
264
|
-
// Add assistant message to conversation
|
|
265
|
-
this.messages.push({ role: 'assistant', content: response.content });
|
|
266
|
-
// Yield assistant message
|
|
267
|
-
yield {
|
|
268
|
-
type: 'assistant',
|
|
269
|
-
message: {
|
|
270
|
-
role: 'assistant',
|
|
271
|
-
content: response.content,
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
// Handle max_output_tokens recovery
|
|
275
|
-
if (response.stopReason === 'max_tokens' &&
|
|
276
|
-
maxOutputRecoveryAttempts < MAX_OUTPUT_RECOVERY) {
|
|
277
|
-
maxOutputRecoveryAttempts++;
|
|
278
|
-
// Add continuation prompt
|
|
279
|
-
this.messages.push({
|
|
280
|
-
role: 'user',
|
|
281
|
-
content: 'Please continue from where you left off.',
|
|
282
|
-
});
|
|
283
|
-
continue;
|
|
284
282
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
this.config.abortSignal?.throwIfAborted();
|
|
284
|
+
if (status === 'error_max_turns' && atBudget())
|
|
285
|
+
status = 'error_max_budget_usd';
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
status = this.config.abortSignal?.aborted ? 'cancelled' : 'error_during_execution';
|
|
289
|
+
errors = [err?.message ? String(err.message) : String(err)];
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
if (pendingTools.length) {
|
|
293
|
+
this.messages.push({ role: 'user', content: pendingTools.map(block => ({
|
|
294
|
+
type: 'tool_result', tool_use_id: block.id,
|
|
295
|
+
content: 'Tool execution interrupted before a result was recorded.', is_error: true,
|
|
296
|
+
})) });
|
|
289
297
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// Apply spill policy to large tool results before they enter context.
|
|
295
|
-
const spilled = this.spillStore ? await this.applySpillToResults(toolResults) : toolResults;
|
|
296
|
-
// Yield tool results
|
|
297
|
-
for (const result of spilled) {
|
|
298
|
-
yield {
|
|
299
|
-
type: 'tool_result',
|
|
300
|
-
result: {
|
|
301
|
-
tool_use_id: result.tool_use_id,
|
|
302
|
-
tool_name: result.tool_name || '',
|
|
303
|
-
output: typeof result.content === 'string'
|
|
304
|
-
? result.content
|
|
305
|
-
: JSON.stringify(result.content),
|
|
306
|
-
},
|
|
307
|
-
};
|
|
298
|
+
backgroundTasks = await settleTaskGroup(this.taskGroup, this.config.sessionState, status !== 'success');
|
|
299
|
+
if (this.config.abortSignal?.aborted) {
|
|
300
|
+
status = 'cancelled';
|
|
301
|
+
errors = ['Run cancelled'];
|
|
308
302
|
}
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
role: 'user',
|
|
312
|
-
content: spilled.map((r) => ({
|
|
313
|
-
type: 'tool_result',
|
|
314
|
-
tool_use_id: r.tool_use_id,
|
|
315
|
-
content: typeof r.content === 'string'
|
|
316
|
-
? r.content
|
|
317
|
-
: JSON.stringify(r.content),
|
|
318
|
-
is_error: r.is_error,
|
|
319
|
-
})),
|
|
320
|
-
});
|
|
321
|
-
if (response.stopReason === 'end_turn')
|
|
322
|
-
break;
|
|
303
|
+
await this.executeHooks('Stop');
|
|
304
|
+
await this.executeHooks('SessionEnd');
|
|
323
305
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
? 'error_max_turns'
|
|
333
|
-
: 'success';
|
|
306
|
+
for (const task of backgroundTasks)
|
|
307
|
+
yield { type: 'system', subtype: 'task_notification', task_id: task.id, status: task.status, message: task.output };
|
|
308
|
+
const usage = this.getUsage();
|
|
309
|
+
const cost = this.getCost();
|
|
310
|
+
const modelUsage = Object.fromEntries(Object.entries(this.ledger.modelUsage ?? {}).map(([model, value]) => [model, {
|
|
311
|
+
input_tokens: value.input_tokens - (this.initialModelUsage[model]?.input_tokens ?? 0),
|
|
312
|
+
output_tokens: value.output_tokens - (this.initialModelUsage[model]?.output_tokens ?? 0),
|
|
313
|
+
}]));
|
|
334
314
|
yield {
|
|
335
|
-
type: 'result',
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
num_turns: this.turnCount,
|
|
340
|
-
total_cost_usd: this.totalCost,
|
|
341
|
-
duration_api_ms: Math.round(this.apiTimeMs),
|
|
342
|
-
usage: this.totalUsage,
|
|
343
|
-
model_usage: { [this.config.model]: { input_tokens: this.totalUsage.input_tokens, output_tokens: this.totalUsage.output_tokens } },
|
|
344
|
-
cost: this.totalCost,
|
|
315
|
+
type: 'result', subtype: status, session_id: this.sessionId,
|
|
316
|
+
is_error: status !== 'success', errors, num_turns: this.turnCount,
|
|
317
|
+
total_cost_usd: cost, cost, usage, duration_api_ms: Math.round(this.apiTimeMs),
|
|
318
|
+
model_usage: modelUsage,
|
|
345
319
|
};
|
|
346
320
|
}
|
|
321
|
+
chargeUsage(usage) {
|
|
322
|
+
this.ledger.modelUsage ??= {};
|
|
323
|
+
const modelUsage = this.ledger.modelUsage[this.config.model] ??= { input_tokens: 0, output_tokens: 0 };
|
|
324
|
+
for (const key of ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
|
|
325
|
+
if (usage[key] !== undefined) {
|
|
326
|
+
this.ledger.usage[key] = (this.ledger.usage[key] ?? 0) + usage[key];
|
|
327
|
+
modelUsage[key] = (modelUsage[key] ?? 0) + usage[key];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
this.ledger.cost += estimateCost(this.config.model, usage, this.config.pricingPerMillion);
|
|
331
|
+
}
|
|
347
332
|
/**
|
|
348
333
|
* Execute tool calls with concurrency control.
|
|
349
334
|
*
|
|
350
|
-
*
|
|
335
|
+
* Adjacent explicitly concurrency-safe read-only tools run concurrently (up to 10).
|
|
351
336
|
* Mutation tools run sequentially.
|
|
352
337
|
*/
|
|
353
338
|
async executeTools(toolUseBlocks) {
|
|
354
339
|
const context = {
|
|
340
|
+
taskGroup: this.taskGroup,
|
|
355
341
|
cwd: this.config.cwd,
|
|
356
342
|
abortSignal: this.config.abortSignal,
|
|
357
343
|
provider: this.provider,
|
|
358
344
|
model: this.config.model,
|
|
359
345
|
apiType: this.provider.apiType,
|
|
346
|
+
sessionState: this.config.sessionState,
|
|
347
|
+
tools: this.config.tools,
|
|
348
|
+
agents: this.config.agents,
|
|
349
|
+
canUseTool: this.config.canUseTool,
|
|
350
|
+
executionBudget: this.ledger,
|
|
351
|
+
maxBudgetUsd: this.config.maxBudgetUsd,
|
|
352
|
+
pricingPerMillion: this.config.pricingPerMillion,
|
|
353
|
+
hookRegistry: this.hookRegistry,
|
|
354
|
+
sessionId: this.sessionId,
|
|
360
355
|
};
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
356
|
+
const configuredConcurrency = Number(process.env.AGENT_SDK_MAX_TOOL_CONCURRENCY ?? 10);
|
|
357
|
+
const MAX_CONCURRENCY = Number.isFinite(configuredConcurrency) && configuredConcurrency >= 1
|
|
358
|
+
? Math.floor(configuredConcurrency) : 10;
|
|
359
|
+
const results = [];
|
|
360
|
+
// Mutations and tools without an explicit concurrency guarantee are barriers.
|
|
361
|
+
// Parallelize only adjacent safe reads; never move a read before an earlier write.
|
|
362
|
+
for (let i = 0; i < toolUseBlocks.length;) {
|
|
363
|
+
this.config.abortSignal?.throwIfAborted();
|
|
364
|
+
const block = toolUseBlocks[i];
|
|
365
|
+
const tool = this.config.tools.find(t => t.name === block.name);
|
|
366
|
+
const safe = (t) => t?.isReadOnly?.() === true && t.isConcurrencySafe?.() === true;
|
|
367
|
+
if (!safe(tool)) {
|
|
368
|
+
results.push(await this.executeSingleTool(block, tool, context));
|
|
369
|
+
i++;
|
|
370
|
+
continue;
|
|
369
371
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
+
const batch = [];
|
|
373
|
+
while (i < toolUseBlocks.length && batch.length < MAX_CONCURRENCY) {
|
|
374
|
+
const next = toolUseBlocks[i];
|
|
375
|
+
const nextTool = this.config.tools.find(t => t.name === next.name);
|
|
376
|
+
if (!safe(nextTool))
|
|
377
|
+
break;
|
|
378
|
+
batch.push({ block: next, tool: nextTool });
|
|
379
|
+
i++;
|
|
372
380
|
}
|
|
373
|
-
|
|
374
|
-
const results = [];
|
|
375
|
-
// Execute read-only tools concurrently (batched by MAX_CONCURRENCY)
|
|
376
|
-
for (let i = 0; i < readOnly.length; i += MAX_CONCURRENCY) {
|
|
377
|
-
const batch = readOnly.slice(i, i + MAX_CONCURRENCY);
|
|
378
|
-
const batchResults = await Promise.all(batch.map((item) => this.executeSingleTool(item.block, item.tool, context)));
|
|
379
|
-
results.push(...batchResults);
|
|
380
|
-
}
|
|
381
|
-
// Execute mutation tools sequentially
|
|
382
|
-
for (const item of mutations) {
|
|
383
|
-
const result = await this.executeSingleTool(item.block, item.tool, context);
|
|
384
|
-
results.push(result);
|
|
381
|
+
results.push(...await Promise.all(batch.map(item => this.executeSingleTool(item.block, item.tool, context))));
|
|
385
382
|
}
|
|
386
383
|
return results;
|
|
387
384
|
}
|
|
@@ -438,6 +435,10 @@ export class QueryEngine {
|
|
|
438
435
|
* Execute a single tool with permission checking.
|
|
439
436
|
*/
|
|
440
437
|
async executeSingleTool(block, tool, context) {
|
|
438
|
+
const cancelled = () => ({ type: 'tool_result', tool_use_id: block.id,
|
|
439
|
+
content: 'Tool execution cancelled', is_error: true, tool_name: block.name });
|
|
440
|
+
if (context.abortSignal?.aborted)
|
|
441
|
+
return cancelled();
|
|
441
442
|
if (!tool) {
|
|
442
443
|
return {
|
|
443
444
|
type: 'tool_result',
|
|
@@ -484,6 +485,8 @@ export class QueryEngine {
|
|
|
484
485
|
};
|
|
485
486
|
}
|
|
486
487
|
}
|
|
488
|
+
if (context.abortSignal?.aborted)
|
|
489
|
+
return cancelled();
|
|
487
490
|
// Hook: PreToolUse
|
|
488
491
|
const preHookResults = await this.executeHooks('PreToolUse', {
|
|
489
492
|
toolName: block.name,
|
|
@@ -503,6 +506,8 @@ export class QueryEngine {
|
|
|
503
506
|
}
|
|
504
507
|
// Execute the tool
|
|
505
508
|
try {
|
|
509
|
+
if (context.abortSignal?.aborted)
|
|
510
|
+
return cancelled();
|
|
506
511
|
const result = await tool.call(block.input, context);
|
|
507
512
|
// Hook: PostToolUse
|
|
508
513
|
await this.executeHooks('PostToolUse', {
|
|
@@ -540,13 +545,18 @@ export class QueryEngine {
|
|
|
540
545
|
* Get total usage across all turns.
|
|
541
546
|
*/
|
|
542
547
|
getUsage() {
|
|
543
|
-
|
|
548
|
+
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
549
|
+
for (const key of ['input_tokens', 'output_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
|
|
550
|
+
if (this.ledger.usage[key] !== undefined)
|
|
551
|
+
usage[key] = (this.ledger.usage[key] ?? 0) - (this.initialUsage[key] ?? 0);
|
|
552
|
+
}
|
|
553
|
+
return usage;
|
|
544
554
|
}
|
|
545
555
|
/**
|
|
546
556
|
* Get total cost.
|
|
547
557
|
*/
|
|
548
558
|
getCost() {
|
|
549
|
-
return this.
|
|
559
|
+
return this.ledger.cost - this.initialCost;
|
|
550
560
|
}
|
|
551
561
|
}
|
|
552
562
|
//# sourceMappingURL=engine.js.map
|