@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +75 -3
  3. package/dist/agent.d.ts +12 -22
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +209 -224
  6. package/dist/agent.js.map +1 -1
  7. package/dist/engine.d.ts +7 -3
  8. package/dist/engine.d.ts.map +1 -1
  9. package/dist/engine.js +234 -224
  10. package/dist/engine.js.map +1 -1
  11. package/dist/index.d.ts +4 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp/client.d.ts +3 -0
  16. package/dist/mcp/client.d.ts.map +1 -1
  17. package/dist/mcp/client.js +13 -4
  18. package/dist/mcp/client.js.map +1 -1
  19. package/dist/providers/anthropic.d.ts +2 -1
  20. package/dist/providers/anthropic.d.ts.map +1 -1
  21. package/dist/providers/anthropic.js +31 -1
  22. package/dist/providers/anthropic.js.map +1 -1
  23. package/dist/providers/openai.d.ts +3 -1
  24. package/dist/providers/openai.d.ts.map +1 -1
  25. package/dist/providers/openai.js +54 -3
  26. package/dist/providers/openai.js.map +1 -1
  27. package/dist/providers/sse.d.ts +3 -0
  28. package/dist/providers/sse.d.ts.map +1 -0
  29. package/dist/providers/sse.js +36 -0
  30. package/dist/providers/sse.js.map +1 -0
  31. package/dist/providers/types.d.ts +16 -0
  32. package/dist/providers/types.d.ts.map +1 -1
  33. package/dist/session.d.ts +10 -0
  34. package/dist/session.d.ts.map +1 -1
  35. package/dist/session.js +98 -4
  36. package/dist/session.js.map +1 -1
  37. package/dist/tools/agent-tool.d.ts.map +1 -1
  38. package/dist/tools/agent-tool.js +32 -9
  39. package/dist/tools/agent-tool.js.map +1 -1
  40. package/dist/tools/ask-user.d.ts +3 -2
  41. package/dist/tools/ask-user.d.ts.map +1 -1
  42. package/dist/tools/ask-user.js +16 -15
  43. package/dist/tools/ask-user.js.map +1 -1
  44. package/dist/tools/bash.d.ts +0 -3
  45. package/dist/tools/bash.d.ts.map +1 -1
  46. package/dist/tools/bash.js +79 -59
  47. package/dist/tools/bash.js.map +1 -1
  48. package/dist/tools/config-tool.d.ts +4 -3
  49. package/dist/tools/config-tool.d.ts.map +1 -1
  50. package/dist/tools/config-tool.js +20 -16
  51. package/dist/tools/config-tool.js.map +1 -1
  52. package/dist/tools/cron-tools.d.ts +3 -2
  53. package/dist/tools/cron-tools.d.ts.map +1 -1
  54. package/dist/tools/cron-tools.js +33 -29
  55. package/dist/tools/cron-tools.js.map +1 -1
  56. package/dist/tools/index.js +55 -53
  57. package/dist/tools/index.js.map +1 -1
  58. package/dist/tools/lsp-tool.js +2 -2
  59. package/dist/tools/lsp-tool.js.map +1 -1
  60. package/dist/tools/mcp-resource-tools.d.ts +2 -1
  61. package/dist/tools/mcp-resource-tools.d.ts.map +1 -1
  62. package/dist/tools/mcp-resource-tools.js +44 -27
  63. package/dist/tools/mcp-resource-tools.js.map +1 -1
  64. package/dist/tools/plan-tools.d.ts +3 -2
  65. package/dist/tools/plan-tools.d.ts.map +1 -1
  66. package/dist/tools/plan-tools.js +24 -21
  67. package/dist/tools/plan-tools.js.map +1 -1
  68. package/dist/tools/send-message.d.ts +5 -4
  69. package/dist/tools/send-message.d.ts.map +1 -1
  70. package/dist/tools/send-message.js +24 -21
  71. package/dist/tools/send-message.js.map +1 -1
  72. package/dist/tools/session-state.d.ts +4 -0
  73. package/dist/tools/session-state.d.ts.map +1 -0
  74. package/dist/tools/session-state.js +9 -0
  75. package/dist/tools/session-state.js.map +1 -0
  76. package/dist/tools/task-tools.d.ts +19 -4
  77. package/dist/tools/task-tools.d.ts.map +1 -1
  78. package/dist/tools/task-tools.js +119 -31
  79. package/dist/tools/task-tools.js.map +1 -1
  80. package/dist/tools/team-tools.d.ts +7 -3
  81. package/dist/tools/team-tools.d.ts.map +1 -1
  82. package/dist/tools/team-tools.js +26 -22
  83. package/dist/tools/team-tools.js.map +1 -1
  84. package/dist/tools/todo-tool.d.ts +3 -2
  85. package/dist/tools/todo-tool.d.ts.map +1 -1
  86. package/dist/tools/todo-tool.js +24 -21
  87. package/dist/tools/todo-tool.js.map +1 -1
  88. package/dist/tools/tool-search.d.ts +2 -1
  89. package/dist/tools/tool-search.d.ts.map +1 -1
  90. package/dist/tools/tool-search.js +14 -13
  91. package/dist/tools/tool-search.js.map +1 -1
  92. package/dist/tools/update-goal.d.ts +9 -7
  93. package/dist/tools/update-goal.d.ts.map +1 -1
  94. package/dist/tools/update-goal.js +20 -21
  95. package/dist/tools/update-goal.js.map +1 -1
  96. package/dist/tools/worktree-tools.d.ts +0 -6
  97. package/dist/tools/worktree-tools.d.ts.map +1 -1
  98. package/dist/tools/worktree-tools.js +14 -6
  99. package/dist/tools/worktree-tools.js.map +1 -1
  100. package/dist/types.d.ts +31 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/utils/compact.d.ts +5 -1
  103. package/dist/utils/compact.d.ts.map +1 -1
  104. package/dist/utils/compact.js +11 -1
  105. package/dist/utils/compact.js.map +1 -1
  106. package/dist/utils/permissions.d.ts +7 -0
  107. package/dist/utils/permissions.d.ts.map +1 -0
  108. package/dist/utils/permissions.js +39 -0
  109. package/dist/utils/permissions.js.map +1 -0
  110. package/dist/utils/retry.d.ts.map +1 -1
  111. package/dist/utils/retry.js +17 -2
  112. package/dist/utils/retry.js.map +1 -1
  113. package/docs/background-tasks.md +62 -0
  114. package/docs/runtime-progress.md +63 -0
  115. 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 totalUsage;
