@stackbilt/aegis-core 0.6.5 → 0.7.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 (112) hide show
  1. package/cli/aegis.mjs +356 -0
  2. package/package.json +9 -1
  3. package/schema.sql +4 -0
  4. package/src/adapters/voice/cloudflare-agent.ts +0 -0
  5. package/src/auth.ts +13 -5
  6. package/src/bluesky.ts +0 -0
  7. package/src/claude-tools/content.ts +0 -0
  8. package/src/claude-tools/email.ts +0 -0
  9. package/src/claude.ts +133 -268
  10. package/src/codebeast.ts +0 -0
  11. package/src/composite.ts +49 -79
  12. package/src/content/column.ts +0 -0
  13. package/src/content/hero-image.ts +0 -0
  14. package/src/content/index.ts +0 -0
  15. package/src/content/journal.ts +0 -0
  16. package/src/content/roundtable.ts +0 -0
  17. package/src/contracts/agenda-item.contract.ts +0 -0
  18. package/src/contracts/cc-task.contract.ts +0 -0
  19. package/src/contracts/goal.contract.ts +0 -0
  20. package/src/contracts/memory-entry.contract.ts +0 -0
  21. package/src/core.ts +5 -0
  22. package/src/dashboard.ts +0 -0
  23. package/src/decision-docs.ts +0 -0
  24. package/src/dispatch.ts +0 -0
  25. package/src/durable-objects/chat-session-auth.ts +20 -0
  26. package/src/durable-objects/chat-session.ts +251 -0
  27. package/src/edge-env.ts +0 -0
  28. package/src/exports.ts +0 -0
  29. package/src/github-projects.ts +0 -0
  30. package/src/groq.ts +61 -113
  31. package/src/index.ts +4 -0
  32. package/src/kernel/argus-actions.ts +0 -0
  33. package/src/kernel/argus-correlation.ts +0 -0
  34. package/src/kernel/board.ts +0 -0
  35. package/src/kernel/classify-memory-topic.ts +0 -0
  36. package/src/kernel/disambiguation.ts +55 -0
  37. package/src/kernel/dispatch.ts +59 -44
  38. package/src/kernel/dynamic-tools.ts +30 -52
  39. package/src/kernel/executor-port.ts +0 -0
  40. package/src/kernel/executor-router.ts +0 -0
  41. package/src/kernel/executors/claude.ts +1 -0
  42. package/src/kernel/executors/direct.ts +14 -0
  43. package/src/kernel/executors/workers-ai.ts +5 -0
  44. package/src/kernel/grounding/fabrication-detector.ts +0 -0
  45. package/src/kernel/grounding/fanout.ts +0 -0
  46. package/src/kernel/grounding/semantic-sanhedrin.ts +0 -0
  47. package/src/kernel/grounding/verify.ts +0 -0
  48. package/src/kernel/grounding-layer.ts +0 -0
  49. package/src/kernel/insight-cache.ts +0 -0
  50. package/src/kernel/memory/episodic.ts +3 -1
  51. package/src/kernel/memory/insights.ts +0 -0
  52. package/src/kernel/memory-guardrails.ts +0 -0
  53. package/src/kernel/memory-service.ts +0 -0
  54. package/src/kernel/patterns.ts +0 -0
  55. package/src/kernel/port.ts +0 -0
  56. package/src/kernel/provider-factory.ts +0 -0
  57. package/src/kernel/resilience.ts +0 -0
  58. package/src/kernel/router.ts +33 -11
  59. package/src/kernel/scheduled/agent-dispatch.ts +0 -0
  60. package/src/kernel/scheduled/argus-analytics.ts +0 -0
  61. package/src/kernel/scheduled/argus-heartbeat.ts +0 -0
  62. package/src/kernel/scheduled/argus-notify.ts +0 -0
  63. package/src/kernel/scheduled/board-sync.ts +0 -0
  64. package/src/kernel/scheduled/ci-watcher.ts +0 -0
  65. package/src/kernel/scheduled/content-drip.ts +0 -0
  66. package/src/kernel/scheduled/content.ts +0 -0
  67. package/src/kernel/scheduled/conversation-facts.ts +9 -7
  68. package/src/kernel/scheduled/cost-report.ts +0 -0
  69. package/src/kernel/scheduled/dev-activity.ts +0 -0
  70. package/src/kernel/scheduled/digest.ts +30 -3
  71. package/src/kernel/scheduled/dreaming/agenda-triage.ts +0 -0
  72. package/src/kernel/scheduled/dreaming/facts.ts +0 -0
  73. package/src/kernel/scheduled/dreaming/index.ts +0 -0
  74. package/src/kernel/scheduled/dreaming/llm.ts +9 -5
  75. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +0 -0
  76. package/src/kernel/scheduled/dreaming/persona.ts +0 -0
  77. package/src/kernel/scheduled/dreaming/symbolic.ts +0 -0
  78. package/src/kernel/scheduled/dreaming/task-proposals.ts +0 -0
  79. package/src/kernel/scheduled/entropy.ts +0 -0
  80. package/src/kernel/scheduled/feed-watcher.ts +0 -0
  81. package/src/kernel/scheduled/inbox-processor.ts +0 -0
  82. package/src/kernel/scheduled/issue-proposer.ts +0 -0
  83. package/src/kernel/scheduled/issue-watcher.ts +0 -0
  84. package/src/kernel/scheduled/pr-automerge.ts +0 -0
  85. package/src/kernel/scheduled/product-health.ts +0 -0
  86. package/src/kernel/scheduled/self-improvement.ts +0 -0
  87. package/src/kernel/scheduled/social-engage.ts +12 -8
  88. package/src/kernel/scheduled/task-audit.ts +0 -0
  89. package/src/kernel/types.ts +6 -0
  90. package/src/landing.ts +0 -0
  91. package/src/lib/audit-chain/chain.ts +0 -0
  92. package/src/lib/audit-chain/types.ts +0 -0
  93. package/src/lib/observability/errors.ts +0 -0
  94. package/src/operator/config.ts +0 -0
  95. package/src/operator/persona.ts +0 -0
  96. package/src/operator/prompt-builder.ts +3 -0
  97. package/src/pulse.ts +0 -0
  98. package/src/routes/bluesky.ts +0 -0
  99. package/src/routes/chat-ws.ts +17 -0
  100. package/src/routes/codebeast.ts +0 -0
  101. package/src/routes/content.ts +0 -0
  102. package/src/routes/dynamic-tools.ts +0 -0
  103. package/src/routes/observability.ts +0 -0
  104. package/src/routes/operator-logs.ts +0 -0
  105. package/src/routes/pages.ts +5 -1
  106. package/src/schema-enums.ts +0 -0
  107. package/src/task-intelligence.ts +0 -0
  108. package/src/types.ts +6 -0
  109. package/src/ui.ts +594 -2
  110. package/src/version.ts +3 -3
  111. package/src/wiki/client.ts +0 -0
  112. package/src/wiki/types.ts +0 -0
