@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,591 @@
1
+ /**
2
+ * Goal Improviser - Execute goals without templates
3
+ *
4
+ * Pure improvisation: LLM figures out what to do step by step,
5
+ * using available tools. Everything is recorded for template extraction.
6
+ */
7
+
8
+ import type { LLMClient } from './llm'
9
+ import type { ToolHandler, ToolResult, Message, ActivityExecution, TaskResult } from './types'
10
+ import { createLLMClient } from './llm'
11
+ import { createToolHandlers } from './tools'
12
+
13
+ // ============================================================================
14
+ // TYPES
15
+ // ============================================================================
16
+
17
+ export interface ImprovisationStep {
18
+ step: number
19
+ thought: string // LLM's reasoning about what to do
20
+ action: string // Tool name to use
21
+ params: Record<string, unknown>
22
+ result: ToolResult
23
+ duration_ms: number
24
+ timestamp: string
25
+ cost_estimate: number // Tokens used this step
26
+ }
27
+
28
+ export interface ImprovisationTrace {
29
+ execution_id: string
30
+ goal: string
31
+ improvisation: true
32
+ context?: Record<string, unknown>
33
+ started_at: string
34
+ completed_at?: string
35
+ steps: ImprovisationStep[]
36
+ outcome: {
37
+ status: 'success' | 'failure' | 'stuck'
38
+ goal_achieved: boolean
39
+ total_duration_ms: number
40
+ total_cost: number
41
+ total_tokens: {
42
+ input: number
43
+ output: number
44
+ }
45
+ files_modified: string[]
46
+ files_created: string[]
47
+ files_deleted: string[]
48
+ error?: string
49
+ }
50
+ }
51
+
52
+ export interface ImprovisationConfig {
53
+ maxSteps?: number // Max steps before giving up (default: 50)
54
+ temperature?: number // LLM temperature for creativity (default: 0.7)
55
+ stuckThreshold?: number // Same action repeated N times = stuck (default: 3)
56
+ saveTrace?: boolean // Save to backend (default: true)
57
+ }
58
+
59
+ // ============================================================================
60
+ // IMPROVISER
61
+ // ============================================================================
62
+
63
+ export interface ImproviserConfig {
64
+ provider?: string
65
+ apiKey?: string
66
+ model?: string
67
+ workingDirectory: string
68
+ }
69
+
70
+ export class GoalImproviser {
71
+ private llm: LLMClient
72
+ private tools: Record<string, ToolHandler>
73
+ private config: ImproviserConfig
74
+
75
+ constructor(config: ImproviserConfig) {
76
+ this.config = config
77
+ const provider = (config.provider || 'anthropic') as 'anthropic' | 'openai'
78
+ const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || ''
79
+ this.llm = createLLMClient(provider, apiKey)
80
+ this.tools = createToolHandlers({
81
+ workingDirectory: config.workingDirectory
82
+ })
83
+ }
84
+
85
+ /**
86
+ * Improvise solution to a goal without pre-defined template
87
+ */
88
+ async improvise(
89
+ goal: string,
90
+ config: ImprovisationConfig = {}
91
+ ): Promise<ImprovisationTrace> {
92
+
93
+ const {
94
+ maxSteps = 50,
95
+ temperature = 0.7,
96
+ stuckThreshold = 3,
97
+ saveTrace = true
98
+ } = config
99
+
100
+ console.log(`[Improviser] Starting improvisation for goal: ${goal} (max ${maxSteps} steps)`)
101
+
102
+ // Initialize trace
103
+ const trace: ImprovisationTrace = {
104
+ execution_id: `exec_improv_${Date.now()}_${this.randomId()}`,
105
+ goal,
106
+ improvisation: true,
107
+ started_at: new Date().toISOString(),
108
+ steps: [],
109
+ outcome: {
110
+ status: 'success',
111
+ goal_achieved: false,
112
+ total_duration_ms: 0,
113
+ total_cost: 0,
114
+ total_tokens: { input: 0, output: 0 },
115
+ files_modified: [],
116
+ files_created: [],
117
+ files_deleted: []
118
+ }
119
+ }
120
+
121
+ const startTime = Date.now()
122
+ const messages: Message[] = []
123
+
124
+ // System prompt for improvisation
125
+ messages.push({
126
+ role: 'system',
127
+ content: this.buildSystemPrompt(goal)
128
+ })
129
+
130
+ // Initial user message
131
+ messages.push({
132
+ role: 'user',
133
+ content: 'Start working toward the goal. What should you do first?'
134
+ })
135
+
136
+ let stepNumber = 0
137
+ let goalAchieved = false
138
+ const maxRetriesPerStep = 3
139
+
140
+ // Improvise step by step
141
+ while (!goalAchieved && stepNumber < maxSteps) {
142
+ stepNumber++
143
+ const stepStartTime = Date.now()
144
+ let retryCount = 0
145
+ let decision: any = null
146
+ let lastResponse: any = null
147
+
148
+ // Retry loop for JSON parse errors
149
+ while (retryCount <= maxRetriesPerStep && !decision) {
150
+ try {
151
+ // Get LLM decision
152
+ console.log(`[Improviser] Step ${stepNumber}: Requesting LLM decision${retryCount > 0 ? ` (retry ${retryCount}/${maxRetriesPerStep})` : ''}`)
153
+
154
+ lastResponse = await this.llm.complete({
155
+ model: this.config.model || 'claude-sonnet-4-20250514',
156
+ messages,
157
+ temperature,
158
+ maxTokens: 2000
159
+ })
160
+
161
+ // Parse decision (LLM should output structured JSON)
162
+ try {
163
+ decision = this.parseDecision(lastResponse.content)
164
+ // Successfully parsed - will break the while loop
165
+ } catch (parseError) {
166
+ // If JSON parse error and we have retries left, provide detailed feedback
167
+ if (parseError instanceof SyntaxError && retryCount < maxRetriesPerStep) {
168
+ const errorDetails = this.formatJSONParseError(parseError, lastResponse.content)
169
+ console.warn(`[Improviser] JSON parse error on step ${stepNumber}, retry ${retryCount + 1}/${maxRetriesPerStep}`)
170
+ console.warn(`[Improviser] Error: ${errorDetails}`)
171
+
172
+ // Add error feedback to messages and retry
173
+ messages.push({
174
+ role: 'assistant',
175
+ content: lastResponse.content
176
+ })
177
+ messages.push({
178
+ role: 'user',
179
+ content: `Your JSON output has a syntax error. Please fix it and try again.
180
+
181
+ ${errorDetails}
182
+
183
+ Remember the correct format:
184
+ {
185
+ "thought": "your reasoning",
186
+ "action": "tool_name",
187
+ "params": { ... },
188
+ "goal_achieved": true/false
189
+ }
190
+
191
+ Please output the corrected JSON now.`
192
+ })
193
+
194
+ retryCount++
195
+ continue // Retry the LLM call
196
+ }
197
+
198
+ // Out of retries or different error type
199
+ throw parseError
200
+ }
201
+ } catch (error) {
202
+ // If we've exhausted retries, throw the error up
203
+ if (retryCount >= maxRetriesPerStep) {
204
+ throw error
205
+ }
206
+ }
207
+ }
208
+
209
+ // At this point we have a valid decision
210
+ if (!decision) {
211
+ throw new Error('Failed to get valid decision after retries')
212
+ }
213
+
214
+ try {
215
+
216
+ console.log(`[Improviser] Step ${stepNumber}: ${decision.action} - ${decision.thought}`)
217
+
218
+ // Execute the action
219
+ const toolHandler = this.tools[decision.action]
220
+ if (!toolHandler) {
221
+ throw new Error(`Unknown tool: ${decision.action}`)
222
+ }
223
+
224
+ const toolResult = await toolHandler(decision.params)
225
+
226
+ // Record step
227
+ const step: ImprovisationStep = {
228
+ step: stepNumber,
229
+ thought: decision.thought,
230
+ action: decision.action,
231
+ params: decision.params,
232
+ result: toolResult,
233
+ duration_ms: Date.now() - stepStartTime,
234
+ timestamp: new Date().toISOString(),
235
+ cost_estimate: this.estimateCost(lastResponse)
236
+ }
237
+ trace.steps.push(step)
238
+
239
+ // Update conversation history
240
+ messages.push({
241
+ role: 'assistant',
242
+ content: JSON.stringify({
243
+ thought: decision.thought,
244
+ action: decision.action,
245
+ params: decision.params
246
+ })
247
+ })
248
+
249
+ messages.push({
250
+ role: 'user',
251
+ content: this.formatToolResult(toolResult, decision.action)
252
+ })
253
+
254
+ // Check if goal achieved
255
+ goalAchieved = decision.goal_achieved || false
256
+
257
+ if (goalAchieved) {
258
+ console.log(`[Improviser] Goal achieved at step ${stepNumber}!`)
259
+ break
260
+ }
261
+
262
+ // Check if stuck
263
+ if (this.isStuck(trace.steps, stuckThreshold)) {
264
+ console.warn(`[Improviser] Appears stuck - repeated action: ${trace.steps.slice(-1)[0]?.action}`)
265
+ trace.outcome.status = 'stuck'
266
+ trace.outcome.error = 'Repeated same action too many times'
267
+ break
268
+ }
269
+
270
+ } catch (error) {
271
+ console.error(`[Improviser] Step ${stepNumber} failed:`, error instanceof Error ? error.message : String(error))
272
+
273
+ // Add error to conversation so LLM can recover
274
+ messages.push({
275
+ role: 'user',
276
+ content: `Error occurred: ${error instanceof Error ? error.message : String(error)}\nHow can you recover?`
277
+ })
278
+ }
279
+ }
280
+
281
+ // Finalize trace
282
+ trace.completed_at = new Date().toISOString()
283
+ trace.outcome.total_duration_ms = Date.now() - startTime
284
+ trace.outcome.goal_achieved = goalAchieved
285
+ trace.outcome.status = goalAchieved ? 'success' : (trace.outcome.status || 'failure')
286
+
287
+ // Extract file changes
288
+ trace.outcome.files_modified = this.extractFilesModified(trace.steps)
289
+ trace.outcome.files_created = this.extractFilesCreated(trace.steps)
290
+ trace.outcome.files_deleted = this.extractFilesDeleted(trace.steps)
291
+
292
+ // Calculate totals
293
+ trace.outcome.total_cost = trace.steps.reduce((sum, s) => sum + s.cost_estimate, 0)
294
+
295
+ console.log(`[Improviser] Complete: ${trace.execution_id} - ${trace.outcome.goal_achieved ? 'SUCCESS' : 'FAILED'} (${trace.steps.length} steps, ${trace.outcome.total_duration_ms}ms, $${trace.outcome.total_cost.toFixed(4)})`)
296
+
297
+ // Save trace if configured
298
+ if (saveTrace) {
299
+ await this.saveTrace(trace)
300
+ }
301
+
302
+ return trace
303
+ }
304
+
305
+ // ==========================================================================
306
+ // HELPER METHODS
307
+ // ==========================================================================
308
+
309
+ private buildSystemPrompt(goal: string): string {
310
+ return `You are MiniBob, an autonomous agent that achieves goals through improvisation.
311
+
312
+ GOAL: ${goal}
313
+
314
+ You have access to these tools:
315
+ - bash: Execute shell commands
316
+ - read: Read file contents
317
+ - write: Create new files
318
+ - edit: Modify existing files
319
+ - git: Git operations
320
+
321
+ Your approach:
322
+ 1. Think about what to do next (reasoning)
323
+ 2. Choose a tool and parameters
324
+ 3. Execute the action
325
+ 4. Observe the result
326
+ 5. Decide if goal is achieved or continue
327
+
328
+ IMPORTANT OUTPUT FORMAT:
329
+ After reasoning, output ONLY valid JSON in this format:
330
+ {
331
+ "thought": "your reasoning about what to do and why",
332
+ "action": "tool_name",
333
+ "params": { "param1": "value1", ... },
334
+ "goal_achieved": true/false
335
+ }
336
+
337
+ Start by understanding the current state, then work systematically toward the goal.
338
+ Take concrete actions - don't just plan, actually do things.`
339
+ }
340
+
341
+ private formatJSONParseError(error: SyntaxError, content: string): string {
342
+ const errorMessage = error.message
343
+
344
+ // Extract JSON from content
345
+ const jsonMatch = content.match(/\{[\s\S]*\}/)
346
+ if (!jsonMatch) {
347
+ return `No JSON object found in output. Make sure to output a valid JSON object.`
348
+ }
349
+
350
+ const jsonStr = jsonMatch[0]
351
+
352
+ // Try to extract position information from error message
353
+ const positionMatch = errorMessage.match(/position (\d+)/)
354
+ if (positionMatch && positionMatch[1]) {
355
+ const position = parseInt(positionMatch[1], 10)
356
+ const before = jsonStr.substring(Math.max(0, position - 50), position)
357
+ const after = jsonStr.substring(position, Math.min(jsonStr.length, position + 50))
358
+
359
+ return `JSON Syntax Error: ${errorMessage}
360
+
361
+ Context around error position ${position}:
362
+ ...${before}⚠️HERE⚠️${after}...
363
+
364
+ The JSON you generated:
365
+ ${jsonStr.substring(0, 500)}${jsonStr.length > 500 ? '...' : ''}
366
+
367
+ Common issues:
368
+ - Unterminated strings (missing closing quote)
369
+ - Unescaped quotes inside strings (use \\" instead of ")
370
+ - Unescaped newlines in strings (use \\n)
371
+ - Missing commas between fields
372
+ - Trailing commas before closing braces`
373
+ }
374
+
375
+ // If no position info, provide general feedback
376
+ return `JSON Syntax Error: ${errorMessage}
377
+
378
+ The JSON you generated:
379
+ ${jsonStr.substring(0, 500)}${jsonStr.length > 500 ? '...' : ''}
380
+
381
+ Common issues:
382
+ - Unterminated strings (missing closing quote)
383
+ - Unescaped quotes inside strings (use \\" instead of ")
384
+ - Unescaped newlines in strings (use \\n)
385
+ - Missing commas between fields
386
+ - Trailing commas before closing braces
387
+ - Make sure all strings are properly quoted`
388
+ }
389
+
390
+ private parseDecision(content: string): {
391
+ thought: string
392
+ action: string
393
+ params: Record<string, unknown>
394
+ goal_achieved: boolean
395
+ } {
396
+ // Extract JSON from content (may have markdown formatting)
397
+ const jsonMatch = content.match(/\{[\s\S]*\}/)
398
+ if (!jsonMatch) {
399
+ throw new Error('LLM did not return valid JSON')
400
+ }
401
+
402
+ const decision = JSON.parse(jsonMatch[0])
403
+
404
+ if (!decision.thought || !decision.action) {
405
+ throw new Error('Decision missing required fields')
406
+ }
407
+
408
+ return {
409
+ thought: decision.thought,
410
+ action: decision.action,
411
+ params: decision.params || {},
412
+ goal_achieved: decision.goal_achieved || false
413
+ }
414
+ }
415
+
416
+ private formatToolResult(result: ToolResult, action: string): string {
417
+ return `Tool ${action} result:
418
+ Success: ${result.success}
419
+ ${result.output ? `Output:\n${result.output}` : ''}
420
+ ${result.error ? `Error:\n${result.error}` : ''}
421
+
422
+ What should you do next?`
423
+ }
424
+
425
+ private isStuck(steps: ImprovisationStep[], threshold: number): boolean {
426
+ if (steps.length < threshold) return false
427
+
428
+ const lastN = steps.slice(-threshold)
429
+ const actions = lastN.map(s => s.action)
430
+
431
+ // Stuck if all last N actions are the same
432
+ return actions.every(a => a === actions[0])
433
+ }
434
+
435
+ private extractFilesModified(steps: ImprovisationStep[]): string[] {
436
+ return [...new Set(
437
+ steps
438
+ .filter(s => s.action === 'edit' && s.result.success)
439
+ .map(s => s.params.path as string)
440
+ .filter(Boolean)
441
+ )]
442
+ }
443
+
444
+ private extractFilesCreated(steps: ImprovisationStep[]): string[] {
445
+ return [...new Set(
446
+ steps
447
+ .filter(s => s.action === 'write' && s.result.success)
448
+ .map(s => s.params.path as string)
449
+ .filter(Boolean)
450
+ )]
451
+ }
452
+
453
+ private extractFilesDeleted(steps: ImprovisationStep[]): string[] {
454
+ return [...new Set(
455
+ steps
456
+ .filter(s => s.action === 'bash' && s.params.command?.toString().includes('rm '))
457
+ .map(s => {
458
+ // Extract file from rm command
459
+ const cmd = s.params.command as string
460
+ const match = cmd.match(/rm\s+(.+)/)
461
+ return match ? match[1] : null
462
+ })
463
+ .filter(Boolean) as string[]
464
+ )]
465
+ }
466
+
467
+ private estimateCost(response: any): number {
468
+ // Rough estimate: $0.003 per 1K input tokens, $0.015 per 1K output tokens
469
+ const inputTokens = response.usage?.input_tokens || 0
470
+ const outputTokens = response.usage?.output_tokens || 0
471
+
472
+ return (inputTokens / 1000) * 0.003 + (outputTokens / 1000) * 0.015
473
+ }
474
+
475
+ private async saveTrace(trace: ImprovisationTrace): Promise<void> {
476
+ // Convert ImprovisationTrace to ActivityExecution format
477
+ // This unifies all execution modes (template, goal-seeking, improvisation)
478
+ // to use the same backend storage and learning mechanisms
479
+ try {
480
+ const { getMCPClient, isMCPEnabled } = await import('./mcp')
481
+ if (!isMCPEnabled()) return
482
+
483
+ const mcp = getMCPClient()
484
+ if (!mcp) return
485
+
486
+ // Generate templateId from goal (enables learning which goals work)
487
+ // Format: "improvised-{sanitized-goal}-{hash}"
488
+ const goalSlug = trace.goal
489
+ .toLowerCase()
490
+ .replace(/[^a-z0-9]+/g, '-')
491
+ .substring(0, 50)
492
+ const templateId = `improvised-${goalSlug}`
493
+
494
+ // Convert to ActivityExecution format
495
+ const activityExecution = {
496
+ id: trace.execution_id,
497
+ templateId: templateId,
498
+ status: trace.outcome.status === 'success' ? 'completed' as const : 'failed' as const,
499
+ variables: trace.context || {},
500
+ impulses: [], // Improvisation starts with no impulses
501
+ taskResults: trace.steps.map((step, idx) => ({
502
+ taskId: `step-${step.step}`,
503
+ status: step.result.success ? 'completed' as const : 'failed' as const,
504
+ output: step.result.output,
505
+ error: step.result.error,
506
+ duration: step.duration_ms,
507
+ })),
508
+ startedAt: Date.parse(trace.started_at),
509
+ completedAt: trace.completed_at ? Date.parse(trace.completed_at) : undefined,
510
+ error: trace.outcome.error,
511
+ executionTrace: {
512
+ tasks: trace.steps.map(step => ({
513
+ id: `step-${step.step}`,
514
+ description: step.thought,
515
+ actualPrompt: step.thought, // The thought is the prompt in improvisation
516
+ toolCalls: [{
517
+ id: `tool:${step.action}:step-${step.step}:${Date.now()}`,
518
+ name: step.action,
519
+ arguments: step.params,
520
+ result: {
521
+ success: step.result.success,
522
+ output: step.result.output,
523
+ error: step.result.error,
524
+ },
525
+ }],
526
+ response: step.result.output || step.result.error || '',
527
+ result: {
528
+ status: step.result.success ? 'success' as const : 'failure' as const,
529
+ error: step.result.error,
530
+ },
531
+ inputState: {
532
+ filesAvailable: [],
533
+ environment: {},
534
+ impulses: [],
535
+ variables: {},
536
+ },
537
+ outputState: {
538
+ filesModified: trace.outcome.files_modified,
539
+ filesCreated: trace.outcome.files_created,
540
+ filesDeleted: trace.outcome.files_deleted,
541
+ },
542
+ stateTransition: {
543
+ before: {},
544
+ after: {},
545
+ workingDirectory: process.cwd(),
546
+ },
547
+ impulsesUsed: [],
548
+ impulsesCreated: [],
549
+ llmInteractions: [{
550
+ messages: [
551
+ {
552
+ role: 'user' as const,
553
+ content: step.thought,
554
+ },
555
+ ],
556
+ response: {
557
+ content: JSON.stringify({ action: step.action, params: step.params }),
558
+ toolCalls: [],
559
+ },
560
+ tokens: { input: 0, output: 0 },
561
+ cost: 0,
562
+ duration: step.duration_ms,
563
+ }],
564
+ duration: step.duration_ms,
565
+ })),
566
+ impulsesCreated: [],
567
+ filesModified: trace.outcome.files_modified,
568
+ goalContext: {
569
+ goal: trace.goal,
570
+ intent: 'improvisation',
571
+ context: trace.context || {},
572
+ },
573
+ },
574
+ metrics: {
575
+ duration: trace.outcome.total_duration_ms,
576
+ cost: trace.outcome.total_cost,
577
+ totalTokens: trace.outcome.total_tokens,
578
+ },
579
+ }
580
+
581
+ await mcp.storeExecutionTrace(activityExecution)
582
+ console.log(`[Improviser] Trace saved to backend: ${trace.execution_id} (template: ${templateId})`)
583
+ } catch (error) {
584
+ console.error('[Improviser] Failed to save trace:', error)
585
+ }
586
+ }
587
+
588
+ private randomId(): string {
589
+ return Math.random().toString(36).substring(7)
590
+ }
591
+ }