21
- private totalCost;
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
- * Read-only tools run concurrently (up to 10 at a time).
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;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAIjB,UAAU,EACX,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAGV,sBAAsB,EAEvB,MAAM,sBAAsB,CAAA;AAwH7B,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAa;IACtB,QAAQ,EAAE,sBAAsB,EAAE,CAAK;IAC9C,OAAO,CAAC,UAAU,CAAoD;IACtE,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,YAAY,CAAC,CAAc;IACnC,OAAO,CAAC,UAAU,CAAC,CAAY;gBAEnB,MAAM,EAAE,iBAAiB;IAWrC;;;OAGG;YACW,YAAY;IAiB1B;;;OAGG;IACI,aAAa,CAClB,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE,GACrB,cAAc,CAAC,UAAU,CAAC;IAkR7B;;;;;OAKG;YACW,YAAY;IAkD1B;;;;;OAKG;YACW,mBAAmB;IAiDjC;;OAEG;YACW,iBAAiB;IAuG/B;;OAEG;IACH,WAAW,IAAI,sBAAsB,EAAE;IAIvC;;OAEG;IACH,QAAQ,IAAI,UAAU;IAItB;;OAEG;IACH,OAAO,IAAI,MAAM;CAGlB"}
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
- totalUsage = { input_tokens: 0, output_tokens: 0 };
92
- totalCost = 0;
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
- // Hook: SessionStart
134
- await this.executeHooks('SessionStart');
135
- // Hook: UserPromptSubmit
136
- const userHookResults = await this.executeHooks('UserPromptSubmit', {
137
- toolInput: prompt,
138
- });
139
- // Check if any hook blocks the submission
140
- if (userHookResults.some((r) => r.block)) {
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: 'result',
143
- subtype: 'error_during_execution',
144
- is_error: true,
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
- return;
151
- }
152
- // Add user message
153
- this.messages.push({ role: 'user', content: prompt });
154
- // Build tool definitions for provider
155
- const tools = this.config.tools.map(toProviderTool);
156
- // Build system prompt
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
- catch {
192
- // Continue with uncompacted messages
189
+ if (shouldAutoCompact(this.messages, this.config.model, this.compactState, this.config.contextWindowSize)) {
190
+ await compact();
193
191
  }
194
- }
195
- // Micro-compact: truncate large tool results
196
- const apiMessages = microCompactMessages(normalizeMessagesForAPI(this.messages));
197
- this.turnCount++;
198
- turnsRemaining--;
199
- // Make API call with retry via provider
200
- let response;
201
- const apiStart = performance.now();
202
- try {
203
- response = await withRetry(async () => {
204
- return this.provider.createMessage({
205
- model: this.config.model,
206
- maxTokens: this.config.maxTokens,
207
- system: systemPrompt,
208
- messages: apiMessages,
209
- tools: tools.length > 0 ? tools : undefined,
210
- thinking: this.config.thinking?.type === 'enabled' &&
211
- this.config.thinking.budgetTokens
212
- ? {
213
- type: 'enabled',
214
- budget_tokens: this.config.thinking.budgetTokens,
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
- : undefined,
217
- });
218
- }, undefined, this.config.abortSignal);
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
- catch {
232
- // Can't compact, give up
226
+ else {
227
+ response = await withRetry(() => this.provider.createMessage(request), undefined, this.config.abortSignal);
233
228
  }
234
229
  }
235
- const message = err?.message ? String(err.message) : String(err);
236
- yield {
237
- type: 'result',
238
- subtype: 'error',
239
- usage: this.totalUsage,
240
- num_turns: this.turnCount,
241
- cost: this.totalCost,
242
- errors: [message],
243
- };
244
- return;
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
- if (response.usage.cache_read_input_tokens) {
258
- this.totalUsage.cache_read_input_tokens =
259
- (this.totalUsage.cache_read_input_tokens || 0) +
260
- response.usage.cache_read_input_tokens;
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
- // Check for tool use
286
- const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
287
- if (toolUseBlocks.length === 0) {
288
- break; // No tool calls - agent is done
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
- // Reset max_output recovery counter on successful tool use
291
- maxOutputRecoveryAttempts = 0;
292
- // Execute tools (concurrent read-only, serial mutations)
293
- const toolResults = await this.executeTools(toolUseBlocks);
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
- // Add tool results to conversation
310
- this.messages.push({
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
- // Hook: Stop (end of agentic loop)
325
- await this.executeHooks('Stop');
326
- // Hook: SessionEnd
327
- await this.executeHooks('SessionEnd');
328
- // Yield enriched final result
329
- const endSubtype = budgetExceeded
330
- ? 'error_max_budget_usd'
331
- : turnsRemaining <= 0
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
- subtype: endSubtype,
337
- session_id: this.sessionId,
338
- is_error: endSubtype !== 'success',
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
- * Read-only tools run concurrently (up to 10 at a time).
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 MAX_CONCURRENCY = parseInt(process.env.AGENT_SDK_MAX_TOOL_CONCURRENCY || '10');
362
- // Partition into read-only (concurrent) and mutation (serial)
363
- const readOnly = [];
364
- const mutations = [];
365
- for (const block of toolUseBlocks) {
366
- const tool = this.config.tools.find((t) => t.name === block.name);
367
- if (tool?.isReadOnly?.()) {
368
- readOnly.push({ block, tool });
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
- else {
371
- mutations.push({ block, tool });
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
- return { ...this.totalUsage };
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.totalCost;
559
+ return this.ledger.cost - this.initialCost;
550
560
  }
551
561
  }
552
562
  //# sourceMappingURL=engine.js.map