@metabob/minibob 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +255 -0
- package/CHANGELOG.md +112 -0
- package/README.md +380 -0
- package/bin/minibob.js +36 -0
- package/dist/acp-gossip.d.ts +72 -0
- package/dist/acp-gossip.d.ts.map +1 -0
- package/dist/acp-gossip.js +156 -0
- package/dist/acp-gossip.js.map +1 -0
- package/dist/acp.d.ts +62 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +292 -0
- package/dist/acp.js.map +1 -0
- package/dist/activity.d.ts +157 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +518 -0
- package/dist/activity.js.map +1 -0
- package/dist/agent-runtime.d.ts +104 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/boredom.d.ts +125 -0
- package/dist/boredom.d.ts.map +1 -0
- package/dist/boredom.js +244 -0
- package/dist/boredom.js.map +1 -0
- package/dist/cli/acp-server.d.ts +23 -0
- package/dist/cli/acp-server.d.ts.map +1 -0
- package/dist/cli/burrow.d.ts +26 -0
- package/dist/cli/burrow.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/goal.d.ts +22 -0
- package/dist/cli/goal.d.ts.map +1 -0
- package/dist/cli/index.d.ts +47 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/instance-registry.d.ts +78 -0
- package/dist/cli/instance-registry.d.ts.map +1 -0
- package/dist/cli/observe.d.ts +35 -0
- package/dist/cli/observe.d.ts.map +1 -0
- package/dist/cli/vessel.d.ts +14 -0
- package/dist/cli/vessel.d.ts.map +1 -0
- package/dist/composition-observer.d.ts +96 -0
- package/dist/composition-observer.d.ts.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +128 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/Dockerfile +35 -0
- package/dist/environment.d.ts +72 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +142 -0
- package/dist/environment.js.map +1 -0
- package/dist/goal-processor.d.ts +165 -0
- package/dist/goal-processor.d.ts.map +1 -0
- package/dist/helm/minibob-cluster/Chart.yaml +13 -0
- package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
- package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
- package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
- package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
- package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
- package/dist/helm/minibob-cluster/values-local.yaml +41 -0
- package/dist/helm/minibob-cluster/values-production.yaml +57 -0
- package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
- package/dist/helm/minibob-cluster/values.yaml +127 -0
- package/dist/improviser.d.ts +74 -0
- package/dist/improviser.d.ts.map +1 -0
- package/dist/impulse-filter.d.ts +74 -0
- package/dist/impulse-filter.d.ts.map +1 -0
- package/dist/impulse.d.ts +92 -0
- package/dist/impulse.d.ts.map +1 -0
- package/dist/impulse.js +234 -0
- package/dist/impulse.js.map +1 -0
- package/dist/lib.d.ts +29 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +18561 -0
- package/dist/lib.js.map +98 -0
- package/dist/lifecycle-hooks.d.ts +99 -0
- package/dist/lifecycle-hooks.d.ts.map +1 -0
- package/dist/lifecycle-hooks.js +135 -0
- package/dist/lifecycle-hooks.js.map +1 -0
- package/dist/llm.d.ts +31 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +349 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp-activity-bridge.d.ts +66 -0
- package/dist/mcp-activity-bridge.d.ts.map +1 -0
- package/dist/mcp-activity-bridge.js +126 -0
- package/dist/mcp-activity-bridge.js.map +1 -0
- package/dist/mcp.d.ts +216 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +292 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory-agent.d.ts +92 -0
- package/dist/memory-agent.d.ts.map +1 -0
- package/dist/memory-agent.js +277 -0
- package/dist/memory-agent.js.map +1 -0
- package/dist/runtime-mapping.d.ts +97 -0
- package/dist/runtime-mapping.d.ts.map +1 -0
- package/dist/search-first-executor.d.ts +113 -0
- package/dist/search-first-executor.d.ts.map +1 -0
- package/dist/session.d.ts +48 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/template-extractor.d.ts +9 -0
- package/dist/template-extractor.d.ts.map +1 -0
- package/dist/template-generator.d.ts +12 -0
- package/dist/template-generator.d.ts.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +771 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +503 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/understanding/analyzer.d.ts +55 -0
- package/dist/understanding/analyzer.d.ts.map +1 -0
- package/dist/understanding/explorer.d.ts +73 -0
- package/dist/understanding/explorer.d.ts.map +1 -0
- package/dist/understanding/index.d.ts +7 -0
- package/dist/understanding/index.d.ts.map +1 -0
- package/dist/understanding/types.d.ts +136 -0
- package/dist/understanding/types.d.ts.map +1 -0
- package/dist/validation.d.ts +29 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +106 -0
- package/dist/validation.js.map +1 -0
- package/dist/vessel-bootstrap.d.ts +190 -0
- package/dist/vessel-bootstrap.d.ts.map +1 -0
- package/dist/vessel-registry.d.ts +229 -0
- package/dist/vessel-registry.d.ts.map +1 -0
- package/index.ts +1329 -0
- package/package.json +54 -0
- package/src/acp-gossip.ts +193 -0
- package/src/acp.ts +362 -0
- package/src/activity.ts +1464 -0
- package/src/agent-runtime.ts +365 -0
- package/src/boredom.ts +423 -0
- package/src/cli/acp-server.ts +377 -0
- package/src/cli/burrow.ts +896 -0
- package/src/cli/doctor.ts +526 -0
- package/src/cli/goal.ts +224 -0
- package/src/cli/index.ts +147 -0
- package/src/cli/instance-registry.ts +271 -0
- package/src/cli/observe.ts +682 -0
- package/src/cli/vessel.ts +287 -0
- package/src/components/SystemOverview.tsx +331 -0
- package/src/composition-observer.ts +449 -0
- package/src/config.ts +172 -0
- package/src/environment.ts +167 -0
- package/src/goal-processor.ts +654 -0
- package/src/improviser.ts +591 -0
- package/src/impulse-filter.ts +273 -0
- package/src/impulse.ts +311 -0
- package/src/lib.ts +147 -0
- package/src/lifecycle-hooks.ts +181 -0
- package/src/llm.ts +434 -0
- package/src/mcp-activity-bridge.ts +158 -0
- package/src/mcp.ts +747 -0
- package/src/memory-agent.ts +316 -0
- package/src/runtime-mapping.ts +527 -0
- package/src/search-first-executor.ts +666 -0
- package/src/session.ts +141 -0
- package/src/template-extractor.ts +256 -0
- package/src/template-generator.ts +130 -0
- package/src/tools.ts +924 -0
- package/src/types.ts +497 -0
- package/src/understanding/analyzer.ts +354 -0
- package/src/understanding/explorer.ts +488 -0
- package/src/understanding/index.ts +27 -0
- package/src/understanding/types.ts +153 -0
- package/src/validation.ts +125 -0
- package/src/vessel-bootstrap.ts +440 -0
- package/src/vessel-registry.ts +621 -0
- package/templates/core/edit-file.json +85 -0
- package/templates/understanding/diagnose-problem.json +32 -0
- package/templates/understanding/explore-codebase-v2.json +57 -0
- package/templates/understanding/explore-codebase.json +37 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition Observer - Track and visualize activity composition
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time observation of which activities call which,
|
|
5
|
+
* detects cycles, tracks performance, and generates reports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExecutorConfig } from './activity'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface CompositionEvent {
|
|
15
|
+
timestamp: number
|
|
16
|
+
parent: string
|
|
17
|
+
child: string
|
|
18
|
+
depth: number
|
|
19
|
+
reason?: string
|
|
20
|
+
status?: 'started' | 'completed' | 'failed' | 'blocked'
|
|
21
|
+
duration?: number
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ActivityMetrics {
|
|
26
|
+
activityId: string
|
|
27
|
+
calls: number
|
|
28
|
+
successes: number
|
|
29
|
+
failures: number
|
|
30
|
+
blocked: number
|
|
31
|
+
totalDuration: number
|
|
32
|
+
avgDuration: number
|
|
33
|
+
successRate: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CompositionGraph {
|
|
37
|
+
nodes: Map<string, { calls: number; depths: Set<number> }>
|
|
38
|
+
edges: Map<string, { count: number; successes: number }>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// COMPOSITION OBSERVER
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
export class CompositionObserver {
|
|
46
|
+
private events: CompositionEvent[] = []
|
|
47
|
+
private callStack: string[] = []
|
|
48
|
+
private activityMetrics = new Map<string, {
|
|
49
|
+
calls: number
|
|
50
|
+
successes: number
|
|
51
|
+
failures: number
|
|
52
|
+
blocked: number
|
|
53
|
+
totalDuration: number
|
|
54
|
+
}>()
|
|
55
|
+
private startTimes = new Map<string, number>()
|
|
56
|
+
private onEventCallback?: (event: CompositionEvent) => void
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create enhanced executor config with composition tracking
|
|
60
|
+
*/
|
|
61
|
+
wrapConfig(config: ExecutorConfig): ExecutorConfig {
|
|
62
|
+
const originalOnActivityExecute = config.onActivityExecute
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...config,
|
|
66
|
+
onActivityExecute: async (templateId, variables, reason) => {
|
|
67
|
+
const parent = this.callStack[this.callStack.length - 1] || 'ROOT'
|
|
68
|
+
const depth = this.callStack.length
|
|
69
|
+
const eventId = `${templateId}_${Date.now()}`
|
|
70
|
+
|
|
71
|
+
// Record start event
|
|
72
|
+
const startEvent: CompositionEvent = {
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
parent,
|
|
75
|
+
child: templateId,
|
|
76
|
+
depth,
|
|
77
|
+
reason,
|
|
78
|
+
status: 'started'
|
|
79
|
+
}
|
|
80
|
+
this.recordEvent(startEvent)
|
|
81
|
+
|
|
82
|
+
// Initialize metrics
|
|
83
|
+
if (!this.activityMetrics.has(templateId)) {
|
|
84
|
+
this.activityMetrics.set(templateId, {
|
|
85
|
+
calls: 0,
|
|
86
|
+
successes: 0,
|
|
87
|
+
failures: 0,
|
|
88
|
+
blocked: 0,
|
|
89
|
+
totalDuration: 0
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const metrics = this.activityMetrics.get(templateId)!
|
|
94
|
+
metrics.calls++
|
|
95
|
+
|
|
96
|
+
// Track start time
|
|
97
|
+
this.startTimes.set(eventId, Date.now())
|
|
98
|
+
|
|
99
|
+
// Check for cycle (before calling original handler)
|
|
100
|
+
const callStack = (config.activityCallStack ?? [])
|
|
101
|
+
if (callStack.includes(templateId)) {
|
|
102
|
+
const cycleChain = [...callStack, templateId].join(' → ')
|
|
103
|
+
|
|
104
|
+
const blockedEvent: CompositionEvent = {
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
parent,
|
|
107
|
+
child: templateId,
|
|
108
|
+
depth,
|
|
109
|
+
reason,
|
|
110
|
+
status: 'blocked',
|
|
111
|
+
error: `Cycle detected: ${cycleChain}`
|
|
112
|
+
}
|
|
113
|
+
this.recordEvent(blockedEvent)
|
|
114
|
+
metrics.blocked++
|
|
115
|
+
|
|
116
|
+
// Return blocked response
|
|
117
|
+
return {
|
|
118
|
+
id: `blocked_cycle_${Date.now()}`,
|
|
119
|
+
templateId,
|
|
120
|
+
status: "failed" as const,
|
|
121
|
+
error: `Cycle detected: ${cycleChain}`,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Update call stack
|
|
126
|
+
this.callStack.push(templateId)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Call original handler or default implementation
|
|
130
|
+
const result = originalOnActivityExecute
|
|
131
|
+
? await originalOnActivityExecute(templateId, variables, reason)
|
|
132
|
+
: { id: `result_${Date.now()}`, templateId, status: 'completed' as const }
|
|
133
|
+
|
|
134
|
+
// Calculate duration
|
|
135
|
+
const startTime = this.startTimes.get(eventId)
|
|
136
|
+
const duration = startTime ? Date.now() - startTime : 0
|
|
137
|
+
this.startTimes.delete(eventId)
|
|
138
|
+
|
|
139
|
+
// Update metrics
|
|
140
|
+
metrics.totalDuration += duration
|
|
141
|
+
if (result.status === 'completed') {
|
|
142
|
+
metrics.successes++
|
|
143
|
+
} else {
|
|
144
|
+
metrics.failures++
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Record completion event
|
|
148
|
+
const completeEvent: CompositionEvent = {
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
parent,
|
|
151
|
+
child: templateId,
|
|
152
|
+
depth,
|
|
153
|
+
reason,
|
|
154
|
+
status: result.status === 'completed' ? 'completed' : 'failed',
|
|
155
|
+
duration,
|
|
156
|
+
error: result.status === 'failed' ? result.error : undefined
|
|
157
|
+
}
|
|
158
|
+
this.recordEvent(completeEvent)
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Calculate duration
|
|
163
|
+
const startTime = this.startTimes.get(eventId)
|
|
164
|
+
const duration = startTime ? Date.now() - startTime : 0
|
|
165
|
+
this.startTimes.delete(eventId)
|
|
166
|
+
|
|
167
|
+
// Update metrics
|
|
168
|
+
metrics.totalDuration += duration
|
|
169
|
+
metrics.failures++
|
|
170
|
+
|
|
171
|
+
// Record failure event
|
|
172
|
+
const failureEvent: CompositionEvent = {
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
parent,
|
|
175
|
+
child: templateId,
|
|
176
|
+
depth,
|
|
177
|
+
reason,
|
|
178
|
+
status: 'failed',
|
|
179
|
+
duration,
|
|
180
|
+
error: error instanceof Error ? error.message : String(error)
|
|
181
|
+
}
|
|
182
|
+
this.recordEvent(failureEvent)
|
|
183
|
+
|
|
184
|
+
throw error
|
|
185
|
+
} finally {
|
|
186
|
+
// Pop call stack
|
|
187
|
+
this.callStack.pop()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register callback for real-time event notifications
|
|
195
|
+
*/
|
|
196
|
+
onEvent(callback: (event: CompositionEvent) => void): void {
|
|
197
|
+
this.onEventCallback = callback
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Record composition event
|
|
202
|
+
*/
|
|
203
|
+
private recordEvent(event: CompositionEvent): void {
|
|
204
|
+
this.events.push(event)
|
|
205
|
+
if (this.onEventCallback) {
|
|
206
|
+
this.onEventCallback(event)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get all recorded events
|
|
212
|
+
*/
|
|
213
|
+
getEvents(): CompositionEvent[] {
|
|
214
|
+
return [...this.events]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get current call stack
|
|
219
|
+
*/
|
|
220
|
+
getCurrentCallStack(): string[] {
|
|
221
|
+
return [...this.callStack]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get activity metrics
|
|
226
|
+
*/
|
|
227
|
+
getActivityMetrics(): ActivityMetrics[] {
|
|
228
|
+
return Array.from(this.activityMetrics.entries()).map(([activityId, metrics]) => ({
|
|
229
|
+
activityId,
|
|
230
|
+
calls: metrics.calls,
|
|
231
|
+
successes: metrics.successes,
|
|
232
|
+
failures: metrics.failures,
|
|
233
|
+
blocked: metrics.blocked,
|
|
234
|
+
totalDuration: metrics.totalDuration,
|
|
235
|
+
avgDuration: metrics.calls > 0 ? Math.round(metrics.totalDuration / metrics.calls) : 0,
|
|
236
|
+
successRate: metrics.calls > 0 ? metrics.successes / metrics.calls : 0
|
|
237
|
+
})).sort((a, b) => b.calls - a.calls)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Build composition graph
|
|
242
|
+
*/
|
|
243
|
+
getCompositionGraph(): CompositionGraph {
|
|
244
|
+
const nodes = new Map<string, { calls: number; depths: Set<number> }>()
|
|
245
|
+
const edges = new Map<string, { count: number; successes: number }>()
|
|
246
|
+
|
|
247
|
+
for (const event of this.events) {
|
|
248
|
+
if (event.status !== 'started') continue
|
|
249
|
+
|
|
250
|
+
// Track nodes
|
|
251
|
+
if (!nodes.has(event.child)) {
|
|
252
|
+
nodes.set(event.child, { calls: 0, depths: new Set() })
|
|
253
|
+
}
|
|
254
|
+
const node = nodes.get(event.child)!
|
|
255
|
+
node.calls++
|
|
256
|
+
node.depths.add(event.depth)
|
|
257
|
+
|
|
258
|
+
// Track edges
|
|
259
|
+
const edgeKey = `${event.parent} → ${event.child}`
|
|
260
|
+
if (!edges.has(edgeKey)) {
|
|
261
|
+
edges.set(edgeKey, { count: 0, successes: 0 })
|
|
262
|
+
}
|
|
263
|
+
const edge = edges.get(edgeKey)!
|
|
264
|
+
edge.count++
|
|
265
|
+
|
|
266
|
+
// Find corresponding completion event
|
|
267
|
+
const completionEvent = this.events.find(e =>
|
|
268
|
+
e.child === event.child &&
|
|
269
|
+
e.timestamp > event.timestamp &&
|
|
270
|
+
e.status === 'completed'
|
|
271
|
+
)
|
|
272
|
+
if (completionEvent) {
|
|
273
|
+
edge.successes++
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { nodes, edges }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Detect cycles in recorded events
|
|
282
|
+
*/
|
|
283
|
+
detectCycles(): Array<{ cycle: string[]; occurrences: number }> {
|
|
284
|
+
const cycles = new Map<string, number>()
|
|
285
|
+
|
|
286
|
+
for (const event of this.events) {
|
|
287
|
+
if (event.status === 'blocked' && event.error?.includes('Cycle detected:')) {
|
|
288
|
+
const match = event.error.match(/Cycle detected: (.+)/)
|
|
289
|
+
if (match) {
|
|
290
|
+
const cycle = match[1].split(' → ')
|
|
291
|
+
const key = cycle.join('→')
|
|
292
|
+
cycles.set(key, (cycles.get(key) ?? 0) + 1)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return Array.from(cycles.entries())
|
|
298
|
+
.map(([cycle, occurrences]) => ({
|
|
299
|
+
cycle: cycle.split('→'),
|
|
300
|
+
occurrences
|
|
301
|
+
}))
|
|
302
|
+
.sort((a, b) => b.occurrences - a.occurrences)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate text report
|
|
307
|
+
*/
|
|
308
|
+
generateReport(): string {
|
|
309
|
+
const lines: string[] = []
|
|
310
|
+
|
|
311
|
+
lines.push('═'.repeat(80))
|
|
312
|
+
lines.push('📊 COMPOSITION REPORT')
|
|
313
|
+
lines.push('═'.repeat(80))
|
|
314
|
+
|
|
315
|
+
// Summary
|
|
316
|
+
lines.push('')
|
|
317
|
+
lines.push('SUMMARY:')
|
|
318
|
+
lines.push(` Total Events: ${this.events.length}`)
|
|
319
|
+
lines.push(` Total Activities: ${this.activityMetrics.size}`)
|
|
320
|
+
|
|
321
|
+
const totalCalls = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.calls, 0)
|
|
322
|
+
const totalSuccesses = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.successes, 0)
|
|
323
|
+
const totalFailures = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.failures, 0)
|
|
324
|
+
const totalBlocked = Array.from(this.activityMetrics.values()).reduce((sum, m) => sum + m.blocked, 0)
|
|
325
|
+
|
|
326
|
+
lines.push(` Total Calls: ${totalCalls}`)
|
|
327
|
+
lines.push(` Successes: ${totalSuccesses} (${((totalSuccesses / totalCalls) * 100).toFixed(0)}%)`)
|
|
328
|
+
lines.push(` Failures: ${totalFailures} (${((totalFailures / totalCalls) * 100).toFixed(0)}%)`)
|
|
329
|
+
lines.push(` Blocked: ${totalBlocked} (${((totalBlocked / totalCalls) * 100).toFixed(0)}%)`)
|
|
330
|
+
|
|
331
|
+
// Activity Metrics
|
|
332
|
+
lines.push('')
|
|
333
|
+
lines.push('─'.repeat(80))
|
|
334
|
+
lines.push('ACTIVITY METRICS:')
|
|
335
|
+
lines.push('─'.repeat(80))
|
|
336
|
+
|
|
337
|
+
for (const metrics of this.getActivityMetrics()) {
|
|
338
|
+
lines.push('')
|
|
339
|
+
lines.push(`${metrics.activityId}:`)
|
|
340
|
+
lines.push(` Calls: ${metrics.calls}`)
|
|
341
|
+
lines.push(` Successes: ${metrics.successes} (${(metrics.successRate * 100).toFixed(0)}%)`)
|
|
342
|
+
lines.push(` Failures: ${metrics.failures}`)
|
|
343
|
+
if (metrics.blocked > 0) {
|
|
344
|
+
lines.push(` Blocked: ${metrics.blocked}`)
|
|
345
|
+
}
|
|
346
|
+
lines.push(` Avg Duration: ${metrics.avgDuration}ms`)
|
|
347
|
+
lines.push(` Total Duration: ${metrics.totalDuration}ms`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Composition Graph
|
|
351
|
+
lines.push('')
|
|
352
|
+
lines.push('─'.repeat(80))
|
|
353
|
+
lines.push('COMPOSITION GRAPH:')
|
|
354
|
+
lines.push('─'.repeat(80))
|
|
355
|
+
|
|
356
|
+
const graph = this.getCompositionGraph()
|
|
357
|
+
|
|
358
|
+
lines.push('')
|
|
359
|
+
lines.push('Nodes (Activities):')
|
|
360
|
+
for (const [activity, data] of graph.nodes.entries()) {
|
|
361
|
+
lines.push(` ${activity}`)
|
|
362
|
+
lines.push(` Calls: ${data.calls}`)
|
|
363
|
+
lines.push(` Depths: ${Array.from(data.depths).sort((a, b) => a - b).join(', ')}`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
lines.push('')
|
|
367
|
+
lines.push('Edges (Compositions):')
|
|
368
|
+
for (const [edge, data] of graph.edges.entries()) {
|
|
369
|
+
const successRate = data.count > 0 ? (data.successes / data.count * 100).toFixed(0) : '0'
|
|
370
|
+
lines.push(` ${edge}`)
|
|
371
|
+
lines.push(` Count: ${data.count}`)
|
|
372
|
+
lines.push(` Success Rate: ${successRate}%`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Cycles
|
|
376
|
+
const cycles = this.detectCycles()
|
|
377
|
+
if (cycles.length > 0) {
|
|
378
|
+
lines.push('')
|
|
379
|
+
lines.push('─'.repeat(80))
|
|
380
|
+
lines.push('⚠️ CYCLES DETECTED:')
|
|
381
|
+
lines.push('─'.repeat(80))
|
|
382
|
+
|
|
383
|
+
for (const { cycle, occurrences } of cycles) {
|
|
384
|
+
lines.push(` ${cycle.join(' → ')} (${occurrences}x)`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
lines.push('')
|
|
389
|
+
lines.push('═'.repeat(80))
|
|
390
|
+
|
|
391
|
+
return lines.join('\n')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Generate ASCII tree visualization
|
|
396
|
+
*/
|
|
397
|
+
generateTree(): string {
|
|
398
|
+
const lines: string[] = []
|
|
399
|
+
|
|
400
|
+
// Group events by parent
|
|
401
|
+
const tree = new Map<string, string[]>()
|
|
402
|
+
for (const event of this.events) {
|
|
403
|
+
if (event.status === 'started') {
|
|
404
|
+
if (!tree.has(event.parent)) {
|
|
405
|
+
tree.set(event.parent, [])
|
|
406
|
+
}
|
|
407
|
+
tree.get(event.parent)!.push(event.child)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Build tree starting from ROOT
|
|
412
|
+
const buildBranch = (node: string, prefix: string, isLast: boolean): void => {
|
|
413
|
+
const marker = isLast ? '└─' : '├─'
|
|
414
|
+
lines.push(`${prefix}${marker} ${node}`)
|
|
415
|
+
|
|
416
|
+
const children = tree.get(node) ?? []
|
|
417
|
+
const newPrefix = prefix + (isLast ? ' ' : '│ ')
|
|
418
|
+
|
|
419
|
+
children.forEach((child, index) => {
|
|
420
|
+
buildBranch(child, newPrefix, index === children.length - 1)
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
lines.push('ROOT')
|
|
425
|
+
const rootChildren = tree.get('ROOT') ?? []
|
|
426
|
+
rootChildren.forEach((child, index) => {
|
|
427
|
+
buildBranch(child, '', index === rootChildren.length - 1)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
return lines.join('\n')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Clear all recorded data
|
|
435
|
+
*/
|
|
436
|
+
clear(): void {
|
|
437
|
+
this.events = []
|
|
438
|
+
this.callStack = []
|
|
439
|
+
this.activityMetrics.clear()
|
|
440
|
+
this.startTimes.clear()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Create a new composition observer
|
|
446
|
+
*/
|
|
447
|
+
export function createCompositionObserver(): CompositionObserver {
|
|
448
|
+
return new CompositionObserver()
|
|
449
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob Configuration
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration from environment variables and optional config file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MinibobConfig, VesselManifest } from "./types"
|
|
8
|
+
import type { RuntimeEnvironment } from "./environment"
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// DEFAULT CONFIGURATION
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
const defaultConfig: MinibobConfig = {
|
|
15
|
+
port: 8080,
|
|
16
|
+
host: "0.0.0.0",
|
|
17
|
+
provider: "anthropic",
|
|
18
|
+
model: "claude-sonnet-4-20250514",
|
|
19
|
+
apiKey: "",
|
|
20
|
+
vessels: {
|
|
21
|
+
metabob: {
|
|
22
|
+
type: "http",
|
|
23
|
+
endpoint: process.env.MINIBOB_MCP_ENDPOINT || "http://api.minibob.local",
|
|
24
|
+
capabilities: ["activities", "impulses", "executions", "thompson-sampling"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
templatesDir: "./templates",
|
|
28
|
+
workingDirectory: process.cwd(),
|
|
29
|
+
autoCommit: false,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// CONFIGURATION LOADING
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safe wrapper for Bun.file() with better error handling
|
|
38
|
+
*/
|
|
39
|
+
function safeReadFile(path: string): ReturnType<typeof Bun.file> {
|
|
40
|
+
if (!path || typeof path !== 'string') {
|
|
41
|
+
throw new Error(`Invalid file path: expected string, got ${typeof path}`)
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return Bun.file(path)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Failed to create file handle for '${path}': ${error instanceof Error ? error.message : String(error)}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load configuration from environment and optional file
|
|
52
|
+
*/
|
|
53
|
+
export async function loadConfig(configPath?: string): Promise<MinibobConfig> {
|
|
54
|
+
let fileConfig: Partial<MinibobConfig> = {}
|
|
55
|
+
|
|
56
|
+
// Try to load config file
|
|
57
|
+
if (configPath) {
|
|
58
|
+
const file = safeReadFile(configPath)
|
|
59
|
+
if (await file.exists()) {
|
|
60
|
+
fileConfig = await file.json()
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Try default locations
|
|
64
|
+
for (const path of ["minibob.json", ".minibob.json", "opencode.json"]) {
|
|
65
|
+
const file = safeReadFile(path)
|
|
66
|
+
if (await file.exists()) {
|
|
67
|
+
fileConfig = await file.json()
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Merge with environment variables
|
|
74
|
+
const config: MinibobConfig = {
|
|
75
|
+
...defaultConfig,
|
|
76
|
+
...fileConfig,
|
|
77
|
+
port: parseInt(process.env.MINIBOB_PORT ?? String(fileConfig.port ?? defaultConfig.port)),
|
|
78
|
+
host: process.env.MINIBOB_HOST ?? fileConfig.host ?? defaultConfig.host,
|
|
79
|
+
provider: (process.env.MINIBOB_PROVIDER ?? fileConfig.provider ?? defaultConfig.provider) as "anthropic" | "openai",
|
|
80
|
+
model: process.env.MINIBOB_MODEL ?? fileConfig.model ?? defaultConfig.model,
|
|
81
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? fileConfig.apiKey ?? "",
|
|
82
|
+
workingDirectory: process.env.MINIBOB_WORKDIR ?? fileConfig.workingDirectory ?? process.cwd(),
|
|
83
|
+
templatesDir: process.env.MINIBOB_TEMPLATES ?? fileConfig.templatesDir ?? defaultConfig.templatesDir,
|
|
84
|
+
autoCommit: process.env.MINIBOB_AUTO_COMMIT === "true" || fileConfig.autoCommit || false,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate required fields
|
|
88
|
+
if (!config.apiKey) {
|
|
89
|
+
console.warn("Warning: No API key configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return config
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// VESSEL MANIFEST
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Runtime context for dynamic manifest generation
|
|
101
|
+
*/
|
|
102
|
+
export interface RuntimeContext {
|
|
103
|
+
environment: RuntimeEnvironment
|
|
104
|
+
clusterMode: boolean
|
|
105
|
+
peerCount: number
|
|
106
|
+
boredomEnabled: boolean
|
|
107
|
+
acpGossipEnabled: boolean
|
|
108
|
+
backendAvailable: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate vessel manifest with dynamic capabilities based on runtime context
|
|
113
|
+
*
|
|
114
|
+
* Capabilities are determined by detected environment:
|
|
115
|
+
* - Base: activities, impulses, git (always enabled)
|
|
116
|
+
* - acp-gossip: Only in cluster mode (3+ pods)
|
|
117
|
+
* - boredom: Only in cluster mode with backend available
|
|
118
|
+
*/
|
|
119
|
+
export function generateManifest(config: MinibobConfig, runtime?: RuntimeContext): VesselManifest {
|
|
120
|
+
const capabilities = ["activities", "impulses", "git"]
|
|
121
|
+
|
|
122
|
+
// Add dynamic capabilities based on runtime context
|
|
123
|
+
if (runtime?.acpGossipEnabled) {
|
|
124
|
+
capabilities.push("acp-gossip")
|
|
125
|
+
}
|
|
126
|
+
if (runtime?.boredomEnabled) {
|
|
127
|
+
capabilities.push("boredom")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Always expose basic ACP endpoint for delegation
|
|
131
|
+
capabilities.push("acp")
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id: "minibob",
|
|
135
|
+
name: "minibob - Minimal Vessel",
|
|
136
|
+
version: "0.1.0",
|
|
137
|
+
capabilities,
|
|
138
|
+
tools: [
|
|
139
|
+
"bash",
|
|
140
|
+
"read",
|
|
141
|
+
"write",
|
|
142
|
+
"edit",
|
|
143
|
+
"glob",
|
|
144
|
+
"grep",
|
|
145
|
+
"list",
|
|
146
|
+
"git",
|
|
147
|
+
],
|
|
148
|
+
acpEndpoint: `http://${config.host}:${config.port}/acp`,
|
|
149
|
+
metadata: runtime ? {
|
|
150
|
+
environment: runtime.environment,
|
|
151
|
+
clusterMode: runtime.clusterMode,
|
|
152
|
+
peerCount: runtime.peerCount,
|
|
153
|
+
backendAvailable: runtime.backendAvailable,
|
|
154
|
+
} : undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get configuration summary for logging
|
|
160
|
+
*/
|
|
161
|
+
export function configSummary(config: MinibobConfig): string {
|
|
162
|
+
return `
|
|
163
|
+
minibob Configuration:
|
|
164
|
+
Port: ${config.port}
|
|
165
|
+
Provider: ${config.provider}
|
|
166
|
+
Model: ${config.model}
|
|
167
|
+
Working Directory: ${config.workingDirectory}
|
|
168
|
+
Templates: ${config.templatesDir}
|
|
169
|
+
Auto-Commit: ${config.autoCommit}
|
|
170
|
+
API Key: ${config.apiKey ? "***" + config.apiKey.slice(-4) : "(not set)"}
|
|
171
|
+
`.trim()
|
|
172
|
+
}
|