package/src/claude.ts CHANGED
@@ -1,160 +1,20 @@
1
- // Edge-native Claude executor — Anthropic Messages API with tool loop
2
- // Replaces the Agent SDK query() for edge deployment
1
+ // Edge-native Claude executor — provider-backed tool loop
2
+ // Replaces direct Anthropic transport with @stackbilt/llm-providers.
3
3
 
4
+ import { createLLMProviderFactory, type LLMMessage, type Tool, type ToolCall, type ToolResult as LLMToolResult } from '@stackbilt/llm-providers';
4
5
  import { McpClient } from './mcp-client.js';
5
6
  import { budgetConversationHistory } from './kernel/memory/index.js';
7
+ import { buildLLMProviderFactory } from './kernel/provider-factory.js';
6
8
  import {
7
9
  buildContext,
8
10
  handleInProcessTool,
9
11
  resolveMcpTool,
10
- getModelCostRates,
11
12
  type ClaudeConfig,
12
- type ContentBlock,
13
- type Message,
14
- type ApiResponse,
15
13
  } from './claude-tools/index.js';
16
14
 
17
15
  // Re-export for external consumers
18
16
  export { buildContext, handleInProcessTool, resolveMcpTool, type ClaudeConfig } from './claude-tools/index.js';
19
17
 
