@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
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Goal Processor - Core orchestrator for goal-driven activity execution
3
+ *
4
+ * This module transforms user goals into activity executions:
5
+ * 1. Receive user goal message
6
+ * 2. Get activity recommendations from metabob-activity-api (Thompson Sampling)
7
+ * 3. Execute recommended activity
8
+ * 4. Check goal completion
9
+ * 5. Loop until goal is complete or max attempts reached
10
+ *
11
+ * Architecture:
12
+ * User goal → GoalProcessor → Backend recommendations → ActivityExecutor → Goal complete
13
+ *
14
+ * Key principle: Backend (metabob-activity-api) handles activity recommendation
15
+ * based on historical execution data, success rates, and goal alignment.
16
+ */
17
+
18
+ import type { ActivityExecution, Impulse } from "./types"
19
+ import { ActivityExecutor, loadTemplateFromMCPOrLocal } from "./activity"
20
+ import { getMCPClient, isMCPEnabled } from "./mcp"
21
+ import { GoalImproviser } from "./improviser"
22
+
23
+ // =============================================================================
24
+ // TYPES
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Structured goal representation
29
+ */
30
+ export interface Goal {
31
+ /** Original user message */
32
+ message: string
33
+ /** Goal type inferred from message */
34
+ type: "feature" | "bugfix" | "refactor" | "exploration" | "other"
35
+ /** Parsed intent (what user wants to accomplish) */
36
+ intent: string
37
+ /** Context from user (files, variables, etc.) */
38
+ context: Record<string, unknown>
39
+ /** Created timestamp */
40
+ createdAt: number
41
+ }
42
+
43
+ /**
44
+ * Activity recommendation from backend
45
+ */
46
+ export interface ActivityRecommendation {
47
+ /** Template ID to execute */
48
+ templateId: string
49
+ /** Selection metadata from backend (Thompson Sampling) */
50
+ selectionMetadata: {
51
+ method: string
52
+ alpha?: number
53
+ beta?: number
54
+ sample?: number
55
+ score?: number
56
+ }
57
+ /** Suggested variables for execution (derived from goal context) */
58
+ variables: Record<string, unknown>
59
+ }
60
+
61
+ /**
62
+ * Goal execution result
63
+ */
64
+ export interface GoalResult {
65
+ /** Goal that was executed */
66
+ goal: Goal
67
+ /** Activities executed during goal pursuit */
68
+ executions: ActivityExecution[]
69
+ /** Whether goal was completed */
70
+ completed: boolean
71
+ /** Completion reason */
72
+ completionReason: string
73
+ /** Total duration in milliseconds */
74
+ totalDuration: number
75
+ /** Total cost in dollars */
76
+ totalCost: number
77
+ /** Total tokens used */
78
+ totalTokens: {
79
+ input: number
80
+ output: number
81
+ }
82
+ }
83
+
84
+ // =============================================================================
85
+ // GOAL PROCESSOR
86
+ // =============================================================================
87
+
88
+ export class GoalProcessor {
89
+ private readonly workingDirectory: string
90
+ private readonly executor: ActivityExecutor
91
+
92
+ constructor(config: {
93
+ workingDirectory: string
94
+ executor: ActivityExecutor
95
+ }) {
96
+ this.workingDirectory = config.workingDirectory
97
+ this.executor = config.executor
98
+ }
99
+
100
+ /**
101
+ * Parse user goal message into structured Goal
102
+ *
103
+ * For now, simple parsing. Can be enhanced with LLM later.
104
+ */
105
+ parseGoal(message: string, context?: Record<string, unknown>): Goal {
106
+ // Simple type inference from keywords
107
+ let type: Goal["type"] = "other"
108
+ const lowerMessage = message.toLowerCase()
109
+
110
+ if (lowerMessage.includes("add") || lowerMessage.includes("create") || lowerMessage.includes("implement")) {
111
+ type = "feature"
112
+ } else if (lowerMessage.includes("fix") || lowerMessage.includes("bug") || lowerMessage.includes("error")) {
113
+ type = "bugfix"
114
+ } else if (lowerMessage.includes("refactor") || lowerMessage.includes("clean") || lowerMessage.includes("reorganize")) {
115
+ type = "refactor"
116
+ }
117
+
118
+ return {
119
+ message,
120
+ type,
121
+ intent: message, // For now, intent = message
122
+ context: context || {},
123
+ createdAt: Date.now(),
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get activity recommendations from backend API
129
+ *
130
+ * NOTE: This is a placeholder. In the real implementation, this will be called
131
+ * by opencode's MinibobIntegration which has access to MetabobCLI.recommendActivities().
132
+ *
133
+ * The backend recommendation engine:
134
+ * - Applies Thompson Sampling for exploration/exploitation
135
+ * - Considers historical execution success rates
136
+ * - Aligns recommendations with goal category
137
+ * - Uses impulse state for context-aware recommendations
138
+ *
139
+ * @param goal - Parsed goal
140
+ * @param loadedImpulseIds - IDs of currently loaded impulses
141
+ * @param limit - Max recommendations to return
142
+ * @returns Array of activity recommendations from backend
143
+ */
144
+ async getRecommendations(
145
+ goal: Goal,
146
+ loadedImpulseIds: string[] = [],
147
+ limit: number = 3
148
+ ): Promise<ActivityRecommendation[]> {
149
+ // Get recommendations from MCP backend using Thompson Sampling
150
+ const mcpClient = getMCPClient()
151
+
152
+ if (!mcpClient) {
153
+ console.warn("[GoalProcessor] No MCP client available - cannot get recommendations")
154
+ console.warn("[GoalProcessor] Set MINIBOB_MCP_ENDPOINT environment variable or call initializeMCP()")
155
+ return []
156
+ }
157
+
158
+ console.log(`[GoalProcessor] Requesting ${limit} recommendations from backend`)
159
+ console.log(`[GoalProcessor] Goal intent: ${goal.intent}`)
160
+ console.log(`[GoalProcessor] Goal type: ${goal.type}`)
161
+
162
+ try {
163
+ const recommendations = await mcpClient.recommendActivities(
164
+ goal.intent,
165
+ goal.type !== 'other' ? goal.type : undefined,
166
+ loadedImpulseIds,
167
+ limit
168
+ )
169
+
170
+ console.log(`[GoalProcessor] Received ${recommendations.length} recommendations from backend`)
171
+
172
+ // Transform backend format to ActivityRecommendation
173
+ return recommendations.map(rec => ({
174
+ templateId: rec.template_id,
175
+ selectionMetadata: rec.selection_metadata || { method: 'thompson_sampling' },
176
+ variables: goal.context || {} // Use goal context as variable suggestions
177
+ }))
178
+ } catch (err) {
179
+ console.error("[GoalProcessor] Failed to get recommendations:", err instanceof Error ? err.message : String(err))
180
+ return []
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Assess relevance of a recommendation to the goal
186
+ *
187
+ * Uses simple keyword matching and semantic similarity to determine if
188
+ * the recommended activity is likely to help achieve the goal.
189
+ *
190
+ * @param goal - The goal being pursued
191
+ * @param recommendation - The activity recommendation
192
+ * @returns Relevance score from 0.0 (not relevant) to 1.0 (highly relevant)
193
+ */
194
+ private assessRelevance(goal: Goal, recommendation: ActivityRecommendation): number {
195
+ const templateId = recommendation.templateId.toLowerCase()
196
+ const goalIntent = goal.intent.toLowerCase()
197
+ const goalType = goal.type.toLowerCase()
198
+
199
+ // Extract key terms from goal
200
+ const goalTerms = goalIntent.split(/\s+/).filter(term => term.length > 3)
201
+
202
+ // Check for type alignment
203
+ let score = 0.0
204
+
205
+ // Type match gives base score
206
+ if (templateId.includes(goalType) || templateId.includes(goal.type)) {
207
+ score += 0.3
208
+ }
209
+
210
+ // Keyword overlap
211
+ const matchedTerms = goalTerms.filter(term => templateId.includes(term))
212
+ if (matchedTerms.length > 0) {
213
+ score += 0.3 * (matchedTerms.length / Math.max(goalTerms.length, 1))
214
+ }
215
+
216
+ // Special patterns
217
+ if (goal.type === 'bugfix' && templateId.includes('fix')) score += 0.2
218
+ if (goal.type === 'bugfix' && templateId.includes('debug')) score += 0.2
219
+ if (goal.type === 'feature' && templateId.includes('add')) score += 0.2
220
+ if (goal.type === 'feature' && templateId.includes('implement')) score += 0.2
221
+ if (goal.type === 'refactor' && templateId.includes('refactor')) score += 0.2
222
+ if (goal.type === 'refactor' && templateId.includes('optimize')) score += 0.2
223
+
224
+ // Generic templates are less relevant (lower priority)
225
+ if (templateId.includes('generic') || templateId.includes('default')) {
226
+ score *= 0.7
227
+ }
228
+
229
+ return Math.min(score, 1.0)
230
+ }
231
+
232
+ /**
233
+ * Check if goal is complete based on executions
234
+ *
235
+ * With optional goal verification:
236
+ * - If last activity succeeded AND goal provided, verify achievement objectively
237
+ * - If last activity failed, goal is incomplete
238
+ * - Without goal, uses simple status check (backward compatibility)
239
+ *
240
+ * @param executions - Activity executions performed
241
+ * @param goal - Optional goal for objective verification
242
+ * @returns Completion status with reason
243
+ */
244
+ isGoalComplete(executions: ActivityExecution[], goal?: Goal): { complete: boolean; reason: string } {
245
+ if (executions.length === 0) {
246
+ return { complete: false, reason: "No activities executed yet" }
247
+ }
248
+
249
+ const lastExecution = executions[executions.length - 1]
250
+ if (!lastExecution) {
251
+ return { complete: false, reason: "No execution data available" }
252
+ }
253
+
254
+ if (lastExecution.status === "failed") {
255
+ return { complete: false, reason: "Last activity failed" }
256
+ }
257
+
258
+ if (lastExecution.status === "completed") {
259
+ // With goal provided, verify achievement objectively
260
+ if (goal) {
261
+ const verification = this.verifyGoalAchievement(goal, executions)
262
+ if (!verification.verified) {
263
+ console.warn("[GoalProcessor] Activity succeeded but goal not achieved:", verification.reason)
264
+ return { complete: false, reason: `Activity succeeded but goal not achieved: ${verification.reason}` }
265
+ }
266
+ return { complete: true, reason: verification.reason }
267
+ }
268
+ // Backward compatibility: without goal, trust activity status
269
+ return { complete: true, reason: "Activity completed successfully" }
270
+ }
271
+
272
+ return { complete: false, reason: "Activity still in progress" }
273
+ }
274
+
275
+ /**
276
+ * Verify goal was actually achieved using objective criteria
277
+ * Prevents LLM hallucination where activity claims success but did nothing
278
+ *
279
+ * @param goal - The goal being evaluated
280
+ * @param executions - Activity executions performed
281
+ * @returns Verification result with reason
282
+ */
283
+ private verifyGoalAchievement(goal: Goal, executions: ActivityExecution[]): { verified: boolean; reason: string } {
284
+ // File modification goals
285
+ if (goal.intent.match(/change|modify|edit|update|replace/i)) {
286
+ const filesModified = executions.reduce((sum, exec) =>
287
+ sum + (exec.executionTrace?.filesModified.length || 0), 0
288
+ )
289
+ if (filesModified === 0) {
290
+ return { verified: false, reason: "No files were modified" }
291
+ }
292
+ return { verified: true, reason: `Verified: ${filesModified} file(s) modified` }
293
+ }
294
+
295
+ // Test execution goals
296
+ if (goal.intent.match(/test/i)) {
297
+ const hasTestOutput = executions.some(exec =>
298
+ exec.taskResults?.some(tr => tr.output?.toLowerCase().includes('test'))
299
+ )
300
+ if (!hasTestOutput) {
301
+ return { verified: false, reason: "No test execution detected" }
302
+ }
303
+ return { verified: true, reason: "Verified: Tests executed" }
304
+ }
305
+
306
+ // Code creation goals
307
+ if (goal.intent.match(/create|add|implement|write/i)) {
308
+ const filesModified = executions.reduce((sum, exec) =>
309
+ sum + (exec.executionTrace?.filesModified.length || 0), 0
310
+ )
311
+ if (filesModified === 0) {
312
+ return { verified: false, reason: "No files created or modified" }
313
+ }
314
+ return { verified: true, reason: `Verified: ${filesModified} file(s) modified` }
315
+ }
316
+
317
+ // Analysis/exploration goals
318
+ if (goal.intent.match(/analyze|explore|find|search/i)) {
319
+ const toolsUsed = executions.reduce((sum, exec) => {
320
+ const taskToolCalls = exec.taskResults?.reduce((taskSum, tr) =>
321
+ taskSum + (tr.metadata?.toolCalls?.length || 0), 0
322
+ ) || 0
323
+ return sum + taskToolCalls
324
+ }, 0)
325
+ if (toolsUsed === 0) {
326
+ return { verified: false, reason: "No tools were used" }
327
+ }
328
+ return { verified: true, reason: `Verified: ${toolsUsed} tool call(s)` }
329
+ }
330
+
331
+ // Default: require measurable work
332
+ const filesModified = executions.reduce((sum, exec) =>
333
+ sum + (exec.executionTrace?.filesModified.length || 0), 0
334
+ )
335
+ const toolsUsed = executions.reduce((sum, exec) => {
336
+ const taskToolCalls = exec.taskResults?.reduce((taskSum, tr) =>
337
+ taskSum + (tr.metadata?.toolCalls?.length || 0), 0
338
+ ) || 0
339
+ return sum + taskToolCalls
340
+ }, 0)
341
+
342
+ if (filesModified === 0 && toolsUsed === 0) {
343
+ console.warn("[GoalProcessor] No verifiable work detected for goal:", goal.intent)
344
+ return { verified: false, reason: "No files modified and no tools used" }
345
+ }
346
+
347
+ return { verified: true, reason: "Activity completed with measurable outcomes" }
348
+ }
349
+
350
+ /**
351
+ * Create impulses from execution output for context propagation
352
+ */
353
+ private createImpulsesFromExecution(execution: ActivityExecution): Impulse[] {
354
+ const impulses: Impulse[] = []
355
+ const timestamp = Date.now()
356
+
357
+ // Create impulse for overall execution trace
358
+ impulses.push({
359
+ id: `impulse:trace:${execution.id}`,
360
+ pointer: {
361
+ type: 'activityExecutionTrace',
362
+ executionId: execution.id,
363
+ },
364
+ budget: 4000, // Generous budget for execution context
365
+ priority: 'high',
366
+ loaded: false,
367
+ createdAt: timestamp,
368
+ tags: ['execution-output', execution.templateId],
369
+ })
370
+
371
+ // Create impulses for individual task outputs (if they have significant output)
372
+ execution.taskResults?.forEach((taskResult, index) => {
373
+ if (taskResult.output && taskResult.output.length > 100) {
374
+ impulses.push({
375
+ id: `impulse:output:${execution.id}:${taskResult.taskId}`,
376
+ pointer: {
377
+ type: 'activityOutput',
378
+ activityId: execution.id,
379
+ taskId: taskResult.taskId,
380
+ },
381
+ budget: 2000,
382
+ priority: 'medium',
383
+ loaded: false,
384
+ createdAt: timestamp,
385
+ tags: ['task-output', execution.templateId],
386
+ })
387
+ }
388
+ })
389
+
390
+ // Create impulses for modified files
391
+ execution.executionTrace?.filesModified?.forEach((filePath, index) => {
392
+ // Only create file impulses for the first few files to avoid context bloat
393
+ if (index < 3) {
394
+ impulses.push({
395
+ id: `impulse:file:${execution.id}:${index}`,
396
+ pointer: {
397
+ type: 'file',
398
+ path: filePath,
399
+ },
400
+ budget: 3000,
401
+ priority: 'medium',
402
+ loaded: false,
403
+ createdAt: timestamp,
404
+ tags: ['modified-file', filePath],
405
+ })
406
+ }
407
+ })
408
+
409
+ return impulses
410
+ }
411
+
412
+ /**
413
+ * Execute goal-seeking process
414
+ *
415
+ * Main loop:
416
+ * 1. Parse goal
417
+ * 2. Get recommendations from backend (Thompson Sampling)
418
+ * 3. Execute top-ranked activity with context from previous steps
419
+ * 4. Create impulses from execution output
420
+ * 5. Check completion
421
+ * 6. Repeat until complete or max attempts
422
+ *
423
+ * @param message - User goal message
424
+ * @param context - Additional context (files, variables, etc.)
425
+ * @param options - Execution options
426
+ * @returns Goal execution result
427
+ */
428
+ async executeGoal(
429
+ message: string,
430
+ context?: Record<string, unknown>,
431
+ options?: {
432
+ maxActivities?: number
433
+ maxCost?: number
434
+ }
435
+ ): Promise<GoalResult> {
436
+ const maxActivities = options?.maxActivities || 5
437
+ const maxCost = options?.maxCost || 10.0
438
+
439
+ const startTime = Date.now()
440
+ const executions: ActivityExecution[] = []
441
+ const accumulatedImpulses: Impulse[] = [] // Accumulate context across iterations
442
+ let totalCost = 0
443
+ let totalTokens = { input: 0, output: 0 }
444
+
445
+ // Parse goal
446
+ const goal = this.parseGoal(message, context)
447
+
448
+ console.log(`[GoalProcessor] Starting goal execution: ${goal.intent}`)
449
+ console.log(`[GoalProcessor] Goal type: ${goal.type}`)
450
+
451
+ // Execute activities until goal complete or limits reached
452
+ for (let i = 0; i < maxActivities; i++) {
453
+ console.log(`[GoalProcessor] Activity iteration ${i + 1}/${maxActivities}`)
454
+ console.log(`[GoalProcessor] Context: ${accumulatedImpulses.length} impulses from previous executions`)
455
+
456
+ // Get impulse IDs for context-aware recommendations
457
+ const impulseIds = accumulatedImpulses.map(imp => imp.id)
458
+
459
+ // Get recommendations from backend (with context from previous executions)
460
+ const recommendations = await this.getRecommendations(goal, impulseIds, 3)
461
+
462
+ if (recommendations.length === 0) {
463
+ console.warn("[GoalProcessor] No recommendations from backend, stopping")
464
+ break
465
+ }
466
+
467
+ console.log(`[GoalProcessor] Received ${recommendations.length} recommendations from backend`)
468
+
469
+ // Execute top recommendation (backend already ranked them)
470
+ const topRecommendation = recommendations[0]
471
+ if (!topRecommendation) {
472
+ console.warn("[GoalProcessor] No valid recommendation, stopping")
473
+ break
474
+ }
475
+
476
+ // Assess relevance of top recommendation
477
+ const relevanceScore = this.assessRelevance(goal, topRecommendation)
478
+ const RELEVANCE_THRESHOLD = 0.3 // Require at least 30% relevance
479
+
480
+ console.log(`[GoalProcessor] Top recommendation: ${topRecommendation.templateId}`)
481
+ console.log(`[GoalProcessor] Relevance score: ${relevanceScore.toFixed(2)}`)
482
+
483
+ let execution: ActivityExecution
484
+
485
+ if (relevanceScore < RELEVANCE_THRESHOLD) {
486
+ console.log(`[GoalProcessor] Relevance too low (${relevanceScore.toFixed(2)} < ${RELEVANCE_THRESHOLD})`)
487
+ console.log(`[GoalProcessor] Falling back to improvisation`)
488
+
489
+ // Improvise instead of using irrelevant template
490
+ const improviser = new GoalImproviser({
491
+ workingDirectory: this.workingDirectory,
492
+ })
493
+
494
+ const improvResult = await improviser.improvise(goal.intent, {
495
+ maxSteps: 10,
496
+ })
497
+
498
+ // Convert improvisation result to ActivityExecution
499
+ // The improviser already stores the trace via MCP
500
+ execution = {
501
+ id: improvResult.execution_id,
502
+ templateId: `improvised-${goal.type}`,
503
+ status: improvResult.outcome.status === 'success' ? 'completed' : 'failed',
504
+ variables: {},
505
+ impulses: [],
506
+ taskResults: improvResult.steps.map(step => ({
507
+ taskId: `step-${step.step}`,
508
+ status: step.result.success ? 'completed' : 'failed',
509
+ output: step.result.output,
510
+ error: step.result.error,
511
+ })),
512
+ startedAt: Date.parse(improvResult.started_at),
513
+ completedAt: improvResult.completed_at ? Date.parse(improvResult.completed_at) : undefined,
514
+ error: improvResult.outcome.error,
515
+ executionTrace: {
516
+ tasks: improvResult.steps.map(step => ({
517
+ id: `step-${step.step}`,
518
+ description: step.thought,
519
+ actualPrompt: step.thought,
520
+ toolCalls: [{
521
+ id: `tool:${step.action}:step-${step.step}:${Date.now()}`,
522
+ name: step.action,
523
+ arguments: step.params,
524
+ result: {
525
+ success: step.result.success,
526
+ output: step.result.output,
527
+ error: step.result.error,
528
+ },
529
+ }],
530
+ response: step.result.output || step.result.error || '',
531
+ result: {
532
+ status: step.result.success ? 'success' : 'failure',
533
+ error: step.result.error,
534
+ },
535
+ inputState: {
536
+ filesAvailable: [],
537
+ environment: {},
538
+ impulses: [],
539
+ variables: {},
540
+ },
541
+ outputState: {
542
+ filesModified: [
543
+ ...(improvResult.outcome.files_modified || []),
544
+ ...(improvResult.outcome.files_created || []),
545
+ ],
546
+ filesCreated: improvResult.outcome.files_created || [],
547
+ filesDeleted: improvResult.outcome.files_deleted || [],
548
+ },
549
+ })),
550
+ filesModified: [
551
+ ...(improvResult.outcome.files_modified || []),
552
+ ...(improvResult.outcome.files_created || []),
553
+ ],
554
+ impulsesCreated: [],
555
+ },
556
+ metrics: {
557
+ duration: 0, // Improviser doesn't track per-step duration
558
+ cost: 0, // Will be calculated from tokens
559
+ totalTokens: {
560
+ input: 0,
561
+ output: 0,
562
+ },
563
+ },
564
+ }
565
+
566
+ console.log(`[GoalProcessor] Improvisation ${execution.status}: ${execution.id}`)
567
+ console.log(`[GoalProcessor] Note: Ribosome will extract template from successful improvisation`)
568
+ } else {
569
+ console.log(`[GoalProcessor] Relevance acceptable (${relevanceScore.toFixed(2)} >= ${RELEVANCE_THRESHOLD})`)
570
+ console.log(`[GoalProcessor] Executing template: ${topRecommendation.templateId}`)
571
+
572
+ // Load template
573
+ const template = await loadTemplateFromMCPOrLocal(topRecommendation.templateId)
574
+ if (!template) {
575
+ console.warn(`[GoalProcessor] Template not found: ${topRecommendation.templateId}, trying next`)
576
+ continue
577
+ }
578
+
579
+ // Execute activity with template and accumulated context
580
+ execution = await this.executor.execute({
581
+ template,
582
+ variables: topRecommendation.variables,
583
+ impulses: accumulatedImpulses, // Pass context from previous executions
584
+ reason: `Goal: ${goal.intent}`,
585
+ })
586
+ }
587
+
588
+ executions.push(execution)
589
+
590
+ console.log(`[GoalProcessor] Activity ${execution.id} completed with status: ${execution.status}`)
591
+
592
+ // Create impulses from execution output for next iteration
593
+ const newImpulses = this.createImpulsesFromExecution(execution)
594
+ accumulatedImpulses.push(...newImpulses)
595
+ console.log(`[GoalProcessor] Created ${newImpulses.length} impulses from execution output`)
596
+
597
+ // Track costs
598
+ if (execution.metrics) {
599
+ totalCost += execution.metrics.cost
600
+ totalTokens.input += execution.metrics.totalTokens.input
601
+ totalTokens.output += execution.metrics.totalTokens.output
602
+ // Note: minibob metrics doesn't have cache field yet
603
+ }
604
+
605
+ console.log(`[GoalProcessor] Total cost so far: $${totalCost.toFixed(4)}`)
606
+
607
+ // Check cost limit
608
+ if (totalCost > maxCost) {
609
+ console.warn(`[GoalProcessor] Cost limit exceeded ($${maxCost})`)
610
+ return {
611
+ goal,
612
+ executions,
613
+ completed: false,
614
+ completionReason: `Cost limit exceeded ($${maxCost})`,
615
+ totalDuration: Date.now() - startTime,
616
+ totalCost,
617
+ totalTokens,
618
+ }
619
+ }
620
+
621
+ // Check goal completion
622
+ const { complete, reason } = this.isGoalComplete(executions, goal)
623
+
624
+ console.log(`[GoalProcessor] Goal complete check: ${complete} - ${reason}`)
625
+
626
+ if (complete) {
627
+ return {
628
+ goal,
629
+ executions,
630
+ completed: true,
631
+ completionReason: reason,
632
+ totalDuration: Date.now() - startTime,
633
+ totalCost,
634
+ totalTokens,
635
+ }
636
+ }
637
+ }
638
+
639
+ // Max activities reached
640
+ const { complete, reason } = this.isGoalComplete(executions, goal)
641
+
642
+ console.log(`[GoalProcessor] Reached max activities (${maxActivities})`)
643
+
644
+ return {
645
+ goal,
646
+ executions,
647
+ completed: complete,
648
+ completionReason: complete ? reason : `Max activities (${maxActivities}) reached`,
649
+ totalDuration: Date.now() - startTime,
650
+ totalCost,
651
+ totalTokens,
652
+ }
653
+ }
654
+ }