@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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impulse Filtering Logic (Phase 1.8)
|
|
3
|
+
*
|
|
4
|
+
* Filters impulses based on learned relevance scores to reduce token usage.
|
|
5
|
+
*
|
|
6
|
+
* Key concepts:
|
|
7
|
+
* - relevance_score: P(success | impulse loaded)
|
|
8
|
+
* - irrelevance_score: P(success | impulse NOT loaded)
|
|
9
|
+
*
|
|
10
|
+
* Decision rules:
|
|
11
|
+
* 1. Always load if relevance_score > 0.8 (strong positive signal)
|
|
12
|
+
* 2. Load if relevance_score > threshold (default: 0.5)
|
|
13
|
+
* 3. Skip if irrelevance_score > relevance_score (better without it)
|
|
14
|
+
* 4. Limit to maxImpulses (default: 10)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ImpulseRelevanceMetric } from "./mcp"
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// CONFIGURATION
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export interface FilterConfig {
|
|
24
|
+
// Threshold for loading impulses (default: 0.5)
|
|
25
|
+
relevanceThreshold: number
|
|
26
|
+
|
|
27
|
+
// Always load impulses with score above this (default: 0.8)
|
|
28
|
+
alwaysLoadThreshold: number
|
|
29
|
+
|
|
30
|
+
// Maximum impulses to load (default: 10)
|
|
31
|
+
maxImpulses: number
|
|
32
|
+
|
|
33
|
+
// Fallback behavior when no metrics available
|
|
34
|
+
fallbackBehavior: 'load-all' | 'load-none' | 'load-top-n'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get filter configuration from environment variables
|
|
39
|
+
*/
|
|
40
|
+
export function getFilterConfig(): FilterConfig {
|
|
41
|
+
const relevanceThreshold = parseFloat(process.env.IMPULSE_RELEVANCE_THRESHOLD || '0.5')
|
|
42
|
+
const alwaysLoadThreshold = parseFloat(process.env.IMPULSE_ALWAYS_LOAD_THRESHOLD || '0.8')
|
|
43
|
+
const maxImpulses = parseInt(process.env.IMPULSE_MAX_LOAD || '10', 10)
|
|
44
|
+
const fallbackBehavior = (process.env.IMPULSE_FALLBACK_BEHAVIOR || 'load-all') as FilterConfig['fallbackBehavior']
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
relevanceThreshold: isNaN(relevanceThreshold) ? 0.5 : relevanceThreshold,
|
|
48
|
+
alwaysLoadThreshold: isNaN(alwaysLoadThreshold) ? 0.8 : alwaysLoadThreshold,
|
|
49
|
+
maxImpulses: isNaN(maxImpulses) ? 10 : maxImpulses,
|
|
50
|
+
fallbackBehavior: ['load-all', 'load-none', 'load-top-n'].includes(fallbackBehavior)
|
|
51
|
+
? fallbackBehavior
|
|
52
|
+
: 'load-all',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_FILTER_CONFIG: FilterConfig = {
|
|
57
|
+
relevanceThreshold: 0.5,
|
|
58
|
+
alwaysLoadThreshold: 0.8,
|
|
59
|
+
maxImpulses: 10,
|
|
60
|
+
fallbackBehavior: 'load-all', // Conservative: load all if no data
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// FILTERING LOGIC
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export interface FilterResult {
|
|
68
|
+
toLoad: string[]
|
|
69
|
+
toSkip: string[]
|
|
70
|
+
reasoning: Record<string, string>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Filter impulses based on relevance scores
|
|
75
|
+
*/
|
|
76
|
+
export function filterImpulsesByRelevance(
|
|
77
|
+
impulseIds: string[],
|
|
78
|
+
metrics: ImpulseRelevanceMetric[],
|
|
79
|
+
config: Partial<FilterConfig> = {}
|
|
80
|
+
): FilterResult {
|
|
81
|
+
// Merge environment config with provided config (provided config takes precedence)
|
|
82
|
+
const envConfig = getFilterConfig()
|
|
83
|
+
const cfg = { ...envConfig, ...config }
|
|
84
|
+
|
|
85
|
+
// Build metric map for fast lookup
|
|
86
|
+
const metricMap = new Map<string, ImpulseRelevanceMetric>()
|
|
87
|
+
for (const metric of metrics) {
|
|
88
|
+
metricMap.set(metric.impulse_id, metric)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const toLoad: string[] = []
|
|
92
|
+
const toSkip: string[] = []
|
|
93
|
+
const reasoning: Record<string, string> = {}
|
|
94
|
+
|
|
95
|
+
for (const impulseId of impulseIds) {
|
|
96
|
+
const metric = metricMap.get(impulseId)
|
|
97
|
+
|
|
98
|
+
if (!metric) {
|
|
99
|
+
// No metrics available - use fallback
|
|
100
|
+
if (cfg.fallbackBehavior === 'load-all') {
|
|
101
|
+
toLoad.push(impulseId)
|
|
102
|
+
reasoning[impulseId] = 'No metrics available (fallback: load)'
|
|
103
|
+
} else if (cfg.fallbackBehavior === 'load-none') {
|
|
104
|
+
toSkip.push(impulseId)
|
|
105
|
+
reasoning[impulseId] = 'No metrics available (fallback: skip)'
|
|
106
|
+
} else {
|
|
107
|
+
// load-top-n: will be handled after sorting
|
|
108
|
+
toLoad.push(impulseId)
|
|
109
|
+
reasoning[impulseId] = 'No metrics available (fallback: top-n)'
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Decision logic based on learned scores
|
|
115
|
+
if (metric.relevance_score >= cfg.alwaysLoadThreshold) {
|
|
116
|
+
// Strong positive signal: always load
|
|
117
|
+
toLoad.push(impulseId)
|
|
118
|
+
reasoning[impulseId] = `High relevance (${metric.relevance_score.toFixed(2)})`
|
|
119
|
+
} else if (metric.irrelevance_score > metric.relevance_score) {
|
|
120
|
+
// Activity succeeds MORE often WITHOUT this impulse: skip (even if above threshold)
|
|
121
|
+
toSkip.push(impulseId)
|
|
122
|
+
reasoning[impulseId] = `More successful without it (irrelevance=${metric.irrelevance_score.toFixed(2)} vs relevance=${metric.relevance_score.toFixed(2)})`
|
|
123
|
+
} else if (metric.relevance_score >= cfg.relevanceThreshold) {
|
|
124
|
+
// Above threshold and relevance > irrelevance: load
|
|
125
|
+
toLoad.push(impulseId)
|
|
126
|
+
reasoning[impulseId] = `Relevant (${metric.relevance_score.toFixed(2)})`
|
|
127
|
+
} else {
|
|
128
|
+
// Below threshold: skip
|
|
129
|
+
toSkip.push(impulseId)
|
|
130
|
+
reasoning[impulseId] = `Low relevance (${metric.relevance_score.toFixed(2)})`
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Enforce max impulses limit
|
|
135
|
+
if (toLoad.length > cfg.maxImpulses) {
|
|
136
|
+
// Sort by relevance score descending
|
|
137
|
+
const loadWithScores = toLoad.map(id => ({
|
|
138
|
+
id,
|
|
139
|
+
score: metricMap.get(id)?.relevance_score || 0,
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
loadWithScores.sort((a, b) => b.score - a.score)
|
|
143
|
+
|
|
144
|
+
const keptIds = loadWithScores.slice(0, cfg.maxImpulses).map(x => x.id)
|
|
145
|
+
const droppedIds = loadWithScores.slice(cfg.maxImpulses).map(x => x.id)
|
|
146
|
+
|
|
147
|
+
for (const id of droppedIds) {
|
|
148
|
+
toSkip.push(id)
|
|
149
|
+
reasoning[id] = `Dropped (exceeded max ${cfg.maxImpulses})`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
toLoad: keptIds,
|
|
154
|
+
toSkip,
|
|
155
|
+
reasoning,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { toLoad, toSkip, reasoning }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// SAVINGS CALCULATION
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
export interface TokenSavings {
|
|
167
|
+
tokensSaved: number
|
|
168
|
+
costSaved: number // USD
|
|
169
|
+
percentSaved: number
|
|
170
|
+
totalTokens: number
|
|
171
|
+
loadedTokens: number
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Calculate token savings from skipped impulses
|
|
176
|
+
*/
|
|
177
|
+
export function calculateSavings(
|
|
178
|
+
skippedImpulses: string[],
|
|
179
|
+
impulseTokenSizes: Map<string, number>
|
|
180
|
+
): TokenSavings {
|
|
181
|
+
const totalTokens = Array.from(impulseTokenSizes.values()).reduce((sum, t) => sum + t, 0)
|
|
182
|
+
const skippedTokens = skippedImpulses
|
|
183
|
+
.map(id => impulseTokenSizes.get(id) || 0)
|
|
184
|
+
.reduce((sum, t) => sum + t, 0)
|
|
185
|
+
|
|
186
|
+
const loadedTokens = totalTokens - skippedTokens
|
|
187
|
+
const percentSaved = totalTokens > 0 ? (skippedTokens / totalTokens) * 100 : 0
|
|
188
|
+
|
|
189
|
+
// Approximate cost: $3 per 1M input tokens (Claude pricing)
|
|
190
|
+
const costSaved = (skippedTokens / 1_000_000) * 3
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
tokensSaved: skippedTokens,
|
|
194
|
+
costSaved,
|
|
195
|
+
percentSaved,
|
|
196
|
+
totalTokens,
|
|
197
|
+
loadedTokens,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Estimate token size for an impulse based on pointer type
|
|
203
|
+
*/
|
|
204
|
+
export function estimateImpulseTokens(impulse: {
|
|
205
|
+
id: string
|
|
206
|
+
pointer: {
|
|
207
|
+
type: string
|
|
208
|
+
[key: string]: any
|
|
209
|
+
}
|
|
210
|
+
}): number {
|
|
211
|
+
const pointerType = impulse.pointer.type
|
|
212
|
+
|
|
213
|
+
// Rough estimates based on typical sizes
|
|
214
|
+
switch (pointerType) {
|
|
215
|
+
case 'file':
|
|
216
|
+
return 2000 // Average file: ~2k tokens
|
|
217
|
+
case 'activityOutput':
|
|
218
|
+
return 1500 // Activity output: ~1.5k tokens
|
|
219
|
+
case 'memo':
|
|
220
|
+
return 500 // Memo: ~500 tokens
|
|
221
|
+
case 'templateDefinition':
|
|
222
|
+
return 1000 // Template def: ~1k tokens
|
|
223
|
+
case 'analysisResult':
|
|
224
|
+
return 1200 // Analysis: ~1.2k tokens
|
|
225
|
+
default:
|
|
226
|
+
return 1000 // Unknown: assume 1k tokens
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// REPORTING
|
|
232
|
+
// =============================================================================
|
|
233
|
+
|
|
234
|
+
export interface FilteringSummary {
|
|
235
|
+
totalImpulses: number
|
|
236
|
+
loadedImpulses: number
|
|
237
|
+
skippedImpulses: number
|
|
238
|
+
tokensSaved: number
|
|
239
|
+
costSaved: number
|
|
240
|
+
percentSaved: number
|
|
241
|
+
topReasons: Array<{ reason: string; count: number }>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate filtering summary for logging/reporting
|
|
246
|
+
*/
|
|
247
|
+
export function generateFilteringSummary(
|
|
248
|
+
filterResult: FilterResult,
|
|
249
|
+
savings: TokenSavings
|
|
250
|
+
): FilteringSummary {
|
|
251
|
+
// Count reasons
|
|
252
|
+
const reasonCounts = new Map<string, number>()
|
|
253
|
+
for (const reason of Object.values(filterResult.reasoning)) {
|
|
254
|
+
const count = reasonCounts.get(reason) || 0
|
|
255
|
+
reasonCounts.set(reason, count + 1)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Sort by count descending
|
|
259
|
+
const topReasons = Array.from(reasonCounts.entries())
|
|
260
|
+
.map(([reason, count]) => ({ reason, count }))
|
|
261
|
+
.sort((a, b) => b.count - a.count)
|
|
262
|
+
.slice(0, 5)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
totalImpulses: filterResult.toLoad.length + filterResult.toSkip.length,
|
|
266
|
+
loadedImpulses: filterResult.toLoad.length,
|
|
267
|
+
skippedImpulses: filterResult.toSkip.length,
|
|
268
|
+
tokensSaved: savings.tokensSaved,
|
|
269
|
+
costSaved: savings.costSaved,
|
|
270
|
+
percentSaved: savings.percentSaved,
|
|
271
|
+
topReasons,
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/impulse.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob Impulse System
|
|
3
|
+
*
|
|
4
|
+
* Minimal impulse implementation for context management.
|
|
5
|
+
* Supports: memo, file, activityOutput, custom pointer types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Impulse, ImpulsePointer } from "./types"
|
|
9
|
+
import { getMCPClient, isMCPEnabled } from "./mcp"
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// IMPULSE STORAGE
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* In-memory impulse store (can be backed by MCP in production)
|
|
17
|
+
*/
|
|
18
|
+
class ImpulseStore {
|
|
19
|
+
private impulses = new Map<string, Impulse>()
|
|
20
|
+
private customResolvers = new Map<string, (data: Record<string, unknown>) => Promise<string>>()
|
|
21
|
+
private activityOutputs = new Map<string, Map<string, string>>() // activityId -> taskId -> output
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new impulse
|
|
25
|
+
*/
|
|
26
|
+
create(impulse: Omit<Impulse, "loaded" | "createdAt">): Impulse {
|
|
27
|
+
const fullImpulse: Impulse = {
|
|
28
|
+
...impulse,
|
|
29
|
+
loaded: false,
|
|
30
|
+
createdAt: Date.now(),
|
|
31
|
+
}
|
|
32
|
+
this.impulses.set(impulse.id, fullImpulse)
|
|
33
|
+
|
|
34
|
+
// Store in backend if MCP enabled
|
|
35
|
+
// This enables cross-execution impulse tracking and learning
|
|
36
|
+
// Data flow: impulse.ts:create() → mcp.ts:storeImpulse() → POST /impulses → backend storage
|
|
37
|
+
if (isMCPEnabled()) {
|
|
38
|
+
const mcp = getMCPClient()
|
|
39
|
+
if (mcp) {
|
|
40
|
+
mcp.storeImpulse(fullImpulse).catch((err: Error) => {
|
|
41
|
+
console.warn(`[Impulse] Failed to store in backend: ${err.message}`)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return fullImpulse
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get an impulse by ID
|
|
51
|
+
*/
|
|
52
|
+
get(id: string): Impulse | undefined {
|
|
53
|
+
return this.impulses.get(id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load an impulse (resolve its pointer and populate content)
|
|
58
|
+
*/
|
|
59
|
+
async load(id: string): Promise<Impulse> {
|
|
60
|
+
const impulse = this.impulses.get(id)
|
|
61
|
+
if (!impulse) {
|
|
62
|
+
throw new Error(`Impulse not found: ${id}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (impulse.loaded && impulse.content) {
|
|
66
|
+
return impulse
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const content = await this.resolvePointer(impulse.pointer)
|
|
70
|
+
const tokenCount = this.estimateTokens(content)
|
|
71
|
+
|
|
72
|
+
// Truncate if over budget
|
|
73
|
+
let finalContent = content
|
|
74
|
+
if (tokenCount > impulse.budget) {
|
|
75
|
+
const ratio = impulse.budget / tokenCount
|
|
76
|
+
const targetChars = Math.floor(content.length * ratio * 0.9) // 10% safety margin
|
|
77
|
+
finalContent = content.substring(0, targetChars) + "\n... (truncated to fit budget)"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const loadedImpulse: Impulse = {
|
|
81
|
+
...impulse,
|
|
82
|
+
loaded: true,
|
|
83
|
+
content: finalContent,
|
|
84
|
+
tokenCount: Math.min(tokenCount, impulse.budget),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.impulses.set(id, loadedImpulse)
|
|
88
|
+
return loadedImpulse
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unload an impulse (free memory)
|
|
93
|
+
*/
|
|
94
|
+
unload(id: string): void {
|
|
95
|
+
const impulse = this.impulses.get(id)
|
|
96
|
+
if (impulse) {
|
|
97
|
+
this.impulses.set(id, {
|
|
98
|
+
...impulse,
|
|
99
|
+
loaded: false,
|
|
100
|
+
content: undefined,
|
|
101
|
+
tokenCount: undefined,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Delete an impulse
|
|
108
|
+
*/
|
|
109
|
+
delete(id: string): boolean {
|
|
110
|
+
return this.impulses.delete(id)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List all impulses
|
|
115
|
+
*/
|
|
116
|
+
list(): Impulse[] {
|
|
117
|
+
return Array.from(this.impulses.values())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register a custom resolver
|
|
122
|
+
*/
|
|
123
|
+
registerResolver(name: string, resolver: (data: Record<string, unknown>) => Promise<string>): void {
|
|
124
|
+
this.customResolvers.set(name, resolver)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Store activity output for activityOutput pointer resolution
|
|
129
|
+
*/
|
|
130
|
+
storeActivityOutput(activityId: string, taskId: string, output: string): void {
|
|
131
|
+
if (!this.activityOutputs.has(activityId)) {
|
|
132
|
+
this.activityOutputs.set(activityId, new Map())
|
|
133
|
+
}
|
|
134
|
+
this.activityOutputs.get(activityId)!.set(taskId, output)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get activity output
|
|
139
|
+
*/
|
|
140
|
+
getActivityOutput(activityId: string, taskId?: string): string | undefined {
|
|
141
|
+
const outputs = this.activityOutputs.get(activityId)
|
|
142
|
+
if (!outputs) return undefined
|
|
143
|
+
|
|
144
|
+
if (taskId) {
|
|
145
|
+
return outputs.get(taskId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Return all outputs concatenated
|
|
149
|
+
return Array.from(outputs.values()).join("\n\n---\n\n")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a pointer to content
|
|
154
|
+
*
|
|
155
|
+
* Architecture: MiniBob handles LOCAL types (memo, file) directly.
|
|
156
|
+
* All other types are delegated to backend via MCP.
|
|
157
|
+
*/
|
|
158
|
+
private async resolvePointer(pointer: ImpulsePointer): Promise<string> {
|
|
159
|
+
// LOCAL TYPE: memo (content embedded in pointer)
|
|
160
|
+
if (pointer.type === "memo" && "content" in pointer) {
|
|
161
|
+
return pointer.content as string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// LOCAL TYPE: file (read from minibob's filesystem)
|
|
165
|
+
if (pointer.type === "file" && "path" in pointer) {
|
|
166
|
+
const filePath = pointer.path
|
|
167
|
+
|
|
168
|
+
// Validate path exists and is a string
|
|
169
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
170
|
+
throw new Error(`Invalid file path in impulse pointer: expected string, got ${typeof filePath}. Pointer: ${JSON.stringify(pointer)}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let file
|
|
174
|
+
try {
|
|
175
|
+
file = Bun.file(filePath)
|
|
176
|
+
} catch (bunError) {
|
|
177
|
+
throw new Error(`Bun.file() failed for impulse path '${filePath}': ${bunError instanceof Error ? bunError.message : String(bunError)}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!(await file.exists())) {
|
|
181
|
+
throw new Error(`File not found: ${filePath}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const content = await file.text()
|
|
185
|
+
const lines = content.split("\n")
|
|
186
|
+
const offset = (pointer as any).offset ?? 0
|
|
187
|
+
const limit = (pointer as any).limit ?? lines.length
|
|
188
|
+
return lines.slice(offset, offset + limit).join("\n")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// BACKEND TYPES: Delegate to backend via MCP
|
|
192
|
+
// This includes: activityOutput, activityExecutionTrace, activityTemplate,
|
|
193
|
+
// activityMetrics, and ANY future types the backend introduces.
|
|
194
|
+
if (isMCPEnabled()) {
|
|
195
|
+
const mcp = getMCPClient()
|
|
196
|
+
if (mcp && "resolveImpulse" in mcp) {
|
|
197
|
+
try {
|
|
198
|
+
const content = await (mcp as any).resolveImpulse(pointer)
|
|
199
|
+
return content
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Failed to resolve impulse type "${pointer.type}" from backend: ${
|
|
203
|
+
error instanceof Error ? error.message : String(error)
|
|
204
|
+
}`
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// FALLBACK: No backend connection
|
|
211
|
+
// Try local activityOutput store (in-memory only)
|
|
212
|
+
if (pointer.type === "activityOutput" && "activityId" in pointer) {
|
|
213
|
+
const output = this.getActivityOutput(
|
|
214
|
+
pointer.activityId as string,
|
|
215
|
+
"taskId" in pointer ? (pointer.taskId as string) : undefined
|
|
216
|
+
)
|
|
217
|
+
if (output) {
|
|
218
|
+
console.warn(
|
|
219
|
+
`[Impulse] Resolved activityOutput from in-memory cache (no backend). ` +
|
|
220
|
+
`This will not work across executions.`
|
|
221
|
+
)
|
|
222
|
+
return output
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// No backend and not a local type - fail
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Impulse type "${pointer.type}" requires backend connection. ` +
|
|
229
|
+
`Only "memo" and "file" types work offline. ` +
|
|
230
|
+
`Please enable MCP connection to metabob-activity-api.`
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Estimate token count (rough approximation: 4 chars per token)
|
|
236
|
+
*/
|
|
237
|
+
private estimateTokens(content: string): number {
|
|
238
|
+
return Math.ceil(content.length / 4)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// IMPULSE MANAGER (Singleton)
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
let store: ImpulseStore | null = null
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get the impulse store (singleton)
|
|
250
|
+
*/
|
|
251
|
+
export function getImpulseStore(): ImpulseStore {
|
|
252
|
+
if (!store) {
|
|
253
|
+
store = new ImpulseStore()
|
|
254
|
+
}
|
|
255
|
+
return store
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create a new impulse
|
|
260
|
+
*/
|
|
261
|
+
export function createImpulse(impulse: Omit<Impulse, "loaded" | "createdAt">): Impulse {
|
|
262
|
+
return getImpulseStore().create(impulse)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load an impulse
|
|
267
|
+
*/
|
|
268
|
+
export async function loadImpulse(id: string): Promise<Impulse> {
|
|
269
|
+
return getImpulseStore().load(id)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Load multiple impulses
|
|
274
|
+
*/
|
|
275
|
+
export async function loadImpulses(ids: string[]): Promise<Impulse[]> {
|
|
276
|
+
return Promise.all(ids.map((id) => loadImpulse(id)))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Format impulses for context injection
|
|
281
|
+
*/
|
|
282
|
+
export function formatImpulsesForContext(impulses: Impulse[]): string {
|
|
283
|
+
if (impulses.length === 0) return ""
|
|
284
|
+
|
|
285
|
+
const formatted = impulses
|
|
286
|
+
.filter((imp) => imp.loaded && imp.content)
|
|
287
|
+
.map((imp) => {
|
|
288
|
+
return `<impulse id="${imp.id}" type="${imp.pointer.type}" tokens="${imp.tokenCount ?? 0}/${imp.budget}">
|
|
289
|
+
${imp.content}
|
|
290
|
+
</impulse>`
|
|
291
|
+
})
|
|
292
|
+
.join("\n\n")
|
|
293
|
+
|
|
294
|
+
return `<impulse_context>
|
|
295
|
+
${formatted}
|
|
296
|
+
</impulse_context>`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Register a custom resolver
|
|
301
|
+
*/
|
|
302
|
+
export function registerResolver(name: string, resolver: (data: Record<string, unknown>) => Promise<string>): void {
|
|
303
|
+
getImpulseStore().registerResolver(name, resolver)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Store activity output
|
|
308
|
+
*/
|
|
309
|
+
export function storeActivityOutput(activityId: string, taskId: string, output: string): void {
|
|
310
|
+
getImpulseStore().storeActivityOutput(activityId, taskId, output)
|
|
311
|
+
}
|