20
- // ─── Anthropic SSE streaming ─────────────────────────────────
21
-
22
- type StreamBlockState = {
23
- type: 'text' | 'tool_use';
24
- text: string;
25
- id: string;
26
- name: string;
27
- inputJson: string;
28
- };
29
-
30
- async function* parseAnthropicSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<Record<string, unknown>> {
31
- const reader = body.getReader();
32
- const decoder = new TextDecoder();
33
- let buffer = '';
34
-
35
- try {
36
- while (true) {
37
- const { done, value } = await reader.read();
38
- if (done) break;
39
- buffer += decoder.decode(value, { stream: true });
40
-
41
- let newlineIdx: number;
42
- while ((newlineIdx = buffer.indexOf('\n')) >= 0) {
43
- const line = buffer.slice(0, newlineIdx).trim();
44
- buffer = buffer.slice(newlineIdx + 1);
45
-
46
- if (!line.startsWith('data: ')) continue;
47
- const payload = line.slice(6).trim();
48
- if (payload === '[DONE]') return;
49
-
50
- try {
51
- yield JSON.parse(payload);
52
- } catch {
53
- // Malformed chunk — skip
54
- }
55
- }
56
- }
57
- } finally {
58
- reader.releaseLock();
59
- }
60
- }
61
-
62
- async function callAnthropicStream(
63
- apiKey: string,
64
- model: string,
65
- system: string,
66
- messages: Message[],
67
- tools: unknown[],
68
- onDelta: (text: string) => void,
69
- baseUrl?: string,
70
- ): Promise<{ content: ContentBlock[]; stopReason: string; inputTokens: number; outputTokens: number }> {
71
- const url = `${baseUrl || 'https://api.anthropic.com'}/v1/messages`;
72
- const response = await fetch(url, {
73
- method: 'POST',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- 'x-api-key': apiKey,
77
- 'anthropic-version': '2023-06-01',
78
- },
79
- body: JSON.stringify({
80
- model,
81
- max_tokens: 4096,
82
- system,
83
- tools,
84
- messages,
85
- stream: true,
86
- }),
87
- });
88
-
89
- if (!response.ok) {
90
- const errText = await response.text();
91
- throw new Error(`Anthropic streaming error ${response.status}: ${errText}`);
92
- }
93
-
94
- if (!response.body) throw new Error('No response body for streaming');
95
-
96
- const content: ContentBlock[] = [];
97
- let currentBlock: StreamBlockState | null = null;
98
- let stopReason = 'end_turn';
99
- let inputTokens = 0;
100
- let outputTokens = 0;
101
-
102
- for await (const event of parseAnthropicSSE(response.body)) {
103
- const type = event.type as string;
104
-
105
- if (type === 'content_block_start') {
106
- const cb = event.content_block as Record<string, unknown>;
107
- currentBlock = {
108
- type: ((cb.type as string) ?? 'text') as 'text' | 'tool_use',
109
- text: (cb.text as string) ?? '',
110
- id: (cb.id as string) ?? '',
111
- name: (cb.name as string) ?? '',
112
- inputJson: '',
113
- };
114
- } else if (type === 'content_block_delta') {
115
- const delta = event.delta as Record<string, unknown>;
116
- if (delta.type === 'text_delta' && currentBlock) {
117
- currentBlock.text += (delta.text as string) ?? '';
118
- onDelta((delta.text as string) ?? '');
119
- } else if (delta.type === 'input_json_delta' && currentBlock) {
120
- currentBlock.inputJson += (delta.partial_json as string) ?? '';
121
- }
122
- } else if (type === 'content_block_stop' && currentBlock) {
123
- if (currentBlock.type === 'text') {
124
- content.push({ type: 'text', text: currentBlock.text });
125
- } else {
126
- let input: Record<string, unknown> = {};
127
- try { input = JSON.parse(currentBlock.inputJson || '{}'); } catch { /* empty input */ }
128
- content.push({ type: 'tool_use', id: currentBlock.id, name: currentBlock.name, input });
129
- }
130
- currentBlock = null;
131
- } else if (type === 'message_delta') {
132
- const md = event.delta as Record<string, unknown> | undefined;
133
- if (md?.stop_reason) stopReason = md.stop_reason as string;
134
- } else if (type === 'message_start') {
135
- const msg = event.message as Record<string, unknown> | undefined;
136
- const usage = msg?.usage as Record<string, number> | undefined;
137
- if (usage) inputTokens = usage.input_tokens ?? 0;
138
- } else if (type === 'message_delta') {
139
- const usage = event.usage as Record<string, number> | undefined;
140
- if (usage) outputTokens = usage.output_tokens ?? 0;
141
- }
142
- }
143
-
144
- // Flush any remaining block
145
- if (currentBlock) {
146
- if (currentBlock.type === 'text') {
147
- content.push({ type: 'text', text: currentBlock.text });
148
- } else {
149
- let input: Record<string, unknown> = {};
150
- try { input = JSON.parse(currentBlock.inputJson || '{}'); } catch { /* empty input */ }
151
- content.push({ type: 'tool_use', id: currentBlock.id, name: currentBlock.name, input });
152
- }
153
- }
154
-
155
- return { content, stopReason, inputTokens, outputTokens };
156
- }
157
-
158
18
  // ─── MCP Tool Health Tracker ─────────────────────────────────
