@metabob/minibob 0.1.2
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/ARCHITECTURE.md +255 -0
- package/CHANGELOG.md +112 -0
- package/README.md +380 -0
- package/bin/minibob.js +36 -0
- package/dist/acp-gossip.d.ts +72 -0
- package/dist/acp-gossip.d.ts.map +1 -0
- package/dist/acp-gossip.js +156 -0
- package/dist/acp-gossip.js.map +1 -0
- package/dist/acp.d.ts +62 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +292 -0
- package/dist/acp.js.map +1 -0
- package/dist/activity.d.ts +157 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +518 -0
- package/dist/activity.js.map +1 -0
- package/dist/agent-runtime.d.ts +104 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/boredom.d.ts +125 -0
- package/dist/boredom.d.ts.map +1 -0
- package/dist/boredom.js +244 -0
- package/dist/boredom.js.map +1 -0
- package/dist/cli/acp-server.d.ts +23 -0
- package/dist/cli/acp-server.d.ts.map +1 -0
- package/dist/cli/burrow.d.ts +26 -0
- package/dist/cli/burrow.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/goal.d.ts +22 -0
- package/dist/cli/goal.d.ts.map +1 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/instance-registry.d.ts +78 -0
- package/dist/cli/instance-registry.d.ts.map +1 -0
- package/dist/cli/observe.d.ts +35 -0
- package/dist/cli/observe.d.ts.map +1 -0
- package/dist/cli/vessel.d.ts +14 -0
- package/dist/cli/vessel.d.ts.map +1 -0
- package/dist/composition-observer.d.ts +96 -0
- package/dist/composition-observer.d.ts.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +128 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/Dockerfile +35 -0
- package/dist/environment.d.ts +72 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +142 -0
- package/dist/environment.js.map +1 -0
- package/dist/goal-processor.d.ts +165 -0
- package/dist/goal-processor.d.ts.map +1 -0
- package/dist/helm/minibob-cluster/Chart.yaml +13 -0
- package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
- package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
- package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
- package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
- package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
- package/dist/helm/minibob-cluster/values-local.yaml +41 -0
- package/dist/helm/minibob-cluster/values-production.yaml +57 -0
- package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
- package/dist/helm/minibob-cluster/values.yaml +127 -0
- package/dist/improviser.d.ts +74 -0
- package/dist/improviser.d.ts.map +1 -0
- package/dist/impulse-filter.d.ts +74 -0
- package/dist/impulse-filter.d.ts.map +1 -0
- package/dist/impulse.d.ts +92 -0
- package/dist/impulse.d.ts.map +1 -0
- package/dist/impulse.js +234 -0
- package/dist/impulse.js.map +1 -0
- package/dist/lib.d.ts +29 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +18561 -0
- package/dist/lib.js.map +98 -0
- package/dist/lifecycle-hooks.d.ts +99 -0
- package/dist/lifecycle-hooks.d.ts.map +1 -0
- package/dist/lifecycle-hooks.js +135 -0
- package/dist/lifecycle-hooks.js.map +1 -0
- package/dist/llm.d.ts +31 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +349 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp-activity-bridge.d.ts +66 -0
- package/dist/mcp-activity-bridge.d.ts.map +1 -0
- package/dist/mcp-activity-bridge.js +126 -0
- package/dist/mcp-activity-bridge.js.map +1 -0
- package/dist/mcp.d.ts +216 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +292 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory-agent.d.ts +92 -0
- package/dist/memory-agent.d.ts.map +1 -0
- package/dist/memory-agent.js +277 -0
- package/dist/memory-agent.js.map +1 -0
- package/dist/runtime-mapping.d.ts +97 -0
- package/dist/runtime-mapping.d.ts.map +1 -0
- package/dist/search-first-executor.d.ts +113 -0
- package/dist/search-first-executor.d.ts.map +1 -0
- package/dist/session.d.ts +48 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/template-extractor.d.ts +9 -0
- package/dist/template-extractor.d.ts.map +1 -0
- package/dist/template-generator.d.ts +12 -0
- package/dist/template-generator.d.ts.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +771 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +503 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/understanding/analyzer.d.ts +55 -0
- package/dist/understanding/analyzer.d.ts.map +1 -0
- package/dist/understanding/explorer.d.ts +73 -0
- package/dist/understanding/explorer.d.ts.map +1 -0
- package/dist/understanding/index.d.ts +7 -0
- package/dist/understanding/index.d.ts.map +1 -0
- package/dist/understanding/types.d.ts +136 -0
- package/dist/understanding/types.d.ts.map +1 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +106 -0
- package/dist/validation.js.map +1 -0
- package/dist/vessel-bootstrap.d.ts +190 -0
- package/dist/vessel-bootstrap.d.ts.map +1 -0
- package/dist/vessel-registry.d.ts +229 -0
- package/dist/vessel-registry.d.ts.map +1 -0
- package/index.ts +1329 -0
- package/package.json +54 -0
- package/src/acp-gossip.ts +193 -0
- package/src/acp.ts +362 -0
- package/src/activity.ts +1464 -0
- package/src/agent-runtime.ts +365 -0
- package/src/boredom.ts +423 -0
- package/src/cli/acp-server.ts +377 -0
- package/src/cli/burrow.ts +896 -0
- package/src/cli/doctor.ts +526 -0
- package/src/cli/goal.ts +224 -0
- package/src/cli/index.ts +147 -0
- package/src/cli/instance-registry.ts +271 -0
- package/src/cli/observe.ts +682 -0
- package/src/cli/vessel.ts +287 -0
- package/src/components/SystemOverview.tsx +331 -0
- package/src/composition-observer.ts +449 -0
- package/src/config.ts +172 -0
- package/src/environment.ts +167 -0
- package/src/goal-processor.ts +654 -0
- package/src/improviser.ts +591 -0
- package/src/impulse-filter.ts +273 -0
- package/src/impulse.ts +311 -0
- package/src/lib.ts +147 -0
- package/src/lifecycle-hooks.ts +181 -0
- package/src/llm.ts +434 -0
- package/src/mcp-activity-bridge.ts +158 -0
- package/src/mcp.ts +747 -0
- package/src/memory-agent.ts +316 -0
- package/src/runtime-mapping.ts +527 -0
- package/src/search-first-executor.ts +666 -0
- package/src/session.ts +141 -0
- package/src/template-extractor.ts +256 -0
- package/src/template-generator.ts +130 -0
- package/src/tools.ts +924 -0
- package/src/types.ts +497 -0
- package/src/understanding/analyzer.ts +354 -0
- package/src/understanding/explorer.ts +488 -0
- package/src/understanding/index.ts +27 -0
- package/src/understanding/types.ts +153 -0
- package/src/validation.ts +125 -0
- package/src/vessel-bootstrap.ts +440 -0
- package/src/vessel-registry.ts +621 -0
- package/templates/core/edit-file.json +85 -0
- package/templates/understanding/diagnose-problem.json +32 -0
- package/templates/understanding/explore-codebase-v2.json +57 -0
- package/templates/understanding/explore-codebase.json +37 -0
package/src/llm.ts
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob LLM Client
|
|
3
|
+
*
|
|
4
|
+
* Minimal LLM integration supporting Anthropic Claude.
|
|
5
|
+
* Handles tool calling loop for activity execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CompletionOptions,
|
|
10
|
+
CompletionResult,
|
|
11
|
+
ToolResult,
|
|
12
|
+
ToolHandler,
|
|
13
|
+
} from "./types"
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// LLM CLIENT
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
export interface LLMClient {
|
|
20
|
+
complete(options: CompletionOptions): Promise<CompletionResult>
|
|
21
|
+
completeWithTools(
|
|
22
|
+
options: CompletionOptions,
|
|
23
|
+
toolHandlers: Record<string, ToolHandler>
|
|
24
|
+
): Promise<{ content: string; toolsUsed: string[]; usage: { inputTokens: number; outputTokens: number } }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create Anthropic LLM client
|
|
29
|
+
*/
|
|
30
|
+
export function createAnthropicClient(apiKey: string): LLMClient {
|
|
31
|
+
const baseUrl = "https://api.anthropic.com/v1"
|
|
32
|
+
|
|
33
|
+
async function complete(options: CompletionOptions): Promise<CompletionResult> {
|
|
34
|
+
// Convert messages to Anthropic format
|
|
35
|
+
const systemMessage = options.messages.find((m) => m.role === "system")
|
|
36
|
+
const conversationMessages = options.messages
|
|
37
|
+
.filter((m) => m.role !== "system")
|
|
38
|
+
.map((m) => {
|
|
39
|
+
// Tool result message
|
|
40
|
+
if (m.role === "tool") {
|
|
41
|
+
return {
|
|
42
|
+
role: "user" as const,
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "tool_result" as const,
|
|
46
|
+
tool_use_id: m.toolCallId!,
|
|
47
|
+
content: m.content,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Assistant message with tool calls
|
|
54
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
|
|
55
|
+
const content: Array<any> = []
|
|
56
|
+
|
|
57
|
+
// Add text content if present
|
|
58
|
+
if (m.content) {
|
|
59
|
+
content.push({ type: "text", text: m.content })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add tool_use blocks
|
|
63
|
+
for (const toolCall of m.toolCalls) {
|
|
64
|
+
content.push({
|
|
65
|
+
type: "tool_use",
|
|
66
|
+
id: toolCall.id,
|
|
67
|
+
name: toolCall.name,
|
|
68
|
+
input: toolCall.arguments,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
role: "assistant" as const,
|
|
74
|
+
content,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Regular message
|
|
79
|
+
return {
|
|
80
|
+
role: m.role,
|
|
81
|
+
content: m.content,
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Convert tools to Anthropic format
|
|
86
|
+
const tools = options.tools?.map((t) => ({
|
|
87
|
+
name: t.name,
|
|
88
|
+
description: t.description,
|
|
89
|
+
input_schema: t.parameters,
|
|
90
|
+
}))
|
|
91
|
+
|
|
92
|
+
const body: Record<string, unknown> = {
|
|
93
|
+
model: options.model,
|
|
94
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
95
|
+
messages: conversationMessages,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (systemMessage) {
|
|
99
|
+
body.system = systemMessage.content
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (tools && tools.length > 0) {
|
|
103
|
+
body.tools = tools
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.temperature !== undefined) {
|
|
107
|
+
body.temperature = options.temperature
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const response = await fetch(`${baseUrl}/messages`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
"x-api-key": apiKey,
|
|
115
|
+
"anthropic-version": "2023-06-01",
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const error = await response.text()
|
|
122
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = (await response.json()) as {
|
|
126
|
+
content: Array<{ type: string; text?: string; id?: string; name?: string; input?: Record<string, unknown> }>
|
|
127
|
+
stop_reason: string
|
|
128
|
+
usage: { input_tokens: number; output_tokens: number }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Extract content and tool calls
|
|
132
|
+
let content = ""
|
|
133
|
+
const toolCalls: Array<{ id: string; name: string; arguments: Record<string, unknown> }> = []
|
|
134
|
+
|
|
135
|
+
for (const block of data.content) {
|
|
136
|
+
if (block.type === "text" && block.text) {
|
|
137
|
+
content += block.text
|
|
138
|
+
} else if (block.type === "tool_use" && block.id && block.name) {
|
|
139
|
+
toolCalls.push({
|
|
140
|
+
id: block.id,
|
|
141
|
+
name: block.name,
|
|
142
|
+
arguments: block.input ?? {},
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content,
|
|
149
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
150
|
+
finishReason: data.stop_reason === "end_turn" ? "stop" : data.stop_reason === "tool_use" ? "tool_calls" : "stop",
|
|
151
|
+
usage: {
|
|
152
|
+
inputTokens: data.usage.input_tokens,
|
|
153
|
+
outputTokens: data.usage.output_tokens,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function completeWithTools(
|
|
159
|
+
options: CompletionOptions,
|
|
160
|
+
toolHandlers: Record<string, ToolHandler>
|
|
161
|
+
): Promise<{ content: string; toolsUsed: string[]; usage: { inputTokens: number; outputTokens: number } }> {
|
|
162
|
+
const messages = [...options.messages]
|
|
163
|
+
const toolsUsed: string[] = []
|
|
164
|
+
let totalInputTokens = 0
|
|
165
|
+
let totalOutputTokens = 0
|
|
166
|
+
let finalContent = ""
|
|
167
|
+
let maxIterations = 20 // Prevent infinite loops
|
|
168
|
+
|
|
169
|
+
while (maxIterations-- > 0) {
|
|
170
|
+
const result = await complete({
|
|
171
|
+
...options,
|
|
172
|
+
messages,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
totalInputTokens += result.usage.inputTokens
|
|
176
|
+
totalOutputTokens += result.usage.outputTokens
|
|
177
|
+
|
|
178
|
+
// If no tool calls, we're done
|
|
179
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
180
|
+
finalContent = result.content
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add assistant message with tool calls
|
|
185
|
+
messages.push({
|
|
186
|
+
role: "assistant",
|
|
187
|
+
content: result.content,
|
|
188
|
+
toolCalls: result.toolCalls,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Execute tool calls
|
|
192
|
+
for (const toolCall of result.toolCalls) {
|
|
193
|
+
const handler = toolHandlers[toolCall.name]
|
|
194
|
+
let toolResult: ToolResult
|
|
195
|
+
|
|
196
|
+
if (!handler) {
|
|
197
|
+
toolResult = {
|
|
198
|
+
success: false,
|
|
199
|
+
error: `Unknown tool: ${toolCall.name}`,
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
toolResult = await handler(toolCall.arguments)
|
|
204
|
+
toolsUsed.push(toolCall.name)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
toolResult = {
|
|
207
|
+
success: false,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add tool result message
|
|
214
|
+
messages.push({
|
|
215
|
+
role: "tool",
|
|
216
|
+
content: toolResult.success
|
|
217
|
+
? toolResult.output ?? "Success"
|
|
218
|
+
: `Error: ${toolResult.error}`,
|
|
219
|
+
toolCallId: toolCall.id,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
content: finalContent,
|
|
226
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
227
|
+
usage: {
|
|
228
|
+
inputTokens: totalInputTokens,
|
|
229
|
+
outputTokens: totalOutputTokens,
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { complete, completeWithTools }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create OpenAI-compatible LLM client
|
|
239
|
+
*/
|
|
240
|
+
export function createOpenAIClient(apiKey: string, baseUrl = "https://api.openai.com/v1"): LLMClient {
|
|
241
|
+
async function complete(options: CompletionOptions): Promise<CompletionResult> {
|
|
242
|
+
// Convert messages to OpenAI format
|
|
243
|
+
const messages = options.messages.map((m) => {
|
|
244
|
+
if (m.role === "tool") {
|
|
245
|
+
return {
|
|
246
|
+
role: "tool" as const,
|
|
247
|
+
tool_call_id: m.toolCallId,
|
|
248
|
+
content: m.content,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (m.role === "assistant" && m.toolCalls) {
|
|
252
|
+
return {
|
|
253
|
+
role: "assistant" as const,
|
|
254
|
+
content: m.content || null,
|
|
255
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
256
|
+
id: tc.id,
|
|
257
|
+
type: "function" as const,
|
|
258
|
+
function: {
|
|
259
|
+
name: tc.name,
|
|
260
|
+
arguments: JSON.stringify(tc.arguments),
|
|
261
|
+
},
|
|
262
|
+
})),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
role: m.role,
|
|
267
|
+
content: m.content,
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// Convert tools to OpenAI format
|
|
272
|
+
const tools = options.tools?.map((t) => ({
|
|
273
|
+
type: "function" as const,
|
|
274
|
+
function: {
|
|
275
|
+
name: t.name,
|
|
276
|
+
description: t.description,
|
|
277
|
+
parameters: t.parameters,
|
|
278
|
+
},
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
const body: Record<string, unknown> = {
|
|
282
|
+
model: options.model,
|
|
283
|
+
messages,
|
|
284
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (tools && tools.length > 0) {
|
|
288
|
+
body.tools = tools
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (options.temperature !== undefined) {
|
|
292
|
+
body.temperature = options.temperature
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: {
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
Authorization: `Bearer ${apiKey}`,
|
|
300
|
+
},
|
|
301
|
+
body: JSON.stringify(body),
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
const error = await response.text()
|
|
306
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const data = (await response.json()) as {
|
|
310
|
+
choices: Array<{
|
|
311
|
+
message: {
|
|
312
|
+
content: string | null
|
|
313
|
+
tool_calls?: Array<{
|
|
314
|
+
id: string
|
|
315
|
+
function: { name: string; arguments: string }
|
|
316
|
+
}>
|
|
317
|
+
}
|
|
318
|
+
finish_reason: string
|
|
319
|
+
}>
|
|
320
|
+
usage: { prompt_tokens: number; completion_tokens: number }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const choice = data.choices[0]
|
|
324
|
+
if (!choice) {
|
|
325
|
+
throw new Error("No response from OpenAI API")
|
|
326
|
+
}
|
|
327
|
+
const toolCalls = choice.message.tool_calls?.map((tc) => ({
|
|
328
|
+
id: tc.id,
|
|
329
|
+
name: tc.function.name,
|
|
330
|
+
arguments: JSON.parse(tc.function.arguments) as Record<string, unknown>,
|
|
331
|
+
}))
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
content: choice.message.content ?? "",
|
|
335
|
+
toolCalls,
|
|
336
|
+
finishReason:
|
|
337
|
+
choice.finish_reason === "stop"
|
|
338
|
+
? "stop"
|
|
339
|
+
: choice.finish_reason === "tool_calls"
|
|
340
|
+
? "tool_calls"
|
|
341
|
+
: "stop",
|
|
342
|
+
usage: {
|
|
343
|
+
inputTokens: data.usage.prompt_tokens,
|
|
344
|
+
outputTokens: data.usage.completion_tokens,
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function completeWithTools(
|
|
350
|
+
options: CompletionOptions,
|
|
351
|
+
toolHandlers: Record<string, ToolHandler>
|
|
352
|
+
): Promise<{ content: string; toolsUsed: string[]; usage: { inputTokens: number; outputTokens: number } }> {
|
|
353
|
+
const messages = [...options.messages]
|
|
354
|
+
const toolsUsed: string[] = []
|
|
355
|
+
let totalInputTokens = 0
|
|
356
|
+
let totalOutputTokens = 0
|
|
357
|
+
let finalContent = ""
|
|
358
|
+
let maxIterations = 20
|
|
359
|
+
|
|
360
|
+
while (maxIterations-- > 0) {
|
|
361
|
+
const result = await complete({
|
|
362
|
+
...options,
|
|
363
|
+
messages,
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
totalInputTokens += result.usage.inputTokens
|
|
367
|
+
totalOutputTokens += result.usage.outputTokens
|
|
368
|
+
|
|
369
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
370
|
+
finalContent = result.content
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
messages.push({
|
|
375
|
+
role: "assistant",
|
|
376
|
+
content: result.content,
|
|
377
|
+
toolCalls: result.toolCalls,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
for (const toolCall of result.toolCalls) {
|
|
381
|
+
const handler = toolHandlers[toolCall.name]
|
|
382
|
+
let toolResult: ToolResult
|
|
383
|
+
|
|
384
|
+
if (!handler) {
|
|
385
|
+
toolResult = {
|
|
386
|
+
success: false,
|
|
387
|
+
error: `Unknown tool: ${toolCall.name}`,
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
try {
|
|
391
|
+
toolResult = await handler(toolCall.arguments)
|
|
392
|
+
toolsUsed.push(toolCall.name)
|
|
393
|
+
} catch (error) {
|
|
394
|
+
toolResult = {
|
|
395
|
+
success: false,
|
|
396
|
+
error: error instanceof Error ? error.message : String(error),
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
messages.push({
|
|
402
|
+
role: "tool",
|
|
403
|
+
content: toolResult.success ? toolResult.output ?? "Success" : `Error: ${toolResult.error}`,
|
|
404
|
+
toolCallId: toolCall.id,
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
content: finalContent,
|
|
411
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
412
|
+
usage: {
|
|
413
|
+
inputTokens: totalInputTokens,
|
|
414
|
+
outputTokens: totalOutputTokens,
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { complete, completeWithTools }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Create LLM client based on provider
|
|
424
|
+
*/
|
|
425
|
+
export function createLLMClient(provider: "anthropic" | "openai", apiKey: string): LLMClient {
|
|
426
|
+
switch (provider) {
|
|
427
|
+
case "anthropic":
|
|
428
|
+
return createAnthropicClient(apiKey)
|
|
429
|
+
case "openai":
|
|
430
|
+
return createOpenAIClient(apiKey)
|
|
431
|
+
default:
|
|
432
|
+
throw new Error(`Unknown provider: ${provider}`)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Activity Bridge
|
|
3
|
+
*
|
|
4
|
+
* Bridges MiniBob's activity callbacks to Metabob MCP tools.
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* - MiniBob activity.ts defines callbacks (onSearchActivities, onCreateActivity)
|
|
8
|
+
* - This bridge translates those callbacks to MCP tool calls
|
|
9
|
+
* - MCP tools (in metabob-cli Python) delegate to RPC API
|
|
10
|
+
* - RPC API handles Thompson Sampling and learning (NOT OpenCode)
|
|
11
|
+
*
|
|
12
|
+
* Specification: minibob-trailblazing-activity-system
|
|
13
|
+
* Compliance: Activity-first constraint enforcement
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getMCPClient } from "./mcp"
|
|
17
|
+
|
|
18
|
+
export namespace MCPActivityBridge {
|
|
19
|
+
/**
|
|
20
|
+
* Search activities via /v2/activities/templates endpoint.
|
|
21
|
+
*
|
|
22
|
+
* This implements the onSearchActivities callback expected by MiniBob tools.
|
|
23
|
+
*
|
|
24
|
+
* @param category Optional category filter (feature, bugfix, refactor, tool, infrastructure)
|
|
25
|
+
* @param verbose If true, return full template details; if false, return minimal info
|
|
26
|
+
* @returns Activity search results
|
|
27
|
+
*/
|
|
28
|
+
export async function searchActivities(
|
|
29
|
+
category?: string,
|
|
30
|
+
_verbose?: boolean // Reserved for future use
|
|
31
|
+
): Promise<{ count: number; activities: unknown[] }> {
|
|
32
|
+
const mcp = getMCPClient()
|
|
33
|
+
|
|
34
|
+
if (!mcp) {
|
|
35
|
+
console.warn("[MCPActivityBridge] MCP not available, returning empty results")
|
|
36
|
+
return { count: 0, activities: [] }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Call the /v2/activities/templates endpoint directly
|
|
41
|
+
const params = new URLSearchParams()
|
|
42
|
+
if (category) params.set("category", category)
|
|
43
|
+
params.set("limit", "50")
|
|
44
|
+
|
|
45
|
+
const url = `/v2/activities/templates?${params.toString()}`
|
|
46
|
+
const response = await mcp["request"]("GET", url)
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`MCP search failed: ${response.status} ${response.statusText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await response.json() as {
|
|
53
|
+
templates?: unknown[]
|
|
54
|
+
total?: number
|
|
55
|
+
error?: string
|
|
56
|
+
message?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
console.error("[MCPActivityBridge] Search error:", result.error)
|
|
61
|
+
return { count: 0, activities: [] }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
count: result.total ?? (result.templates?.length ?? 0),
|
|
66
|
+
activities: result.templates ?? [],
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("[MCPActivityBridge] Search activities failed:", error)
|
|
70
|
+
return { count: 0, activities: [] }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create activity via metabob_create_activity_goal_seeking MCP tool.
|
|
76
|
+
*
|
|
77
|
+
* This implements the onCreateActivity callback expected by MiniBob tools.
|
|
78
|
+
*
|
|
79
|
+
* Architecture Note:
|
|
80
|
+
* - This delegates to MCP tool (metabob_create_activity_goal_seeking)
|
|
81
|
+
* - MCP tool calls RPC API /v2/activities/create-goal-seeking
|
|
82
|
+
* - RPC API handles:
|
|
83
|
+
* - Thompson Sampling initialization (NOT OpenCode)
|
|
84
|
+
* - Goal decomposition
|
|
85
|
+
* - Template registration
|
|
86
|
+
*
|
|
87
|
+
* @param params Goal seeking parameters
|
|
88
|
+
* @returns Created template ID
|
|
89
|
+
*/
|
|
90
|
+
export async function createActivity(params: {
|
|
91
|
+
goalDescription: string
|
|
92
|
+
templateName: string
|
|
93
|
+
category: string
|
|
94
|
+
variables: Record<string, unknown>
|
|
95
|
+
impulseRefs?: string[]
|
|
96
|
+
constraints?: {
|
|
97
|
+
maxTasks?: number
|
|
98
|
+
maxCost?: number
|
|
99
|
+
preferComposition?: boolean
|
|
100
|
+
}
|
|
101
|
+
}): Promise<{ templateId: string }> {
|
|
102
|
+
const mcp = getMCPClient()
|
|
103
|
+
|
|
104
|
+
if (!mcp) {
|
|
105
|
+
throw new Error("MCP not available - cannot create activity template")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Call backend create-goal-seeking API
|
|
110
|
+
const response = await mcp["request"]("POST", "/v2/activities/create-goal-seeking", {
|
|
111
|
+
goal_description: params.goalDescription,
|
|
112
|
+
template_name: params.templateName,
|
|
113
|
+
category: params.category,
|
|
114
|
+
variables: params.variables,
|
|
115
|
+
impulse_refs: params.impulseRefs ?? [],
|
|
116
|
+
constraints: params.constraints ?? {
|
|
117
|
+
max_tasks: 7,
|
|
118
|
+
max_cost: 5.0,
|
|
119
|
+
prefer_composition: true,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`MCP create activity failed: ${response.status} ${response.statusText}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await response.json() as {
|
|
128
|
+
status: string
|
|
129
|
+
template_id?: string
|
|
130
|
+
error?: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.status !== "success" || !result.template_id) {
|
|
134
|
+
throw new Error(`Failed to create activity: ${result.error ?? "Unknown error"}`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(
|
|
138
|
+
`[MCPActivityBridge] Created activity template: ${result.template_id} via goal-seeking`
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
templateId: result.template_id,
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("[MCPActivityBridge] Create activity failed:", error)
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if MCP activity bridge is available.
|
|
152
|
+
*
|
|
153
|
+
* Returns true if MCP client is initialized and bridge can be used.
|
|
154
|
+
*/
|
|
155
|
+
export function isAvailable(): boolean {
|
|
156
|
+
return getMCPClient() !== null
|
|
157
|
+
}
|
|
158
|
+
}
|