@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.
Files changed (174) hide show
  1. package/ARCHITECTURE.md +255 -0
  2. package/CHANGELOG.md +112 -0
  3. package/README.md +380 -0
  4. package/bin/minibob.js +36 -0
  5. package/dist/acp-gossip.d.ts +72 -0
  6. package/dist/acp-gossip.d.ts.map +1 -0
  7. package/dist/acp-gossip.js +156 -0
  8. package/dist/acp-gossip.js.map +1 -0
  9. package/dist/acp.d.ts +62 -0
  10. package/dist/acp.d.ts.map +1 -0
  11. package/dist/acp.js +292 -0
  12. package/dist/acp.js.map +1 -0
  13. package/dist/activity.d.ts +157 -0
  14. package/dist/activity.d.ts.map +1 -0
  15. package/dist/activity.js +518 -0
  16. package/dist/activity.js.map +1 -0
  17. package/dist/agent-runtime.d.ts +104 -0
  18. package/dist/agent-runtime.d.ts.map +1 -0
  19. package/dist/boredom.d.ts +125 -0
  20. package/dist/boredom.d.ts.map +1 -0
  21. package/dist/boredom.js +244 -0
  22. package/dist/boredom.js.map +1 -0
  23. package/dist/cli/acp-server.d.ts +23 -0
  24. package/dist/cli/acp-server.d.ts.map +1 -0
  25. package/dist/cli/burrow.d.ts +26 -0
  26. package/dist/cli/burrow.d.ts.map +1 -0
  27. package/dist/cli/doctor.d.ts +22 -0
  28. package/dist/cli/doctor.d.ts.map +1 -0
  29. package/dist/cli/goal.d.ts +22 -0
  30. package/dist/cli/goal.d.ts.map +1 -0
  31. package/dist/cli/index.d.ts +47 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/instance-registry.d.ts +78 -0
  34. package/dist/cli/instance-registry.d.ts.map +1 -0
  35. package/dist/cli/observe.d.ts +35 -0
  36. package/dist/cli/observe.d.ts.map +1 -0
  37. package/dist/cli/vessel.d.ts +14 -0
  38. package/dist/cli/vessel.d.ts.map +1 -0
  39. package/dist/composition-observer.d.ts +96 -0
  40. package/dist/composition-observer.d.ts.map +1 -0
  41. package/dist/config.d.ts +36 -0
  42. package/dist/config.d.ts.map +1 -0
  43. package/dist/config.js +128 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/docker/Dockerfile +35 -0
  46. package/dist/environment.d.ts +72 -0
  47. package/dist/environment.d.ts.map +1 -0
  48. package/dist/environment.js +142 -0
  49. package/dist/environment.js.map +1 -0
  50. package/dist/goal-processor.d.ts +165 -0
  51. package/dist/goal-processor.d.ts.map +1 -0
  52. package/dist/helm/minibob-cluster/Chart.yaml +13 -0
  53. package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
  54. package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
  55. package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
  56. package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
  57. package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
  58. package/dist/helm/minibob-cluster/values-local.yaml +41 -0
  59. package/dist/helm/minibob-cluster/values-production.yaml +57 -0
  60. package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
  61. package/dist/helm/minibob-cluster/values.yaml +127 -0
  62. package/dist/improviser.d.ts +74 -0
  63. package/dist/improviser.d.ts.map +1 -0
  64. package/dist/impulse-filter.d.ts +74 -0
  65. package/dist/impulse-filter.d.ts.map +1 -0
  66. package/dist/impulse.d.ts +92 -0
  67. package/dist/impulse.d.ts.map +1 -0
  68. package/dist/impulse.js +234 -0
  69. package/dist/impulse.js.map +1 -0
  70. package/dist/lib.d.ts +29 -0
  71. package/dist/lib.d.ts.map +1 -0
  72. package/dist/lib.js +18561 -0
  73. package/dist/lib.js.map +98 -0
  74. package/dist/lifecycle-hooks.d.ts +99 -0
  75. package/dist/lifecycle-hooks.d.ts.map +1 -0
  76. package/dist/lifecycle-hooks.js +135 -0
  77. package/dist/lifecycle-hooks.js.map +1 -0
  78. package/dist/llm.d.ts +31 -0
  79. package/dist/llm.d.ts.map +1 -0
  80. package/dist/llm.js +349 -0
  81. package/dist/llm.js.map +1 -0
  82. package/dist/mcp-activity-bridge.d.ts +66 -0
  83. package/dist/mcp-activity-bridge.d.ts.map +1 -0
  84. package/dist/mcp-activity-bridge.js +126 -0
  85. package/dist/mcp-activity-bridge.js.map +1 -0
  86. package/dist/mcp.d.ts +216 -0
  87. package/dist/mcp.d.ts.map +1 -0
  88. package/dist/mcp.js +292 -0
  89. package/dist/mcp.js.map +1 -0
  90. package/dist/memory-agent.d.ts +92 -0
  91. package/dist/memory-agent.d.ts.map +1 -0
  92. package/dist/memory-agent.js +277 -0
  93. package/dist/memory-agent.js.map +1 -0
  94. package/dist/runtime-mapping.d.ts +97 -0
  95. package/dist/runtime-mapping.d.ts.map +1 -0
  96. package/dist/search-first-executor.d.ts +113 -0
  97. package/dist/search-first-executor.d.ts.map +1 -0
  98. package/dist/session.d.ts +48 -0
  99. package/dist/session.d.ts.map +1 -0
  100. package/dist/template-extractor.d.ts +9 -0
  101. package/dist/template-extractor.d.ts.map +1 -0
  102. package/dist/template-generator.d.ts +12 -0
  103. package/dist/template-generator.d.ts.map +1 -0
  104. package/dist/tools.d.ts +58 -0
  105. package/dist/tools.d.ts.map +1 -0
  106. package/dist/tools.js +771 -0
  107. package/dist/tools.js.map +1 -0
  108. package/dist/types.d.ts +503 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +8 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/understanding/analyzer.d.ts +55 -0
  113. package/dist/understanding/analyzer.d.ts.map +1 -0
  114. package/dist/understanding/explorer.d.ts +73 -0
  115. package/dist/understanding/explorer.d.ts.map +1 -0
  116. package/dist/understanding/index.d.ts +7 -0
  117. package/dist/understanding/index.d.ts.map +1 -0
  118. package/dist/understanding/types.d.ts +136 -0
  119. package/dist/understanding/types.d.ts.map +1 -0
  120. package/dist/validation.d.ts +29 -0
  121. package/dist/validation.d.ts.map +1 -0
  122. package/dist/validation.js +106 -0
  123. package/dist/validation.js.map +1 -0
  124. package/dist/vessel-bootstrap.d.ts +190 -0
  125. package/dist/vessel-bootstrap.d.ts.map +1 -0
  126. package/dist/vessel-registry.d.ts +229 -0
  127. package/dist/vessel-registry.d.ts.map +1 -0
  128. package/index.ts +1329 -0
  129. package/package.json +54 -0
  130. package/src/acp-gossip.ts +193 -0
  131. package/src/acp.ts +362 -0
  132. package/src/activity.ts +1464 -0
  133. package/src/agent-runtime.ts +365 -0
  134. package/src/boredom.ts +423 -0
  135. package/src/cli/acp-server.ts +377 -0
  136. package/src/cli/burrow.ts +896 -0
  137. package/src/cli/doctor.ts +526 -0
  138. package/src/cli/goal.ts +224 -0
  139. package/src/cli/index.ts +147 -0
  140. package/src/cli/instance-registry.ts +271 -0
  141. package/src/cli/observe.ts +682 -0
  142. package/src/cli/vessel.ts +287 -0
  143. package/src/components/SystemOverview.tsx +331 -0
  144. package/src/composition-observer.ts +449 -0
  145. package/src/config.ts +172 -0
  146. package/src/environment.ts +167 -0
  147. package/src/goal-processor.ts +654 -0
  148. package/src/improviser.ts +591 -0
  149. package/src/impulse-filter.ts +273 -0
  150. package/src/impulse.ts +311 -0
  151. package/src/lib.ts +147 -0
  152. package/src/lifecycle-hooks.ts +181 -0
  153. package/src/llm.ts +434 -0
  154. package/src/mcp-activity-bridge.ts +158 -0
  155. package/src/mcp.ts +747 -0
  156. package/src/memory-agent.ts +316 -0
  157. package/src/runtime-mapping.ts +527 -0
  158. package/src/search-first-executor.ts +666 -0
  159. package/src/session.ts +141 -0
  160. package/src/template-extractor.ts +256 -0
  161. package/src/template-generator.ts +130 -0
  162. package/src/tools.ts +924 -0
  163. package/src/types.ts +497 -0
  164. package/src/understanding/analyzer.ts +354 -0
  165. package/src/understanding/explorer.ts +488 -0
  166. package/src/understanding/index.ts +27 -0
  167. package/src/understanding/types.ts +153 -0
  168. package/src/validation.ts +125 -0
  169. package/src/vessel-bootstrap.ts +440 -0
  170. package/src/vessel-registry.ts +621 -0
  171. package/templates/core/edit-file.json +85 -0
  172. package/templates/understanding/diagnose-problem.json +32 -0
  173. package/templates/understanding/explore-codebase-v2.json +57 -0
  174. package/templates/understanding/explore-codebase.json +37 -0