159
19
  // Tracks per-tool call outcomes so the heartbeat can surface degradation.
160
20
 
@@ -237,101 +97,138 @@ export async function callMcpWithRetry(
237
97
  return `Tool unavailable: ${name}`;
238
98
  }
239
99
 
100
+ type AnthropicToolDef = {
101
+ name?: unknown;
102
+ description?: unknown;
103
+ input_schema?: unknown;
104
+ };
105
+
106
+ function toolParameters(inputSchema: unknown): Tool['function']['parameters'] {
107
+ if (inputSchema && typeof inputSchema === 'object' && !Array.isArray(inputSchema)) {
108
+ const schema = inputSchema as Record<string, unknown>;
109
+ if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)) {
110
+ return {
111
+ type: 'object',
112
+ properties: schema.properties as Record<string, unknown>,
113
+ required: Array.isArray(schema.required) ? schema.required.filter((v): v is string => typeof v === 'string') : undefined,
114
+ };
115
+ }
116
+ }
117
+
118
+ return { type: 'object', properties: {} };
119
+ }
120
+
121
+ function toProviderTools(tools: unknown[]): Tool[] {
122
+ return (tools as AnthropicToolDef[])
123
+ .filter(tool => typeof tool.name === 'string')
124
+ .map(tool => ({
125
+ type: 'function' as const,
126
+ function: {
127
+ name: tool.name as string,
128
+ description: typeof tool.description === 'string' ? tool.description : '',
129
+ parameters: toolParameters(tool.input_schema),
130
+ },
131
+ }));
132
+ }
133
+
134
+ function buildClaudeProviderFactory(config: ClaudeConfig) {
135
+ if (config.edgeEnv) return buildLLMProviderFactory(config.edgeEnv);
136
+ return createLLMProviderFactory({
137
+ anthropic: {
138
+ apiKey: config.apiKey,
139
+ baseUrl: config.baseUrl,
140
+ },
141
+ fallbackRules: [],
142
+ enableCircuitBreaker: true,
143
+ enableRetries: true,
144
+ });
145
+ }
146
+
147
+ function initialMessages(conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }>, userText: string): LLMMessage[] {
148
+ return [
149
+ ...budgetConversationHistory(conversationHistory).map(message => ({
150
+ role: message.role,
151
+ content: message.content,
152
+ })),
153
+ { role: 'user', content: userText },
154
+ ];
155
+ }
156
+
157
+ function parseToolArgs(call: ToolCall): Record<string, unknown> {
158
+ try {
159
+ const parsed = JSON.parse(call.function.arguments || '{}');
160
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
161
+ } catch {
162
+ return {};
163
+ }
164
+ }
165
+
166
+ async function executeToolCall(config: ClaudeConfig, anthropicConfig: { apiKey: string; model: string; baseUrl: string }, call: ToolCall): Promise<LLMToolResult> {
167
+ const args = parseToolArgs(call);
168
+ const inProcess = await handleInProcessTool(
169
+ config.db,
170
+ call.function.name,
171
+ args,
172
+ config.githubToken,
173
+ config.githubRepo,
174
+ config.braveApiKey,
175
+ config.roundtableDb,
176
+ anthropicConfig,
177
+ config.memoryBinding,
178
+ config.resendApiKeys,
179
+ config.edgeEnv,
180
+ );
181
+
182
+ if (inProcess !== null) return { id: call.id, output: inProcess };
183
+
184
+ const resolved = resolveMcpTool(call.function.name, config.mcpClient, config.mcpRegistry);
185
+ if (resolved) {
186
+ return { id: call.id, output: await callMcpWithRetry(resolved.client, resolved.mcpName, args) };
187
+ }
188
+
189
+ return { id: call.id, output: `Unknown tool: ${call.function.name}` };
190
+ }
191
+
240
192
  export async function executeClaudeChat(
241
193
  config: ClaudeConfig,
242
194
  userText: string,
243
195
  ): Promise<{ text: string; cost: number }> {
244
196
  config.userQuery = userText;
245
197
  const { systemPrompt, tools, conversationHistory } = await buildContext(config, config.roundtableDb);
246
- const anthropicBase = config.baseUrl || 'https://api.anthropic.com';
247
- const anthropicConfig = { apiKey: config.apiKey, model: config.model, baseUrl: anthropicBase };
248
-
249
- const messages: Message[] = [
250
- ...budgetConversationHistory(conversationHistory),
251
- { role: 'user', content: userText },
252
- ];
198
+ const anthropicConfig = { apiKey: config.apiKey, model: config.model, baseUrl: config.baseUrl || 'https://api.anthropic.com' };
199
+ const factory = buildClaudeProviderFactory(config);
200
+ const providerTools = toProviderTools(tools);
201
+ const messages = initialMessages(conversationHistory, userText);
253
202
 
254
203
  let totalCost = 0;
255
204
  const MAX_TOOL_ROUNDS = 10;
256
205
 
257
206
  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
258
- const response = await fetch(`${anthropicBase}/v1/messages`, {
259
- method: 'POST',
260
- headers: {
261
- 'Content-Type': 'application/json',
262
- 'x-api-key': config.apiKey,
263
- 'anthropic-version': '2023-06-01',
264
- },
265
- body: JSON.stringify({
266
- model: config.model,
267
- max_tokens: 4096,
268
- system: systemPrompt,
269
- tools,
270
- messages,
271
- }),
207
+ const result = await factory.generateResponse({
208
+ messages: [...messages],
209
+ model: config.model,
210
+ systemPrompt,
211
+ tools: providerTools,
212
+ maxTokens: 4096,
272
213
  });
273
214
 
274
- if (!response.ok) {
275
- const errText = await response.text();
276
- throw new Error(`Anthropic API error ${response.status}: ${errText}`);
277
- }
278
-
279
- const data = await response.json<ApiResponse>();
280
-
281
- const rates = getModelCostRates(config.model);
282
- totalCost += (data.usage.input_tokens * rates.input + data.usage.output_tokens * rates.output) / 1_000_000;
283
-
284
- // Check if we're done (no tool use)
285
- if (data.stop_reason === 'end_turn' || data.stop_reason === 'max_tokens') {
286
- const textBlocks = data.content.filter(b => b.type === 'text');
287
- return {
288
- text: textBlocks.map(b => b.text ?? '').join('') || '(no response)',
289
- cost: totalCost,
290
- };
291
- }
292
-
293
- // Handle tool use
294
- if (data.stop_reason === 'tool_use') {
295
- // Add assistant message with all content blocks
296
- messages.push({ role: 'assistant', content: data.content });
297
-
298
- // Process each tool use
299
- const toolResults: ContentBlock[] = [];
300
-
301
- for (const block of data.content) {
302
- if (block.type !== 'tool_use' || !block.id || !block.name) continue;
303
-
304
- let result: string;
305
- const inProcess = await handleInProcessTool(config.db, block.name, block.input ?? {}, config.githubToken, config.githubRepo, config.braveApiKey, config.roundtableDb, anthropicConfig, config.memoryBinding, config.resendApiKeys);
306
- if (inProcess !== null) {
307
- result = inProcess;
308
- } else {
309
- const resolved = resolveMcpTool(block.name, config.mcpClient, config.mcpRegistry);
310
- if (resolved) {
311
- result = await callMcpWithRetry(resolved.client, resolved.mcpName, block.input ?? {});
312
- } else {
313
- result = `Unknown tool: ${block.name}`;
314
- }
315
- }
316
-
317
- toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result } as unknown as ContentBlock);
318
- }
215
+ totalCost += result.usage.cost;
319
216
 
320
- messages.push({ role: 'user', content: toolResults });
321
- continue;
217
+ if (!result.toolCalls || result.toolCalls.length === 0) {
218
+ return { text: result.message || '(no response)', cost: totalCost };
322
219
  }
323
220
 
324
- const textBlocks = data.content.filter(b => b.type === 'text');
325
- return { text: textBlocks.map(b => b.text ?? '').join('') || '(no response)', cost: totalCost };
221
+ const toolResults = await Promise.all(result.toolCalls.map(call => executeToolCall(config, anthropicConfig, call)));
222
+ messages.push({ role: 'assistant', content: result.message, toolCalls: result.toolCalls });
223
+ messages.push({ role: 'user', content: '', toolResults });
326
224
  }
327
225
 
328
226
  return { text: '(reached maximum tool rounds)', cost: totalCost };
329
227
  }
330
228
 
331
229
  // ─── Streaming variant ───────────────────────────────────────
332
- // Tool-use rounds are non-streaming (fast); only the final end_turn text streams.
333
- // onDelta is buffered per round and flushed only on end_turn, so intermediate
334
- // "I'll check the dashboard..." text never leaks to the UI.
230
+ // Tool-use rounds use the provider factory. The final answer is emitted as one
231
+ // delta so intermediate tool-planning text stays invisible to the UI.
335
232
 
336
233
  export async function executeClaudeChatStream(
337
234
  config: ClaudeConfig,
@@ -340,66 +237,34 @@ export async function executeClaudeChatStream(
340
237
  ): Promise<{ text: string; cost: number }> {
341
238
  config.userQuery = userText;
342
239
  const { systemPrompt, tools, conversationHistory } = await buildContext(config, config.roundtableDb);
343
- const anthropicBaseStream = config.baseUrl || 'https://api.anthropic.com';
344
- const anthropicConfigStream = { apiKey: config.apiKey, model: config.model, baseUrl: anthropicBaseStream };
345
-
346
- const messages: Message[] = [...budgetConversationHistory(conversationHistory), { role: 'user', content: userText }];
240
+ const anthropicConfig = { apiKey: config.apiKey, model: config.model, baseUrl: config.baseUrl || 'https://api.anthropic.com' };
241
+ const factory = buildClaudeProviderFactory(config);
242
+ const providerTools = toProviderTools(tools);
243
+ const messages = initialMessages(conversationHistory, userText);
347
244
 
348
245
  let totalCost = 0;
349
246
  const MAX_TOOL_ROUNDS = 10;
350
247
 
351
248
  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
352
- // Buffer deltas only flush to onDelta if this round ends with end_turn
353
- const roundBuffer: string[] = [];
354
-
355
- const { content, stopReason, inputTokens, outputTokens } = await callAnthropicStream(
356
- config.apiKey, config.model, systemPrompt, messages, tools,
357
- (delta) => roundBuffer.push(delta),
358
- config.baseUrl,
359
- );
360
-
361
- const rates = getModelCostRates(config.model);
362
- totalCost += (inputTokens * rates.input + outputTokens * rates.output) / 1_000_000;
363
-
364
- if (stopReason === 'end_turn' || stopReason === 'max_tokens') {
365
- // Flush buffered deltas to caller now that we know it's the final round
366
- for (const delta of roundBuffer) onDelta(delta);
367
- const text = content.filter(b => b.type === 'text').map(b => b.text ?? '').join('') || '(no response)';
368
- return { text, cost: totalCost };
369
- }
249
+ const result = await factory.generateResponse({
250
+ messages: [...messages],
251
+ model: config.model,
252
+ systemPrompt,
253
+ tools: providerTools,
254
+ maxTokens: 4096,
255
+ });
370
256
 
371
- if (stopReason === 'tool_use') {
372
- // Discard roundBuffer — tool-use intermediate text stays invisible
373
- messages.push({ role: 'assistant', content });
374
- const toolResults: ContentBlock[] = [];
375
-
376
- for (const block of content) {
377
- if (block.type !== 'tool_use' || !block.id || !block.name) continue;
378
- let result: string;
379
-
380
- const inProcess = await handleInProcessTool(config.db, block.name, block.input ?? {}, config.githubToken, config.githubRepo, config.braveApiKey, config.roundtableDb, anthropicConfigStream, config.memoryBinding);
381
- if (inProcess !== null) {
382
- result = inProcess;
383
- } else {
384
- const resolved = resolveMcpTool(block.name, config.mcpClient, config.mcpRegistry);
385
- if (resolved) {
386
- result = await callMcpWithRetry(resolved.client, resolved.mcpName, block.input ?? {});
387
- } else {
388
- result = `Unknown tool: ${block.name}`;
389
- }
390
- }
391
-
392
- toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: result } as unknown as ContentBlock);
393
- }
257
+ totalCost += result.usage.cost;
394
258
 
395
- messages.push({ role: 'user', content: toolResults });
396
- continue;
259
+ if (!result.toolCalls || result.toolCalls.length === 0) {
260
+ const text = result.message || '(no response)';
261
+ onDelta(text);
262
+ return { text, cost: totalCost };
397
263
  }
398
264
 
399
- // Unexpected stop reason flush whatever we have
400
- for (const delta of roundBuffer) onDelta(delta);
401
- const text = content.filter(b => b.type === 'text').map(b => b.text ?? '').join('') || '(no response)';
402
- return { text, cost: totalCost };
265
+ const toolResults = await Promise.all(result.toolCalls.map(call => executeToolCall(config, anthropicConfig, call)));
266
+ messages.push({ role: 'assistant', content: result.message, toolCalls: result.toolCalls });
267
+ messages.push({ role: 'user', content: '', toolResults });
403
268
  }
404
269
 
405
270
  return { text: '(reached maximum tool rounds)', cost: totalCost };
package/src/codebeast.ts CHANGED
File without changes