package/src/mcp.ts ADDED
@@ -0,0 +1,747 @@
1
+ /**
2
+ * minibob MCP Client
3
+ *
4
+ * Integrates with Metabob backend for:
5
+ * - Activity template fetching
6
+ * - Execution metrics reporting
7
+ * - Impulse storage/retrieval
8
+ * - Learning and optimization
9
+ */
10
+
11
+ import type { ActivityTemplate, ActivityExecution, Impulse } from "./types"
12
+
13
+ // Impulse Relevance Metric (Phase 1.8)
14
+ export interface ImpulseRelevanceMetric {
15
+ impulse_id: string
16
+ activity_variant_id: string
17
+ task_id?: string
18
+ times_loaded: number
19
+ times_execution_succeeded: number
20
+ times_execution_failed: number
21
+ times_not_loaded_succeeded: number
22
+ times_not_loaded_failed: number
23
+ relevance_score: number
24
+ irrelevance_score: number
25
+ avg_content_size_tokens: number
26
+ typical_pointer_type?: string
27
+ created_at: string
28
+ updated_at: string
29
+ }
30
+
31
+ // =============================================================================
32
+ // MCP CLIENT CONFIGURATION
33
+ // =============================================================================
34
+
35
+ export interface MCPConfig {
36
+ endpoint: string
37
+ apiKey?: string
38
+ timeout?: number
39
+ }
40
+
41
+ // =============================================================================
42
+ // MCP CLIENT
43
+ // =============================================================================
44
+
45
+ export class MCPClient {
46
+ private endpoint: string
47
+ private apiKey?: string
48
+ private timeout: number
49
+
50
+ constructor(config: MCPConfig) {
51
+ this.endpoint = config.endpoint.replace(/\/$/, "") // Remove trailing slash
52
+ this.apiKey = config.apiKey
53
+ this.timeout = config.timeout ?? 30000
54
+ }
55
+
56
+ /**
57
+ * Fetch activity template by ID
58
+ */
59
+ async getActivityTemplate(templateId: string): Promise<ActivityTemplate | null> {
60
+ try {
61
+ const response = await this.request("GET", `/v2/activities/templates/${templateId}`)
62
+
63
+ if (!response.ok) {
64
+ if (response.status === 404) return null
65
+ throw new Error(`Failed to fetch template: ${response.status} ${response.statusText}`)
66
+ }
67
+
68
+ const data = await response.json() as any
69
+ const raw = data.template ?? data
70
+
71
+ // Transform API schema to minibob schema
72
+ return {
73
+ id: raw.variant_id || raw.id,
74
+ name: raw.variant_name || raw.name,
75
+ description: raw.description,
76
+ category: raw.category,
77
+ tasks: raw.task_steps || raw.tasks || [],
78
+ variables: raw.variables || [],
79
+ contextRequirements: raw.contextRequirements
80
+ } as ActivityTemplate
81
+ } catch (error) {
82
+ console.error(`[MCP] Error fetching template ${templateId}:`, error)
83
+ return null
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Search activity templates
89
+ */
90
+ async searchActivityTemplates(query?: {
91
+ category?: string
92
+ limit?: number
93
+ }): Promise<Array<{ id: string; name: string; category: string; successRate?: number }>> {
94
+ try {
95
+ const params = new URLSearchParams()
96
+ if (query?.category) params.set("category", query.category)
97
+ if (query?.limit) params.set("limit", String(query.limit))
98
+
99
+ const url = `/v2/activities/templates?${params.toString()}`
100
+ const response = await this.request("GET", url)
101
+
102
+ if (!response.ok) {
103
+ throw new Error(`Failed to search templates: ${response.status}`)
104
+ }
105
+
106
+ const data = await response.json() as { templates?: Array<{ id: string; name: string; category: string; successRate?: number }> }
107
+ return data.templates ?? []
108
+ } catch (error) {
109
+ console.error("[MCP] Error searching templates:", error)
110
+ return []
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Recommend activities for a goal using Thompson Sampling
116
+ *
117
+ * This is the key method for goal-driven execution.
118
+ * The backend uses Thompson Sampling with historical execution data
119
+ * to recommend the best activities for achieving a goal.
120
+ *
121
+ * @param taskDescription - User's goal description
122
+ * @param category - Optional category filter
123
+ * @param loadedImpulses - IDs of currently loaded impulses for context
124
+ * @param limit - Max number of recommendations
125
+ * @returns Array of activity recommendations with selection metadata
126
+ */
127
+ async recommendActivities(
128
+ taskDescription: string,
129
+ category?: string,
130
+ loadedImpulses?: string[],
131
+ limit: number = 3
132
+ ): Promise<Array<{ template_id: string; selection_metadata: any }>> {
133
+ try {
134
+ // Call backend recommendation API using Thompson Sampling
135
+ const payload = {
136
+ task_description: taskDescription,
137
+ category,
138
+ loaded_impulses: loadedImpulses || [],
139
+ limit,
140
+ }
141
+
142
+ const response = await this.request("POST", "/v2/activities/recommend", payload)
143
+
144
+ if (!response.ok) {
145
+ console.warn(`[MCP] Recommendation failed: ${response.status}`)
146
+ return []
147
+ }
148
+
149
+ const data = await response.json() as { recommendations?: Array<{ template_id: string; selection_metadata: any }> }
150
+ return data.recommendations ?? []
151
+ } catch (error) {
152
+ console.error("[MCP] Error getting recommendations:", error)
153
+ return []
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Register activity template variant
159
+ * Called when executing a template loaded from local JSON file
160
+ */
161
+ async registerTemplate(template: ActivityTemplate): Promise<boolean> {
162
+ try {
163
+ // Transform tasks to match backend schema (add required fields)
164
+ const task_steps = template.tasks.map(task => ({
165
+ id: task.id,
166
+ subagent: "general-purpose", // Default subagent
167
+ description: task.description,
168
+ dependencies: [], // No dependencies by default
169
+ prompt: task.prompt,
170
+ validation: task.validation,
171
+ retry: task.retry
172
+ }))
173
+
174
+ const payload: Record<string, any> = {
175
+ variant_id: template.id,
176
+ activity_id: template.id, // For now, variant_id === activity_id
177
+ variant_name: template.name,
178
+ description: template.description,
179
+ category: template.category,
180
+ task_steps,
181
+ scope: "global",
182
+ }
183
+
184
+ // Omit null/undefined fields (SurrealDB doesn't accept null, needs NONE or omitted)
185
+ Object.keys(payload).forEach(key => {
186
+ if (payload[key] === null || payload[key] === undefined) {
187
+ delete payload[key]
188
+ }
189
+ })
190
+
191
+ const response = await this.request("POST", "/v2/activities/templates", payload)
192
+
193
+ if (!response.ok) {
194
+ // 409 Conflict means template already exists - that's fine
195
+ if (response.status === 409) {
196
+ console.log(`[MCP] Template ${template.id} already registered`)
197
+ return true
198
+ }
199
+
200
+ const errorText = await response.text()
201
+ console.warn(`[MCP] Failed to register template: ${response.status} - ${errorText}`)
202
+ return false
203
+ }
204
+
205
+ console.log(`[MCP] ✓ Template ${template.id} registered successfully`)
206
+ return true
207
+ } catch (error) {
208
+ console.error("[MCP] Error registering template:", error)
209
+ return false
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Report activity execution results
215
+ */
216
+ async reportExecution(execution: ActivityExecution): Promise<boolean> {
217
+ try {
218
+ // Determine first failed task if execution failed
219
+ const failedTask = execution.taskResults.find((t) => t.status === "failed")
220
+
221
+ const payload: Record<string, any> = {
222
+ variant_id: execution.templateId,
223
+ success: execution.status === "completed",
224
+ duration_ms: execution.metrics?.duration || 0,
225
+ cost: execution.metrics?.cost || 0,
226
+ tokens: {
227
+ input: execution.metrics?.totalTokens?.input || 0,
228
+ output: execution.metrics?.totalTokens?.output || 0,
229
+ cache: 0, // MiniBob doesn't track cache tokens yet
230
+ },
231
+ }
232
+
233
+ // Only include optional fields if they have values (SurrealDB doesn't accept null)
234
+ if (failedTask?.error) {
235
+ payload.error_message = failedTask.error
236
+ }
237
+ if (failedTask) {
238
+ payload.error_type = "task_execution_error"
239
+ payload.failed_task_id = failedTask.taskId
240
+ }
241
+ // Omit empty arrays - SurrealDB will use default [] if field allows it
242
+ if (execution.impulses && execution.impulses.length > 0) {
243
+ payload.impulses_used = execution.impulses.map(imp => imp.id)
244
+ }
245
+
246
+ const response = await this.request("POST", "/v2/activities/executions", payload)
247
+
248
+ if (!response.ok) {
249
+ const errorText = await response.text()
250
+ console.warn(`[MCP] Failed to report execution: ${response.status} - ${errorText}`)
251
+ return false
252
+ }
253
+
254
+ return true
255
+ } catch (error) {
256
+ console.error("[MCP] Error reporting execution:", error)
257
+ return false
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Record activity composition event (when one activity calls another)
263
+ *
264
+ * This enables learning of composition patterns:
265
+ * - Which activities typically work together
266
+ * - Success rates of different compositions
267
+ * - Optimal activity sequences
268
+ */
269
+ async recordComposition(params: {
270
+ parentActivityId: string
271
+ childActivityId: string
272
+ executionId: string
273
+ goalContext?: string
274
+ success: boolean
275
+ }): Promise<boolean> {
276
+ try {
277
+ const payload = {
278
+ parent_activity_id: params.parentActivityId,
279
+ child_activity_id: params.childActivityId,
280
+ execution_id: params.executionId,
281
+ goal_context: params.goalContext,
282
+ success: params.success,
283
+ }
284
+
285
+ const response = await this.request("POST", "/v2/activities/composition", payload)
286
+
287
+ if (!response.ok) {
288
+ const errorText = await response.text()
289
+ console.warn(`[MCP] Failed to record composition: ${response.status} - ${errorText}`)
290
+ return false
291
+ }
292
+
293
+ const data = await response.json() as { success: boolean; edge?: any }
294
+ if (data.edge) {
295
+ console.log(`[MCP] Composition recorded: ${params.parentActivityId} → ${params.childActivityId} (weight: ${data.edge.weight})`)
296
+ }
297
+
298
+ return true
299
+ } catch (error) {
300
+ console.error("[MCP] Error recording composition:", error)
301
+ return false
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Record tool usage during activity execution
307
+ *
308
+ * This enables learning of tool usage patterns:
309
+ * - Which tools are required vs optional for each activity
310
+ * - Success correlation between tool usage and outcomes
311
+ * - Pre-flight checks (does vessel have required tools?)
312
+ */
313
+ async recordToolUsage(params: {
314
+ toolName: string
315
+ activityVariantId: string
316
+ taskId?: string
317
+ executionId: string
318
+ toolSucceeded: boolean
319
+ activitySucceeded: boolean
320
+ paramsComplexity?: number
321
+ }): Promise<boolean> {
322
+ try {
323
+ const payload = {
324
+ tool_name: params.toolName,
325
+ activity_variant_id: params.activityVariantId,
326
+ task_id: params.taskId,
327
+ execution_id: params.executionId,
328
+ tool_succeeded: params.toolSucceeded,
329
+ activity_succeeded: params.activitySucceeded,
330
+ params_complexity: params.paramsComplexity,
331
+ }
332
+
333
+ const response = await this.request("POST", "/v2/activities/tool-usage", payload)
334
+
335
+ if (!response.ok) {
336
+ const errorText = await response.text()
337
+ console.warn(`[MCP] Failed to record tool usage: ${response.status} - ${errorText}`)
338
+ return false
339
+ }
340
+
341
+ console.log(`[MCP] Tool usage recorded: ${params.toolName} in ${params.activityVariantId}`)
342
+ return true
343
+ } catch (error) {
344
+ console.error("[MCP] Error recording tool usage:", error)
345
+ return false
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Record execution sequence for session-level learning
351
+ *
352
+ * This enables learning:
353
+ * - Which activities typically run together
354
+ * - Successful sequences for achieving goals
355
+ * - Optimal sequence length and patterns
356
+ */
357
+ async recordExecutionSequence(params: {
358
+ sessionId: string
359
+ goalContext?: string
360
+ sequence: Array<{
361
+ activityId: string
362
+ executionId: string
363
+ order: number
364
+ triggerType: 'goal' | 'nested' | 'boredom' | 'manual'
365
+ parentExecutionId?: string
366
+ success: boolean
367
+ durationMs: number
368
+ costUsd: number
369
+ }>
370
+ outcome: 'success' | 'partial' | 'failure'
371
+ }): Promise<boolean> {
372
+ try {
373
+ const payload = {
374
+ session_id: params.sessionId,
375
+ goal_context: params.goalContext,
376
+ sequence: params.sequence.map(item => ({
377
+ activity_id: item.activityId,
378
+ execution_id: item.executionId,
379
+ order: item.order,
380
+ trigger_type: item.triggerType,
381
+ parent_execution_id: item.parentExecutionId,
382
+ success: item.success,
383
+ duration_ms: item.durationMs,
384
+ cost_usd: item.costUsd,
385
+ })),
386
+ outcome: params.outcome,
387
+ }
388
+
389
+ const response = await this.request("POST", "/v2/activities/execution-sequences", payload)
390
+
391
+ if (!response.ok) {
392
+ const errorText = await response.text()
393
+ console.warn(`[MCP] Failed to record execution sequence: ${response.status} - ${errorText}`)
394
+ return false
395
+ }
396
+
397
+ console.log(`[MCP] Execution sequence recorded: ${params.sequence.length} activities (${params.outcome})`)
398
+ return true
399
+ } catch (error) {
400
+ console.error("[MCP] Error recording execution sequence:", error)
401
+ return false
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Store impulse in backend
407
+ */
408
+ async storeImpulse(impulse: Impulse): Promise<boolean> {
409
+ try {
410
+ // Map MiniBob impulse format to API schema
411
+ const payload = {
412
+ impulse_id: impulse.id,
413
+ project_id: "minibob-default", // Default project for internal impulses
414
+ impulse_data: {
415
+ id: impulse.id, // Required by ImpulseDataSchema
416
+ type: impulse.pointer?.type ?? "memo",
417
+ pointer: impulse.pointer ?? { type: "memo", content: impulse.content ?? "" },
418
+ budget: impulse.budget ?? 4000,
419
+ priority: typeof impulse.priority === "string" ?
420
+ ({ critical: 1, high: 2, medium: 3, low: 4 }[impulse.priority] ?? 3) :
421
+ (impulse.priority ?? 3),
422
+ metadata: {
423
+ tags: impulse.tags,
424
+ content: impulse.content,
425
+ },
426
+ },
427
+ }
428
+
429
+ const response = await this.request("POST", "/v2/impulses", payload)
430
+
431
+ if (!response.ok) {
432
+ console.warn(`[MCP] Failed to store impulse: ${response.status}`)
433
+ return false
434
+ }
435
+
436
+ return true
437
+ } catch (error) {
438
+ console.error("[MCP] Error storing impulse:", error)
439
+ return false
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Retrieve impulse from backend
445
+ */
446
+ async retrieveImpulse(impulseId: string): Promise<Impulse | null> {
447
+ try {
448
+ const response = await this.request("GET", `/v2/impulses/${impulseId}`)
449
+
450
+ if (!response.ok) {
451
+ if (response.status === 404) return null
452
+ throw new Error(`Failed to retrieve impulse: ${response.status}`)
453
+ }
454
+
455
+ const data = await response.json() as { impulse?: Impulse }
456
+ return data.impulse ?? null
457
+ } catch (error) {
458
+ console.error(`[MCP] Error retrieving impulse ${impulseId}:`, error)
459
+ return null
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Track impulse usage for learning
465
+ */
466
+ async trackImpulseUsage(impulseId: string, context: {
467
+ activityId: string
468
+ taskId: string
469
+ tokensUsed: number
470
+ }): Promise<boolean> {
471
+ try {
472
+ const response = await this.request("POST", `/v2/impulses/${impulseId}/usage`, context)
473
+
474
+ if (!response.ok) {
475
+ console.warn(`[MCP] Failed to track impulse usage: ${response.status}`)
476
+ return false
477
+ }
478
+
479
+ return true
480
+ } catch (error) {
481
+ console.error("[MCP] Error tracking impulse usage:", error)
482
+ return false
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Query impulse relevance metrics for an activity (Phase 1.8)
488
+ */
489
+ async queryImpulseRelevance(params: {
490
+ activityVariantId: string
491
+ impulseIds?: string[]
492
+ }): Promise<ImpulseRelevanceMetric[]> {
493
+ try {
494
+ const queryParams = new URLSearchParams({
495
+ activity_variant_id: params.activityVariantId,
496
+ })
497
+
498
+ if (params.impulseIds && params.impulseIds.length > 0) {
499
+ for (const id of params.impulseIds) {
500
+ queryParams.append('impulse_id', id)
501
+ }
502
+ }
503
+
504
+ const response = await this.request(
505
+ "GET",
506
+ `/v2/activities/impulse-relevance?${queryParams.toString()}`
507
+ )
508
+
509
+ if (!response.ok) {
510
+ if (response.status === 404) return []
511
+ throw new Error(`Failed to query impulse relevance: ${response.status}`)
512
+ }
513
+
514
+ const data = await response.json() as any
515
+ return data.metrics || []
516
+ } catch (error) {
517
+ console.error("[MCP] Error querying impulse relevance:", error)
518
+ return []
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Record impulse relevance for learning (Phase 1.8)
524
+ */
525
+ async recordImpulseRelevance(params: {
526
+ impulseId: string
527
+ activityVariantId: string
528
+ taskId?: string
529
+ wasLoaded: boolean
530
+ executionSucceeded: boolean
531
+ contentSizeTokens?: number
532
+ pointerType?: string
533
+ }): Promise<boolean> {
534
+ try {
535
+ const response = await this.request("POST", "/v2/activities/impulse-relevance", {
536
+ impulse_id: params.impulseId,
537
+ activity_variant_id: params.activityVariantId,
538
+ task_id: params.taskId,
539
+ was_loaded: params.wasLoaded,
540
+ execution_succeeded: params.executionSucceeded,
541
+ content_size_tokens: params.contentSizeTokens,
542
+ pointer_type: params.pointerType,
543
+ })
544
+
545
+ if (!response.ok) {
546
+ console.warn(`[MCP] Failed to record impulse relevance: ${response.status}`)
547
+ return false
548
+ }
549
+
550
+ return true
551
+ } catch (error) {
552
+ console.error("[MCP] Error recording impulse relevance:", error)
553
+ return false
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Resolve an impulse pointer via backend (Phase 1.8+)
559
+ *
560
+ * Architecture: MiniBob delegates all non-local impulse resolution to backend.
561
+ * This allows backend to introduce new pointer types without minibob changes.
562
+ */
563
+ async resolveImpulse(pointer: any): Promise<string> {
564
+ try {
565
+ const response = await this.request("POST", "/v2/impulses/resolve", {
566
+ pointer: pointer,
567
+ })
568
+
569
+ if (!response.ok) {
570
+ throw new Error(`Failed to resolve impulse: ${response.status} ${response.statusText}`)
571
+ }
572
+
573
+ const data = await response.json() as { content?: string }
574
+ if (!data.content) {
575
+ throw new Error("Backend returned no content for impulse resolution")
576
+ }
577
+
578
+ return data.content
579
+ } catch (error) {
580
+ throw new Error(
581
+ `MCP impulse resolution failed: ${error instanceof Error ? error.message : String(error)}`
582
+ )
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Store execution trace in backend (Phase 1.8+)
588
+ *
589
+ * Enables execution traces to be referenced as impulses in future activities.
590
+ */
591
+ async storeExecutionTrace(execution: ActivityExecution): Promise<boolean> {
592
+ try {
593
+ // Map MiniBob's status to backend's expected values
594
+ let status: 'success' | 'failure' | 'partial' = 'failure'
595
+ if (execution.status === 'completed') {
596
+ status = 'success'
597
+ } else if (execution.status === 'failed') {
598
+ status = 'failure'
599
+ } else if (execution.status === 'executing') {
600
+ status = 'partial'
601
+ }
602
+
603
+ const response = await this.request("POST", "/v2/activities/execution-traces", {
604
+ execution_id: execution.id,
605
+ template_id: execution.templateId,
606
+ activity_id: execution.templateId, // Backend expects activity_id
607
+ status: status, // Map to backend's enum values
608
+ success: execution.status === "completed",
609
+ duration_ms: execution.metrics?.duration || 0,
610
+ cost_usd: execution.metrics?.cost || 0,
611
+ tokens: {
612
+ input: execution.metrics?.totalTokens?.input || 0,
613
+ output: execution.metrics?.totalTokens?.output || 0,
614
+ cache: 0, // MiniBob doesn't track cache tokens yet
615
+ },
616
+ // Include execution trace with all collected data
617
+ execution_trace: execution.executionTrace || {
618
+ tasks: [],
619
+ impulsesCreated: [],
620
+ filesModified: [],
621
+ },
622
+ // Include impulse IDs used
623
+ impulses_used: execution.impulses?.map(i => i.id) || [],
624
+ })
625
+
626
+ if (!response.ok) {
627
+ const errorText = await response.text()
628
+ console.warn(`[MCP] Failed to store execution trace: ${response.status} - ${errorText}`)
629
+ return false
630
+ }
631
+
632
+ console.log(`[MCP] ✓ Execution trace stored: ${execution.id}`)
633
+ return true
634
+ } catch (error) {
635
+ console.error("[MCP] Error storing execution trace:", error)
636
+ return false
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Register minibob vessel with backend
642
+ */
643
+ async registerVessel(manifest: {
644
+ id: string
645
+ name: string
646
+ version: string
647
+ capabilities: string[]
648
+ endpoint: string
649
+ }): Promise<boolean> {
650
+ try {
651
+ const response = await this.request("POST", "/v2/vessels/register", manifest)
652
+
653
+ if (!response.ok) {
654
+ console.warn(`[MCP] Failed to register vessel: ${response.status}`)
655
+ return false
656
+ }
657
+
658
+ console.log("[MCP] Vessel registered successfully")
659
+ return true
660
+ } catch (error) {
661
+ console.error("[MCP] Error registering vessel:", error)
662
+ return false
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Make HTTP request to MCP backend
668
+ */
669
+ private async request(
670
+ method: "GET" | "POST" | "PUT" | "DELETE",
671
+ path: string,
672
+ body?: unknown
673
+ ): Promise<Response> {
674
+ const url = `${this.endpoint}${path}`
675
+ const headers: Record<string, string> = {
676
+ "Content-Type": "application/json",
677
+ }
678
+
679
+ if (this.apiKey) {
680
+ headers["Authorization"] = `Bearer ${this.apiKey}`
681
+ // Also send internal service key for impulse endpoints
682
+ headers["X-Internal-Api-Key"] = this.apiKey
683
+ }
684
+
685
+ const controller = new AbortController()
686
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
687
+
688
+ try {
689
+ const response = await fetch(url, {
690
+ method,
691
+ headers,
692
+ body: body ? JSON.stringify(body) : undefined,
693
+ signal: controller.signal,
694
+ })
695
+
696
+ return response
697
+ } finally {
698
+ clearTimeout(timeoutId)
699
+ }
700
+ }
701
+ }
702
+
703
+ // =============================================================================
704
+ // SINGLETON MCP CLIENT
705
+ // =============================================================================
706
+
707
+ let mcpClient: MCPClient | null = null
708
+
709
+ /**
710
+ * Initialize MCP client with backend health check (singleton)
711
+ *
712
+ * Checks backend availability before initialization.
713
+ * Returns null if backend is unreachable, allowing graceful fallback to local mode.
714
+ *
715
+ * @param config MCP configuration
716
+ * @param skipHealthCheck Skip health check (for testing)
717
+ * @returns MCPClient instance or null if backend unavailable
718
+ */
719
+ export async function initializeMCP(config: MCPConfig, skipHealthCheck = false): Promise<MCPClient | null> {
720
+ if (!skipHealthCheck) {
721
+ const { checkBackendHealth } = await import("./environment")
722
+ const healthy = await checkBackendHealth(config.endpoint)
723
+
724
+ if (!healthy) {
725
+ console.warn("[MCP] Backend unavailable, using local mode")
726
+ return null
727
+ }
728
+ }
729
+
730
+ mcpClient = new MCPClient(config)
731
+ console.log("[MCP] ✓ Client initialized")
732
+ return mcpClient
733
+ }
734
+
735
+ /**
736
+ * Get MCP client instance
737
+ */
738
+ export function getMCPClient(): MCPClient | null {
739
+ return mcpClient
740
+ }
741
+
742
+ /**
743
+ * Check if MCP is enabled
744
+ */
745
+ export function isMCPEnabled(): boolean {
746
+ return mcpClient !== null
747
+ }