@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
package/src/activity.ts
ADDED
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob Activity Executor
|
|
3
|
+
*
|
|
4
|
+
* Executes activity templates with impulse injection and tool calling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ActivityTemplate,
|
|
9
|
+
ActivityExecution,
|
|
10
|
+
ActivityTask,
|
|
11
|
+
TaskResult,
|
|
12
|
+
Impulse,
|
|
13
|
+
Message,
|
|
14
|
+
ToolResult,
|
|
15
|
+
ToolHandler,
|
|
16
|
+
ExecutedTask,
|
|
17
|
+
} from "./types"
|
|
18
|
+
import { createLLMClient, type LLMClient } from "./llm"
|
|
19
|
+
import { createToolHandlers, getAllToolDefinitions, type ToolHandlerOptions, type ToolDefinition } from "./tools"
|
|
20
|
+
import {
|
|
21
|
+
createImpulse,
|
|
22
|
+
loadImpulses,
|
|
23
|
+
formatImpulsesForContext,
|
|
24
|
+
storeActivityOutput,
|
|
25
|
+
getImpulseStore,
|
|
26
|
+
} from "./impulse"
|
|
27
|
+
import { getMCPClient, isMCPEnabled } from "./mcp"
|
|
28
|
+
import {
|
|
29
|
+
filterImpulsesByRelevance,
|
|
30
|
+
calculateSavings,
|
|
31
|
+
estimateImpulseTokens,
|
|
32
|
+
generateFilteringSummary,
|
|
33
|
+
} from "./impulse-filter"
|
|
34
|
+
import { assembleTemplateFromExecution } from "./template-generator"
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safe wrapper for Bun.file() with better error handling
|
|
38
|
+
*/
|
|
39
|
+
function safeReadFile(path: string, context?: string): ReturnType<typeof Bun.file> {
|
|
40
|
+
if (!path || typeof path !== 'string') {
|
|
41
|
+
const stack = new Error().stack
|
|
42
|
+
throw new Error(`[activity.ts] Invalid file path: expected string, got ${typeof path}${context ? ` (context: ${context})` : ''}\nStack: ${stack}`)
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return Bun.file(path)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Failed to create file handle for '${path}': ${error instanceof Error ? error.message : String(error)}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Substitute {{impulse:id}} placeholders with impulse content
|
|
53
|
+
*/
|
|
54
|
+
async function substituteImpulses(template: string, _impulseIds: string[]): Promise<string> {
|
|
55
|
+
let result = template
|
|
56
|
+
|
|
57
|
+
// Find all {{impulse:id}} patterns
|
|
58
|
+
const impulsePattern = /{{impulse:([a-zA-Z0-9_-]+)}}/g
|
|
59
|
+
const matches = Array.from(template.matchAll(impulsePattern))
|
|
60
|
+
|
|
61
|
+
if (matches.length === 0) {
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Load all referenced impulses
|
|
66
|
+
const referencedIds = matches.map(m => m[1]).filter((id): id is string => id !== undefined)
|
|
67
|
+
const impulses = await loadImpulses(referencedIds)
|
|
68
|
+
|
|
69
|
+
// Format impulses for context
|
|
70
|
+
const formatted = formatImpulsesForContext(impulses)
|
|
71
|
+
|
|
72
|
+
// Replace each placeholder
|
|
73
|
+
for (const match of matches) {
|
|
74
|
+
const impulseId = match[1]
|
|
75
|
+
const impulseContent = impulses.find(imp => imp.id === impulseId)
|
|
76
|
+
|
|
77
|
+
if (impulseContent) {
|
|
78
|
+
// Use formatted content (respects budget)
|
|
79
|
+
const content = typeof impulseContent.content === 'string'
|
|
80
|
+
? impulseContent.content
|
|
81
|
+
: JSON.stringify(impulseContent.content, null, 2)
|
|
82
|
+
result = result.replace(match[0], content)
|
|
83
|
+
} else {
|
|
84
|
+
// Impulse not found, leave placeholder or use empty
|
|
85
|
+
console.warn(`Impulse not found: ${impulseId}`)
|
|
86
|
+
result = result.replace(match[0], `[impulse:${impulseId} not found]`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// ACTIVITY EXECUTOR
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
export interface ExecutorConfig {
|
|
98
|
+
provider: "anthropic" | "openai"
|
|
99
|
+
apiKey: string
|
|
100
|
+
model: string
|
|
101
|
+
workingDirectory: string
|
|
102
|
+
systemPrompt?: string
|
|
103
|
+
// Activity management callbacks (for autonomous trailblazing)
|
|
104
|
+
onSearchActivities?: (category?: string, verbose?: boolean) => Promise<{ count: number; activities: unknown[] }>
|
|
105
|
+
onCreateActivity?: (params: {
|
|
106
|
+
goalDescription: string
|
|
107
|
+
templateName: string
|
|
108
|
+
category: string
|
|
109
|
+
variables: Record<string, unknown>
|
|
110
|
+
}) => Promise<{ templateId: string }>
|
|
111
|
+
/**
|
|
112
|
+
* Callback when an activity is executed (for composition tracking)
|
|
113
|
+
*/
|
|
114
|
+
onActivityExecute?: (templateId: string, variables: Record<string, unknown>, reason?: string) => Promise<ActivityExecution>
|
|
115
|
+
/**
|
|
116
|
+
* Custom domain-specific tools to expose to the LLM in addition to built-in tools.
|
|
117
|
+
* The host application (e.g. PerspectiveMinibobAdapter) injects these so that
|
|
118
|
+
* activity templates can call Perspective-specific database / API operations.
|
|
119
|
+
*/
|
|
120
|
+
customTools?: ToolHandlerOptions["customTools"]
|
|
121
|
+
/**
|
|
122
|
+
* Maximum nesting depth for nested activity execution.
|
|
123
|
+
* Prevents runaway recursion when activities call other activities.
|
|
124
|
+
* Default: 3
|
|
125
|
+
*/
|
|
126
|
+
maxNestingDepth?: number
|
|
127
|
+
/**
|
|
128
|
+
* Activity call stack for cycle detection.
|
|
129
|
+
* Tracks the chain of activity IDs from root to current execution.
|
|
130
|
+
* Used to detect and prevent circular activity calls (A → B → A).
|
|
131
|
+
*
|
|
132
|
+
* Example: ["process-goal", "explore-codebase", "read-file"]
|
|
133
|
+
*
|
|
134
|
+
* Internal use only - populated automatically during composition.
|
|
135
|
+
*/
|
|
136
|
+
activityCallStack?: string[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ExecuteOptions {
|
|
140
|
+
template: ActivityTemplate
|
|
141
|
+
variables: Record<string, unknown>
|
|
142
|
+
reason?: string
|
|
143
|
+
impulses?: Impulse[] // Context from previous executions
|
|
144
|
+
onTaskStart?: (taskId: string) => void
|
|
145
|
+
onTaskComplete?: (taskId: string, result: TaskResult) => void
|
|
146
|
+
// Parent activity context for composition tracking
|
|
147
|
+
parentActivityId?: string
|
|
148
|
+
parentExecutionId?: string
|
|
149
|
+
goalContext?: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// PHASE 1.8: State Capture Utilities for Debugging-as-Activity
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Capture input state before task execution
|
|
158
|
+
*/
|
|
159
|
+
async function captureInputState(
|
|
160
|
+
workingDirectory: string,
|
|
161
|
+
impulseIds: string[],
|
|
162
|
+
variables: Record<string, unknown>
|
|
163
|
+
): Promise<{
|
|
164
|
+
filesAvailable: string[]
|
|
165
|
+
environment: Record<string, string>
|
|
166
|
+
impulses: string[]
|
|
167
|
+
variables: Record<string, unknown>
|
|
168
|
+
}> {
|
|
169
|
+
// List files in working directory (shallow, don't recurse into node_modules, etc.)
|
|
170
|
+
let filesAvailable: string[] = []
|
|
171
|
+
try {
|
|
172
|
+
const glob = new Bun.Glob("**/*")
|
|
173
|
+
const entries = await Array.fromAsync(glob.scan({ cwd: workingDirectory }))
|
|
174
|
+
filesAvailable = entries.filter(f => !f.includes("node_modules") && !f.startsWith(".") && !f.startsWith("dist"))
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn("[State Capture] Failed to list files:", error instanceof Error ? error.message : String(error))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Capture relevant environment variables (filter sensitive ones)
|
|
180
|
+
const environment: Record<string, string> = {}
|
|
181
|
+
const envVarsToCapture = [
|
|
182
|
+
"NODE_ENV",
|
|
183
|
+
"PATH",
|
|
184
|
+
"HOME",
|
|
185
|
+
"USER",
|
|
186
|
+
"PWD",
|
|
187
|
+
"SHELL",
|
|
188
|
+
// Add more as needed
|
|
189
|
+
]
|
|
190
|
+
for (const key of envVarsToCapture) {
|
|
191
|
+
const value = process.env[key]
|
|
192
|
+
if (value) {
|
|
193
|
+
environment[key] = value
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
filesAvailable,
|
|
199
|
+
environment,
|
|
200
|
+
impulses: impulseIds,
|
|
201
|
+
variables: JSON.parse(JSON.stringify(variables)), // Deep clone
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Capture output state after task execution
|
|
207
|
+
*/
|
|
208
|
+
async function captureOutputState(
|
|
209
|
+
workingDirectory: string,
|
|
210
|
+
beforeFiles: string[],
|
|
211
|
+
toolCalls: Array<{ toolName: string; params: any; result: ToolResult }>
|
|
212
|
+
): Promise<{
|
|
213
|
+
filesModified: string[]
|
|
214
|
+
filesCreated: string[]
|
|
215
|
+
filesDeleted: string[]
|
|
216
|
+
exitCode?: number
|
|
217
|
+
stderr?: string
|
|
218
|
+
}> {
|
|
219
|
+
// List files after execution
|
|
220
|
+
let afterFiles: string[] = []
|
|
221
|
+
try {
|
|
222
|
+
const glob = new Bun.Glob("**/*")
|
|
223
|
+
const entries = await Array.fromAsync(glob.scan({ cwd: workingDirectory }))
|
|
224
|
+
afterFiles = entries.filter(f => !f.includes("node_modules") && !f.startsWith(".") && !f.startsWith("dist"))
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.warn("[State Capture] Failed to list files after execution:", error instanceof Error ? error.message : String(error))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Detect created, modified, deleted files
|
|
230
|
+
const beforeSet = new Set(beforeFiles)
|
|
231
|
+
const afterSet = new Set(afterFiles)
|
|
232
|
+
|
|
233
|
+
const filesCreated = afterFiles.filter(f => !beforeSet.has(f))
|
|
234
|
+
const filesDeleted = beforeFiles.filter(f => !afterSet.has(f))
|
|
235
|
+
|
|
236
|
+
// Modified files: files that existed before and after (tool calls track actual modifications)
|
|
237
|
+
const filesModified: string[] = []
|
|
238
|
+
for (const call of toolCalls) {
|
|
239
|
+
if (call.toolName === "write" || call.toolName === "edit") {
|
|
240
|
+
const filePath = call.params.filePath as string
|
|
241
|
+
if (filePath && beforeSet.has(filePath)) {
|
|
242
|
+
filesModified.push(filePath)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Extract exit code and stderr from bash tool calls
|
|
248
|
+
let exitCode: number | undefined
|
|
249
|
+
let stderr: string | undefined
|
|
250
|
+
|
|
251
|
+
for (const call of toolCalls) {
|
|
252
|
+
if (call.toolName === "bash" && call.result.metadata) {
|
|
253
|
+
if (call.result.metadata.exitCode !== undefined) {
|
|
254
|
+
exitCode = call.result.metadata.exitCode as number
|
|
255
|
+
}
|
|
256
|
+
if (call.result.metadata.stderr) {
|
|
257
|
+
stderr = call.result.metadata.stderr as string
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
filesModified: Array.from(new Set(filesModified)), // Deduplicate
|
|
264
|
+
filesCreated,
|
|
265
|
+
filesDeleted,
|
|
266
|
+
exitCode,
|
|
267
|
+
stderr,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Capture lightweight file hashes for state transition tracking
|
|
273
|
+
*/
|
|
274
|
+
async function captureFileHashes(
|
|
275
|
+
workingDirectory: string,
|
|
276
|
+
files: string[]
|
|
277
|
+
): Promise<Record<string, string>> {
|
|
278
|
+
const hashes: Record<string, string> = {}
|
|
279
|
+
|
|
280
|
+
for (const file of files.slice(0, 50)) { // Limit to 50 files to avoid overhead
|
|
281
|
+
try {
|
|
282
|
+
const fullPath = `${workingDirectory}/${file}`
|
|
283
|
+
const fileContent = await safeReadFile(fullPath).text()
|
|
284
|
+
const hash = Bun.hash(fileContent).toString(16)
|
|
285
|
+
hashes[file] = hash
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// File might have been deleted or is not readable
|
|
288
|
+
hashes[file] = "ERROR"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return hashes
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Activity executor for running activity templates
|
|
297
|
+
*/
|
|
298
|
+
export class ActivityExecutor {
|
|
299
|
+
private llm: LLMClient
|
|
300
|
+
private toolHandlers: ReturnType<typeof createToolHandlers>
|
|
301
|
+
private config: ExecutorConfig
|
|
302
|
+
private customToolDefinitions: ToolDefinition[]
|
|
303
|
+
private currentActivityId?: string
|
|
304
|
+
private currentExecutionId?: string
|
|
305
|
+
private currentGoalContext?: string
|
|
306
|
+
private toolCallRecords: Array<{ toolName: string; params: any; result: ToolResult; timestamp: number }> = []
|
|
307
|
+
|
|
308
|
+
/** Public accessor for LLM client (needed by GoalProcessor for improvisation fallback) */
|
|
309
|
+
get llmClient(): LLMClient {
|
|
310
|
+
return this.llm
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
constructor(config: ExecutorConfig) {
|
|
314
|
+
this.config = config
|
|
315
|
+
this.llm = createLLMClient(config.provider, config.apiKey)
|
|
316
|
+
// Collect custom tool definitions for passing to getAllToolDefinitions later
|
|
317
|
+
this.customToolDefinitions = Object.values(config.customTools ?? {}).map(({ definition }) => definition)
|
|
318
|
+
this.toolHandlers = createToolHandlers({
|
|
319
|
+
workingDirectory: config.workingDirectory,
|
|
320
|
+
onActivityExecute: config.onActivityExecute ?? (async (templateId, variables, reason) => {
|
|
321
|
+
// Default implementation if no custom callback provided
|
|
322
|
+
// Get current call stack (or initialize)
|
|
323
|
+
const callStack = config.activityCallStack ?? []
|
|
324
|
+
|
|
325
|
+
// CYCLE DETECTION: Check if this activity is already in the call stack
|
|
326
|
+
if (callStack.includes(templateId)) {
|
|
327
|
+
const cycleChain = [...callStack, templateId].join(' → ')
|
|
328
|
+
console.error(`[Activity] Cycle detected: ${cycleChain}`)
|
|
329
|
+
return {
|
|
330
|
+
id: `blocked_cycle_${Date.now()}`,
|
|
331
|
+
templateId,
|
|
332
|
+
status: "failed",
|
|
333
|
+
error: `Cycle detected: ${cycleChain}`,
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// DEPTH CHECK: Verify we haven't exceeded maximum depth
|
|
338
|
+
if (callStack.length >= (config.maxNestingDepth ?? 3)) {
|
|
339
|
+
const depthChain = [...callStack, templateId].join(' → ')
|
|
340
|
+
console.warn(`[Activity] Max nesting depth reached: ${depthChain}`)
|
|
341
|
+
return {
|
|
342
|
+
id: `blocked_depth_${Date.now()}`,
|
|
343
|
+
templateId,
|
|
344
|
+
status: "failed",
|
|
345
|
+
error: `Maximum nesting depth reached (${callStack.length + 1} > ${config.maxNestingDepth ?? 3})`,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// CONTEXT ISOLATION: Nested activities execute with minimal context
|
|
350
|
+
// to prevent token accumulation across nesting levels
|
|
351
|
+
const template = await loadTemplateFromMCPOrLocal(templateId)
|
|
352
|
+
|
|
353
|
+
// Create isolated config - only essential settings, no accumulated state
|
|
354
|
+
const isolatedConfig: ExecutorConfig = {
|
|
355
|
+
workingDirectory: config.workingDirectory,
|
|
356
|
+
model: config.model,
|
|
357
|
+
provider: config.provider,
|
|
358
|
+
apiKey: config.apiKey,
|
|
359
|
+
systemPrompt: config.systemPrompt,
|
|
360
|
+
// Don't inherit parent's custom tools that might have accumulated state
|
|
361
|
+
customTools: {},
|
|
362
|
+
// Wire callbacks for further nesting (but depth-limited)
|
|
363
|
+
onSearchActivities: config.onSearchActivities,
|
|
364
|
+
onCreateActivity: config.onCreateActivity,
|
|
365
|
+
onActivityExecute: config.onActivityExecute, // Pass observer callback for composition tracking
|
|
366
|
+
// IMPORTANT: Pass updated call stack for cycle detection
|
|
367
|
+
activityCallStack: [...callStack, templateId],
|
|
368
|
+
// Adjust max depth based on current depth
|
|
369
|
+
maxNestingDepth: (config.maxNestingDepth ?? 3) - callStack.length,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const nestedExecutor = new ActivityExecutor(isolatedConfig)
|
|
373
|
+
const result = await nestedExecutor.execute({
|
|
374
|
+
template,
|
|
375
|
+
// Only pass essential variables, not accumulated context
|
|
376
|
+
variables: variables ?? {},
|
|
377
|
+
reason,
|
|
378
|
+
parentActivityId: this.currentActivityId,
|
|
379
|
+
parentExecutionId: this.currentExecutionId,
|
|
380
|
+
// Only pass a brief summary of goal context, not full accumulated context
|
|
381
|
+
goalContext: reason ? `Parent goal: ${reason.substring(0, 200)}` : undefined,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Record composition if MCP is enabled
|
|
385
|
+
if (isMCPEnabled() && this.currentActivityId && this.currentExecutionId) {
|
|
386
|
+
const mcp = getMCPClient()
|
|
387
|
+
if (mcp) {
|
|
388
|
+
await mcp.recordComposition({
|
|
389
|
+
parentActivityId: this.currentActivityId,
|
|
390
|
+
childActivityId: template.id,
|
|
391
|
+
executionId: this.currentExecutionId,
|
|
392
|
+
goalContext: reason ? reason.substring(0, 200) : undefined,
|
|
393
|
+
success: result.status === "completed",
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Return summarized result to prevent full trace accumulation
|
|
399
|
+
return {
|
|
400
|
+
id: result.id,
|
|
401
|
+
templateId: result.templateId,
|
|
402
|
+
status: result.status,
|
|
403
|
+
summary: result.status === "completed"
|
|
404
|
+
? `Activity "${templateId}" completed successfully`
|
|
405
|
+
: `Activity "${templateId}" failed: ${result.error?.substring(0, 200) ?? "unknown error"}`,
|
|
406
|
+
}
|
|
407
|
+
}),
|
|
408
|
+
onSearchActivities: config.onSearchActivities, // Wire activity search callback
|
|
409
|
+
onCreateActivity: config.onCreateActivity, // Wire activity creation callback
|
|
410
|
+
onImpulseCreate: (impulse) => {
|
|
411
|
+
// Create impulse
|
|
412
|
+
createImpulse({
|
|
413
|
+
id: impulse.id,
|
|
414
|
+
pointer: impulse.type === "memo" && impulse.content
|
|
415
|
+
? { type: "memo", content: impulse.content }
|
|
416
|
+
: impulse.type === "file" && impulse.content
|
|
417
|
+
? { type: "file", path: impulse.content }
|
|
418
|
+
: { type: "custom", resolver: impulse.type, data: {} },
|
|
419
|
+
budget: impulse.budget,
|
|
420
|
+
priority: impulse.priority as "critical" | "high" | "medium" | "low",
|
|
421
|
+
})
|
|
422
|
+
},
|
|
423
|
+
customTools: config.customTools,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Execute an activity template
|
|
429
|
+
*/
|
|
430
|
+
async execute(options: ExecuteOptions): Promise<ActivityExecution> {
|
|
431
|
+
const { template, variables, reason, impulses, onTaskStart, onTaskComplete, parentActivityId, parentExecutionId, goalContext } = options
|
|
432
|
+
const activityId = `act_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
433
|
+
|
|
434
|
+
// Store context for composition tracking
|
|
435
|
+
this.currentActivityId = template.id
|
|
436
|
+
this.currentExecutionId = activityId
|
|
437
|
+
this.currentGoalContext = goalContext || reason
|
|
438
|
+
|
|
439
|
+
const execution: ActivityExecution = {
|
|
440
|
+
id: activityId,
|
|
441
|
+
templateId: template.id,
|
|
442
|
+
status: "executing",
|
|
443
|
+
variables,
|
|
444
|
+
impulses: impulses || [], // Use provided impulses or empty array
|
|
445
|
+
taskResults: [],
|
|
446
|
+
startedAt: Date.now(),
|
|
447
|
+
// Initialize execution trace for debugging and ribosome pattern
|
|
448
|
+
executionTrace: {
|
|
449
|
+
tasks: [],
|
|
450
|
+
impulsesCreated: [],
|
|
451
|
+
filesModified: [],
|
|
452
|
+
goalContext: goalContext ? {
|
|
453
|
+
goal: goalContext,
|
|
454
|
+
intent: reason || "",
|
|
455
|
+
context: variables
|
|
456
|
+
} : undefined,
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log(`[Activity] Starting: ${template.name} (${activityId})`)
|
|
461
|
+
if (reason) {
|
|
462
|
+
console.log(`[Activity] Reason: ${reason}`)
|
|
463
|
+
}
|
|
464
|
+
if (parentActivityId) {
|
|
465
|
+
console.log(`[Activity] Parent: ${parentActivityId}`)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Register template to backend if MCP is enabled (ensure variant tracking)
|
|
469
|
+
if (isMCPEnabled()) {
|
|
470
|
+
const mcp = getMCPClient()
|
|
471
|
+
if (mcp) {
|
|
472
|
+
console.log(`[Activity] Registering template variant: ${template.id}`)
|
|
473
|
+
await mcp.registerTemplate(template)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
// Step 1: Create impulses from context requirements
|
|
479
|
+
const impulses = await this.createImpulsesFromRequirements(activityId, template, variables)
|
|
480
|
+
execution.impulses = impulses
|
|
481
|
+
|
|
482
|
+
// Step 2: Sort tasks by dependencies
|
|
483
|
+
const sortedTasks = this.topologicalSort(template.tasks)
|
|
484
|
+
|
|
485
|
+
// Step 3: Execute tasks in order
|
|
486
|
+
let totalInputTokens = 0
|
|
487
|
+
let totalOutputTokens = 0
|
|
488
|
+
|
|
489
|
+
for (const task of sortedTasks) {
|
|
490
|
+
onTaskStart?.(task.id)
|
|
491
|
+
console.log(`[Task] Executing: ${task.id} - ${task.description}`)
|
|
492
|
+
|
|
493
|
+
const result = await this.executeTask(activityId, task, variables, impulses, undefined, template.id)
|
|
494
|
+
execution.taskResults.push(result)
|
|
495
|
+
|
|
496
|
+
// Add executed task to trace for dashboard display
|
|
497
|
+
if (execution.executionTrace) {
|
|
498
|
+
const executedTask: ExecutedTask = {
|
|
499
|
+
id: task.id,
|
|
500
|
+
description: task.description,
|
|
501
|
+
actualPrompt: result.metadata?.actualPrompt || "",
|
|
502
|
+
toolCalls: result.metadata?.toolCalls || [],
|
|
503
|
+
response: result.output || "",
|
|
504
|
+
result: {
|
|
505
|
+
status: result.status === "completed" ? "success" :
|
|
506
|
+
result.status === "failed" ? "failure" : "partial",
|
|
507
|
+
error: result.error,
|
|
508
|
+
metadata: result.metadata
|
|
509
|
+
},
|
|
510
|
+
inputState: result.metadata?.inputState,
|
|
511
|
+
outputState: result.metadata?.outputState,
|
|
512
|
+
stateTransition: result.metadata?.stateTransition,
|
|
513
|
+
}
|
|
514
|
+
execution.executionTrace.tasks.push(executedTask)
|
|
515
|
+
|
|
516
|
+
// Track files modified across all tasks
|
|
517
|
+
if (result.metadata?.outputState?.filesModified) {
|
|
518
|
+
for (const file of result.metadata.outputState.filesModified) {
|
|
519
|
+
if (!execution.executionTrace.filesModified.includes(file)) {
|
|
520
|
+
execution.executionTrace.filesModified.push(file)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (result.metadata?.outputState?.filesCreated) {
|
|
525
|
+
for (const file of result.metadata.outputState.filesCreated) {
|
|
526
|
+
if (!execution.executionTrace.filesModified.includes(file)) {
|
|
527
|
+
execution.executionTrace.filesModified.push(file)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (result.tokens) {
|
|
534
|
+
totalInputTokens += result.tokens.input
|
|
535
|
+
totalOutputTokens += result.tokens.output
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Create output impulses if task succeeded
|
|
539
|
+
if (result.status === "completed" && task.outputImpulses && task.outputImpulses.length > 0) {
|
|
540
|
+
for (const impulseId of task.outputImpulses) {
|
|
541
|
+
try {
|
|
542
|
+
createImpulse({
|
|
543
|
+
id: impulseId,
|
|
544
|
+
pointer: {
|
|
545
|
+
type: "memo",
|
|
546
|
+
content: result.output || ""
|
|
547
|
+
},
|
|
548
|
+
budget: 5000,
|
|
549
|
+
priority: "medium"
|
|
550
|
+
})
|
|
551
|
+
console.log(`[Activity] Created output impulse: ${impulseId}`)
|
|
552
|
+
} catch (error) {
|
|
553
|
+
console.error(`[Activity] Failed to create impulse ${impulseId}:`, error instanceof Error ? error.message : String(error))
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
onTaskComplete?.(task.id, result)
|
|
559
|
+
|
|
560
|
+
if (result.status === "failed") {
|
|
561
|
+
console.error(`[Task] Failed: ${task.id} - ${result.error}`)
|
|
562
|
+
|
|
563
|
+
// Check retry policy
|
|
564
|
+
if (task.retry && task.retry.maxAttempts > 1) {
|
|
565
|
+
let retryCount = 1
|
|
566
|
+
while (retryCount < task.retry.maxAttempts && result.status === "failed") {
|
|
567
|
+
console.log(`[Task] Retrying ${task.id} (attempt ${retryCount + 1}/${task.retry.maxAttempts})`)
|
|
568
|
+
const retryResult = await this.executeTask(activityId, task, variables, impulses, result.error, template.id)
|
|
569
|
+
execution.taskResults[execution.taskResults.length - 1] = retryResult
|
|
570
|
+
|
|
571
|
+
// Update executed task in trace with retry result
|
|
572
|
+
if (execution.executionTrace && execution.executionTrace.tasks.length > 0) {
|
|
573
|
+
const executedTask: ExecutedTask = {
|
|
574
|
+
id: task.id,
|
|
575
|
+
description: task.description,
|
|
576
|
+
actualPrompt: retryResult.metadata?.actualPrompt || "",
|
|
577
|
+
toolCalls: retryResult.metadata?.toolCalls || [],
|
|
578
|
+
response: retryResult.output || "",
|
|
579
|
+
result: {
|
|
580
|
+
status: retryResult.status === "completed" ? "success" :
|
|
581
|
+
retryResult.status === "failed" ? "failure" : "partial",
|
|
582
|
+
error: retryResult.error,
|
|
583
|
+
metadata: retryResult.metadata
|
|
584
|
+
},
|
|
585
|
+
inputState: retryResult.metadata?.inputState,
|
|
586
|
+
outputState: retryResult.metadata?.outputState,
|
|
587
|
+
stateTransition: retryResult.metadata?.stateTransition,
|
|
588
|
+
}
|
|
589
|
+
execution.executionTrace.tasks[execution.executionTrace.tasks.length - 1] = executedTask
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (retryResult.tokens) {
|
|
593
|
+
totalInputTokens += retryResult.tokens.input
|
|
594
|
+
totalOutputTokens += retryResult.tokens.output
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (retryResult.status === "completed") break
|
|
598
|
+
retryCount++
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// If still failed after retries, abort
|
|
603
|
+
if (execution.taskResults[execution.taskResults.length - 1]?.status === "failed") {
|
|
604
|
+
execution.status = "failed"
|
|
605
|
+
break
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Step 4: Complete execution
|
|
611
|
+
execution.completedAt = Date.now()
|
|
612
|
+
if (execution.status !== "failed") {
|
|
613
|
+
execution.status = "completed"
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
execution.metrics = {
|
|
617
|
+
duration: execution.completedAt - execution.startedAt,
|
|
618
|
+
cost: this.estimateCost(totalInputTokens, totalOutputTokens),
|
|
619
|
+
totalTokens: { input: totalInputTokens, output: totalOutputTokens },
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
console.log(`[Activity] Completed: ${execution.status} in ${execution.metrics.duration}ms`)
|
|
623
|
+
|
|
624
|
+
// Report execution to MCP backend if enabled
|
|
625
|
+
if (isMCPEnabled()) {
|
|
626
|
+
const mcp = getMCPClient()
|
|
627
|
+
if (mcp) {
|
|
628
|
+
console.log(`[Activity] Reporting execution to MCP backend...`)
|
|
629
|
+
const reported = await mcp.reportExecution(execution)
|
|
630
|
+
if (reported) {
|
|
631
|
+
console.log(`[Activity] ✓ Execution reported to backend`)
|
|
632
|
+
} else {
|
|
633
|
+
console.warn(`[Activity] ⚠ Failed to report execution to backend`)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Store execution trace for debugging-as-activity
|
|
637
|
+
if (execution.executionTrace) {
|
|
638
|
+
console.log(`[Activity] Storing execution trace...`)
|
|
639
|
+
const traceStored = await mcp.storeExecutionTrace(execution)
|
|
640
|
+
if (traceStored) {
|
|
641
|
+
console.log(`[Activity] ✓ Execution trace stored: ${execution.id}`)
|
|
642
|
+
} else {
|
|
643
|
+
console.warn(`[Activity] ⚠ Failed to store execution trace`)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Report tool usage patterns
|
|
648
|
+
if (this.toolCallRecords.length > 0) {
|
|
649
|
+
console.log(`[Activity] Reporting ${this.toolCallRecords.length} tool usage records...`)
|
|
650
|
+
const activitySucceeded = execution.status === "completed"
|
|
651
|
+
|
|
652
|
+
for (const record of this.toolCallRecords) {
|
|
653
|
+
await mcp.recordToolUsage({
|
|
654
|
+
toolName: record.toolName,
|
|
655
|
+
activityVariantId: activityId,
|
|
656
|
+
taskId: undefined, // Could be enhanced to track per-task
|
|
657
|
+
executionId: execution.id,
|
|
658
|
+
toolSucceeded: record.result.success,
|
|
659
|
+
activitySucceeded,
|
|
660
|
+
paramsComplexity: record.params ? JSON.stringify(record.params).length : 0,
|
|
661
|
+
}).catch((error) => {
|
|
662
|
+
console.warn(`[Activity] ⚠ Failed to report tool usage for ${record.toolName}:`, error)
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
console.log(`[Activity] ✓ Tool usage patterns reported`)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// RIBOSOME: Extract reusable template from successful execution
|
|
670
|
+
// Only extract if:
|
|
671
|
+
// 1. Execution was successful
|
|
672
|
+
// 2. Has execution trace (detailed task history)
|
|
673
|
+
// 3. Not already a ribosome-generated template (avoid infinite loop)
|
|
674
|
+
if (
|
|
675
|
+
execution.status === "completed" &&
|
|
676
|
+
execution.executionTrace &&
|
|
677
|
+
template.metadata?.author !== "ribosome"
|
|
678
|
+
) {
|
|
679
|
+
try {
|
|
680
|
+
console.log(`[Ribosome] Extracting template from successful execution...`)
|
|
681
|
+
|
|
682
|
+
// Infer category from original template or default to "tool"
|
|
683
|
+
const category = template.category || "tool"
|
|
684
|
+
|
|
685
|
+
// Generate a descriptive name from the goal or template name
|
|
686
|
+
const goalDescription = execution.executionTrace.goalContext?.goal || template.name || "unnamed"
|
|
687
|
+
const shortGoal = goalDescription.substring(0, 50).replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()
|
|
688
|
+
const templateName = `learned-${shortGoal}-${Date.now().toString(36)}`
|
|
689
|
+
|
|
690
|
+
// Call ribosome to extract template
|
|
691
|
+
const extractedTemplate = assembleTemplateFromExecution(
|
|
692
|
+
execution,
|
|
693
|
+
templateName,
|
|
694
|
+
category as "feature" | "bugfix" | "refactor" | "tool" | "infrastructure"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
console.log(`[Ribosome] Template extracted: ${extractedTemplate.name}`)
|
|
698
|
+
|
|
699
|
+
// Register with backend for Thompson Sampling
|
|
700
|
+
const registered = await mcp.registerTemplate(extractedTemplate)
|
|
701
|
+
if (registered) {
|
|
702
|
+
console.log(`[Ribosome] ✓ Template registered: ${extractedTemplate.id}`)
|
|
703
|
+
|
|
704
|
+
// Enqueue for future testing via boredom queue (low priority)
|
|
705
|
+
try {
|
|
706
|
+
const enqueueResponse = await fetch(`${(mcp as any).endpoint}/v2/activities/boredom/enqueue`, {
|
|
707
|
+
method: "POST",
|
|
708
|
+
headers: { "Content-Type": "application/json" },
|
|
709
|
+
body: JSON.stringify({
|
|
710
|
+
templateId: extractedTemplate.id,
|
|
711
|
+
priority: "low",
|
|
712
|
+
reason: `Ribosome-extracted from ${execution.id}`,
|
|
713
|
+
variables: {},
|
|
714
|
+
}),
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
if (enqueueResponse.ok) {
|
|
718
|
+
console.log(`[Ribosome] ✓ Template enqueued for testing`)
|
|
719
|
+
}
|
|
720
|
+
} catch (enqueueError) {
|
|
721
|
+
// Non-fatal: template is registered, just not queued for immediate testing
|
|
722
|
+
console.log(`[Ribosome] Template registered but not enqueued for testing`)
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
console.log(`[Ribosome] Template already exists or registration failed`)
|
|
726
|
+
}
|
|
727
|
+
} catch (ribosomeError) {
|
|
728
|
+
// Non-fatal: execution succeeded, ribosome extraction is bonus
|
|
729
|
+
console.log(`[Ribosome] Could not extract template:`, ribosomeError)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
} catch (error) {
|
|
736
|
+
execution.status = "failed"
|
|
737
|
+
execution.completedAt = Date.now()
|
|
738
|
+
console.error(`[Activity] Error:`, error)
|
|
739
|
+
|
|
740
|
+
// Still try to report failure to backend
|
|
741
|
+
if (isMCPEnabled()) {
|
|
742
|
+
const mcp = getMCPClient()
|
|
743
|
+
if (mcp) {
|
|
744
|
+
await mcp.reportExecution(execution).catch(() => {
|
|
745
|
+
// Ignore reporting errors on failure
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return execution
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Get current executor state for TUI display
|
|
756
|
+
* Returns real-time information about what MiniBob is doing
|
|
757
|
+
*/
|
|
758
|
+
getState(): {
|
|
759
|
+
currentActivityId: string | undefined
|
|
760
|
+
currentExecutionId: string | undefined
|
|
761
|
+
currentGoalContext: string | undefined
|
|
762
|
+
toolCallRecords: Array<{ toolName: string; params: any; result: ToolResult; timestamp: number }>
|
|
763
|
+
workingDirectory: string
|
|
764
|
+
} {
|
|
765
|
+
return {
|
|
766
|
+
currentActivityId: this.currentActivityId,
|
|
767
|
+
currentExecutionId: this.currentExecutionId,
|
|
768
|
+
currentGoalContext: this.currentGoalContext,
|
|
769
|
+
toolCallRecords: this.toolCallRecords.slice(-10), // Last 10 tool calls
|
|
770
|
+
workingDirectory: this.config.workingDirectory,
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Create impulses from template context requirements
|
|
776
|
+
*/
|
|
777
|
+
private async createImpulsesFromRequirements(
|
|
778
|
+
activityId: string,
|
|
779
|
+
template: ActivityTemplate,
|
|
780
|
+
variables: Record<string, unknown>
|
|
781
|
+
): Promise<Impulse[]> {
|
|
782
|
+
const impulses: Impulse[] = []
|
|
783
|
+
|
|
784
|
+
if (!template.contextRequirements) {
|
|
785
|
+
return impulses
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
for (const req of template.contextRequirements) {
|
|
789
|
+
// Interpolate source with variables
|
|
790
|
+
const source = this.interpolate(req.source, variables)
|
|
791
|
+
|
|
792
|
+
let impulse: Impulse
|
|
793
|
+
|
|
794
|
+
switch (req.type) {
|
|
795
|
+
case "file":
|
|
796
|
+
impulse = createImpulse({
|
|
797
|
+
id: req.id,
|
|
798
|
+
pointer: { type: "file", path: source },
|
|
799
|
+
budget: req.budget,
|
|
800
|
+
priority: req.priority,
|
|
801
|
+
})
|
|
802
|
+
break
|
|
803
|
+
|
|
804
|
+
case "glob":
|
|
805
|
+
// Resolve glob to files and create impulses for each
|
|
806
|
+
// For simplicity, create a single impulse that will list files
|
|
807
|
+
impulse = createImpulse({
|
|
808
|
+
id: req.id,
|
|
809
|
+
pointer: {
|
|
810
|
+
type: "custom",
|
|
811
|
+
resolver: "glob",
|
|
812
|
+
data: { pattern: source, cwd: this.config.workingDirectory },
|
|
813
|
+
},
|
|
814
|
+
budget: req.budget,
|
|
815
|
+
priority: req.priority,
|
|
816
|
+
})
|
|
817
|
+
break
|
|
818
|
+
|
|
819
|
+
case "memo":
|
|
820
|
+
impulse = createImpulse({
|
|
821
|
+
id: req.id,
|
|
822
|
+
pointer: { type: "memo", content: source },
|
|
823
|
+
budget: req.budget,
|
|
824
|
+
priority: req.priority,
|
|
825
|
+
})
|
|
826
|
+
break
|
|
827
|
+
|
|
828
|
+
default:
|
|
829
|
+
impulse = createImpulse({
|
|
830
|
+
id: req.id,
|
|
831
|
+
pointer: {
|
|
832
|
+
type: "custom",
|
|
833
|
+
resolver: req.type,
|
|
834
|
+
data: { source },
|
|
835
|
+
},
|
|
836
|
+
budget: req.budget,
|
|
837
|
+
priority: req.priority,
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
impulses.push(impulse)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Register glob resolver if not already registered
|
|
845
|
+
getImpulseStore().registerResolver("glob", async (data) => {
|
|
846
|
+
const pattern = data.pattern as string
|
|
847
|
+
const cwd = (data.cwd as string) ?? this.config.workingDirectory
|
|
848
|
+
const glob = new Bun.Glob(pattern)
|
|
849
|
+
const files: string[] = []
|
|
850
|
+
for await (const file of glob.scan({ cwd })) {
|
|
851
|
+
files.push(file)
|
|
852
|
+
if (files.length >= 50) break
|
|
853
|
+
}
|
|
854
|
+
return files.join("\n")
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
return impulses
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Execute a single task
|
|
862
|
+
*/
|
|
863
|
+
private async executeTask(
|
|
864
|
+
activityId: string,
|
|
865
|
+
task: ActivityTask,
|
|
866
|
+
variables: Record<string, unknown>,
|
|
867
|
+
impulses: Impulse[],
|
|
868
|
+
lastError?: string,
|
|
869
|
+
templateId?: string
|
|
870
|
+
): Promise<TaskResult> {
|
|
871
|
+
const startedAt = Date.now()
|
|
872
|
+
|
|
873
|
+
// Phase 1.8: Declare variables outside try block for catch block access
|
|
874
|
+
const taskImpulseIds = task.impulseReferences ?? impulses.map((i) => i.id)
|
|
875
|
+
let impulsesToLoad = taskImpulseIds
|
|
876
|
+
|
|
877
|
+
// Phase 1.8+: Declare state tracking variables outside try block
|
|
878
|
+
let inputState: any
|
|
879
|
+
let beforeHashes: Record<string, string> = {}
|
|
880
|
+
let outputState: any
|
|
881
|
+
let afterHashes: Record<string, string> = {}
|
|
882
|
+
let stateTransition: any
|
|
883
|
+
let prompt = ""
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
// Phase 1.8: Intelligent impulse filtering
|
|
887
|
+
let loadedImpulses: Impulse[]
|
|
888
|
+
let filteringSummary: ReturnType<typeof generateFilteringSummary> | undefined
|
|
889
|
+
|
|
890
|
+
// Query relevance metrics if MCP is enabled
|
|
891
|
+
if (isMCPEnabled() && templateId && taskImpulseIds.length > 0) {
|
|
892
|
+
const mcp = getMCPClient()
|
|
893
|
+
if (mcp) {
|
|
894
|
+
try {
|
|
895
|
+
// Query relevance metrics from backend
|
|
896
|
+
const metrics = await mcp.queryImpulseRelevance({
|
|
897
|
+
activityVariantId: templateId,
|
|
898
|
+
impulseIds: taskImpulseIds,
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
// Filter impulses based on learned relevance
|
|
902
|
+
const filterResult = filterImpulsesByRelevance(taskImpulseIds, metrics)
|
|
903
|
+
impulsesToLoad = filterResult.toLoad
|
|
904
|
+
|
|
905
|
+
// Calculate and log savings
|
|
906
|
+
if (filterResult.toSkip.length > 0) {
|
|
907
|
+
const impulseStore = getImpulseStore()
|
|
908
|
+
const tokenSizes = new Map<string, number>()
|
|
909
|
+
|
|
910
|
+
for (const impulseId of taskImpulseIds) {
|
|
911
|
+
const impulse = impulseStore.get(impulseId)
|
|
912
|
+
if (impulse) {
|
|
913
|
+
tokenSizes.set(impulseId, estimateImpulseTokens(impulse))
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const savings = calculateSavings(filterResult.toSkip, tokenSizes)
|
|
918
|
+
filteringSummary = generateFilteringSummary(filterResult, savings)
|
|
919
|
+
|
|
920
|
+
console.log(`[Impulse Filter] Task ${task.id}:`)
|
|
921
|
+
console.log(` - Original: ${taskImpulseIds.length} impulses`)
|
|
922
|
+
console.log(` - Loaded: ${filterResult.toLoad.length} impulses`)
|
|
923
|
+
console.log(` - Skipped: ${filterResult.toSkip.length} impulses`)
|
|
924
|
+
console.log(` - Saved: ~${savings.tokensSaved} tokens (~$${savings.costSaved.toFixed(4)})`)
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
// Fallback to loading all impulses if filtering fails
|
|
928
|
+
console.warn(`[Impulse Filter] Failed to filter impulses, loading all:`, error instanceof Error ? error.message : String(error))
|
|
929
|
+
impulsesToLoad = taskImpulseIds
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Load filtered impulses
|
|
935
|
+
loadedImpulses = await loadImpulses(impulsesToLoad)
|
|
936
|
+
|
|
937
|
+
// Phase 1.8: Capture input state BEFORE task execution
|
|
938
|
+
inputState = await captureInputState(
|
|
939
|
+
this.config.workingDirectory,
|
|
940
|
+
impulsesToLoad,
|
|
941
|
+
variables
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
// Capture file hashes for state transition tracking
|
|
945
|
+
beforeHashes = await captureFileHashes(
|
|
946
|
+
this.config.workingDirectory,
|
|
947
|
+
inputState.filesAvailable
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
// Build prompt with impulse substitution first
|
|
951
|
+
prompt = await substituteImpulses(task.prompt.template, impulsesToLoad)
|
|
952
|
+
prompt = this.interpolate(prompt, variables)
|
|
953
|
+
|
|
954
|
+
// Add impulse context
|
|
955
|
+
const impulseContext = formatImpulsesForContext(loadedImpulses)
|
|
956
|
+
if (impulseContext) {
|
|
957
|
+
prompt = `${impulseContext}\n\n${prompt}`
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Add error context if retrying
|
|
961
|
+
if (lastError) {
|
|
962
|
+
prompt = `Previous attempt failed with error:\n${lastError}\n\nPlease try again, addressing the error.\n\n${prompt}`
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Build messages
|
|
966
|
+
const messages: Message[] = [
|
|
967
|
+
{
|
|
968
|
+
role: "system",
|
|
969
|
+
content: this.config.systemPrompt ?? this.getDefaultSystemPrompt(),
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
role: "user",
|
|
973
|
+
content: prompt,
|
|
974
|
+
},
|
|
975
|
+
]
|
|
976
|
+
|
|
977
|
+
// Clear previous tool call records for this task
|
|
978
|
+
this.toolCallRecords = []
|
|
979
|
+
|
|
980
|
+
// Wrap tool handlers to capture tool execution data
|
|
981
|
+
const wrappedHandlers: Record<string, ToolHandler> = {}
|
|
982
|
+
for (const [toolName, handler] of Object.entries(this.toolHandlers)) {
|
|
983
|
+
wrappedHandlers[toolName] = async (params: any): Promise<ToolResult> => {
|
|
984
|
+
const toolResult = await handler(params)
|
|
985
|
+
|
|
986
|
+
// Record tool call for impulse creation
|
|
987
|
+
this.toolCallRecords.push({
|
|
988
|
+
toolName,
|
|
989
|
+
params,
|
|
990
|
+
result: toolResult,
|
|
991
|
+
timestamp: Date.now(),
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
return toolResult
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Execute with tool calling (using wrapped handlers)
|
|
999
|
+
const result = await this.llm.completeWithTools(
|
|
1000
|
+
{
|
|
1001
|
+
model: this.config.model,
|
|
1002
|
+
messages,
|
|
1003
|
+
tools: getAllToolDefinitions(this.customToolDefinitions),
|
|
1004
|
+
maxTokens: task.prompt.maxTokens ?? 4096,
|
|
1005
|
+
},
|
|
1006
|
+
wrappedHandlers
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
// Create impulses from tool calls
|
|
1010
|
+
for (const record of this.toolCallRecords) {
|
|
1011
|
+
if (record.result.success && record.result.output) {
|
|
1012
|
+
const impulseId = `tool:${record.toolName}:${task.id}:${record.timestamp}`
|
|
1013
|
+
|
|
1014
|
+
createImpulse({
|
|
1015
|
+
id: impulseId,
|
|
1016
|
+
pointer: {
|
|
1017
|
+
type: "memo", // Store tool output as memo
|
|
1018
|
+
content: record.result.output,
|
|
1019
|
+
},
|
|
1020
|
+
budget: Math.min(Math.ceil(record.result.output.length / 4), 2000), // Estimate tokens
|
|
1021
|
+
priority: "medium",
|
|
1022
|
+
tags: [`tool:${record.toolName}`, `activity:${activityId}`, `task:${task.id}`],
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
console.log(`[Impulse] Created from tool call: ${impulseId}`)
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Store output for potential downstream tasks
|
|
1030
|
+
storeActivityOutput(activityId, task.id, result.content)
|
|
1031
|
+
|
|
1032
|
+
// Phase 1.8: Capture output state AFTER task execution
|
|
1033
|
+
outputState = await captureOutputState(
|
|
1034
|
+
this.config.workingDirectory,
|
|
1035
|
+
inputState.filesAvailable,
|
|
1036
|
+
this.toolCallRecords
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
// Capture file hashes for state transition tracking
|
|
1040
|
+
afterHashes = await captureFileHashes(
|
|
1041
|
+
this.config.workingDirectory,
|
|
1042
|
+
outputState.filesCreated.concat(outputState.filesModified)
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
stateTransition = {
|
|
1046
|
+
before: beforeHashes,
|
|
1047
|
+
after: afterHashes,
|
|
1048
|
+
workingDirectory: this.config.workingDirectory,
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Run validation if specified
|
|
1052
|
+
if (task.validation) {
|
|
1053
|
+
const validationResult = await this.runValidation(task.validation, result.content)
|
|
1054
|
+
if (!validationResult.success) {
|
|
1055
|
+
// Phase 1.8: Record impulse relevance (validation failed)
|
|
1056
|
+
await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, false)
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
taskId: task.id,
|
|
1060
|
+
status: "failed",
|
|
1061
|
+
output: result.content,
|
|
1062
|
+
error: `Validation failed: ${validationResult.error}`,
|
|
1063
|
+
startedAt,
|
|
1064
|
+
completedAt: Date.now(),
|
|
1065
|
+
tokens: { input: result.usage.inputTokens, output: result.usage.outputTokens },
|
|
1066
|
+
// Phase 1.8+: Include state even for validation failures (helps debugging)
|
|
1067
|
+
metadata: {
|
|
1068
|
+
inputState,
|
|
1069
|
+
outputState,
|
|
1070
|
+
stateTransition,
|
|
1071
|
+
toolCalls: this.toolCallRecords.map((r, index) => ({
|
|
1072
|
+
id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
|
|
1073
|
+
name: r.toolName,
|
|
1074
|
+
arguments: r.params,
|
|
1075
|
+
result: {
|
|
1076
|
+
success: r.result.success,
|
|
1077
|
+
output: r.result.output,
|
|
1078
|
+
error: r.result.error,
|
|
1079
|
+
metadata: r.result.metadata
|
|
1080
|
+
}
|
|
1081
|
+
})),
|
|
1082
|
+
actualPrompt: prompt,
|
|
1083
|
+
},
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Phase 1.8: Record impulse relevance (success)
|
|
1089
|
+
await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, true)
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
taskId: task.id,
|
|
1093
|
+
status: "completed",
|
|
1094
|
+
output: result.content,
|
|
1095
|
+
startedAt,
|
|
1096
|
+
completedAt: Date.now(),
|
|
1097
|
+
tokens: { input: result.usage.inputTokens, output: result.usage.outputTokens },
|
|
1098
|
+
// Phase 1.8+: Include state tracking for ribosome
|
|
1099
|
+
metadata: {
|
|
1100
|
+
inputState,
|
|
1101
|
+
outputState,
|
|
1102
|
+
stateTransition,
|
|
1103
|
+
toolCalls: this.toolCallRecords.map((r, index) => ({
|
|
1104
|
+
id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
|
|
1105
|
+
name: r.toolName,
|
|
1106
|
+
arguments: r.params,
|
|
1107
|
+
result: {
|
|
1108
|
+
success: r.result.success,
|
|
1109
|
+
output: r.result.output,
|
|
1110
|
+
error: r.result.error,
|
|
1111
|
+
metadata: r.result.metadata
|
|
1112
|
+
}
|
|
1113
|
+
})),
|
|
1114
|
+
actualPrompt: prompt,
|
|
1115
|
+
},
|
|
1116
|
+
}
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
// Phase 1.8: Record impulse relevance (execution failed)
|
|
1119
|
+
await this.recordImpulseRelevance(templateId, taskImpulseIds, impulsesToLoad, false)
|
|
1120
|
+
|
|
1121
|
+
// Phase 1.8+: Try to capture output state even on error
|
|
1122
|
+
let errorOutputState
|
|
1123
|
+
let errorAfterHashes
|
|
1124
|
+
try {
|
|
1125
|
+
// Only capture state if inputState was initialized (error occurred after state capture)
|
|
1126
|
+
if (inputState && inputState.filesAvailable) {
|
|
1127
|
+
errorOutputState = await captureOutputState(
|
|
1128
|
+
this.config.workingDirectory,
|
|
1129
|
+
inputState.filesAvailable,
|
|
1130
|
+
this.toolCallRecords
|
|
1131
|
+
)
|
|
1132
|
+
errorAfterHashes = await captureFileHashes(
|
|
1133
|
+
this.config.workingDirectory,
|
|
1134
|
+
errorOutputState.filesCreated.concat(errorOutputState.filesModified)
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
} catch (stateError) {
|
|
1138
|
+
// State capture failed - not critical, continue
|
|
1139
|
+
console.warn(`[State Capture] Failed during error handling:`, stateError)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return {
|
|
1143
|
+
taskId: task.id,
|
|
1144
|
+
status: "failed",
|
|
1145
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1146
|
+
startedAt,
|
|
1147
|
+
completedAt: Date.now(),
|
|
1148
|
+
// Phase 1.8+: Include partial state (very helpful for debugging failures)
|
|
1149
|
+
metadata: errorOutputState ? {
|
|
1150
|
+
inputState,
|
|
1151
|
+
outputState: errorOutputState,
|
|
1152
|
+
stateTransition: {
|
|
1153
|
+
before: beforeHashes,
|
|
1154
|
+
after: errorAfterHashes || {},
|
|
1155
|
+
workingDirectory: this.config.workingDirectory,
|
|
1156
|
+
},
|
|
1157
|
+
toolCalls: this.toolCallRecords.map((r, index) => ({
|
|
1158
|
+
id: `tool:${r.toolName}:${task.id}:${r.timestamp}`,
|
|
1159
|
+
name: r.toolName,
|
|
1160
|
+
arguments: r.params,
|
|
1161
|
+
result: {
|
|
1162
|
+
success: r.result.success,
|
|
1163
|
+
output: r.result.output,
|
|
1164
|
+
error: r.result.error,
|
|
1165
|
+
metadata: r.result.metadata
|
|
1166
|
+
}
|
|
1167
|
+
})),
|
|
1168
|
+
} : undefined,
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Phase 1.8: Record impulse relevance metrics to backend
|
|
1175
|
+
*/
|
|
1176
|
+
private async recordImpulseRelevance(
|
|
1177
|
+
templateId: string | undefined,
|
|
1178
|
+
allImpulseIds: string[],
|
|
1179
|
+
loadedImpulseIds: string[],
|
|
1180
|
+
executionSucceeded: boolean
|
|
1181
|
+
): Promise<void> {
|
|
1182
|
+
if (!isMCPEnabled() || !templateId || allImpulseIds.length === 0) {
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const mcp = getMCPClient()
|
|
1187
|
+
if (!mcp) return
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
// Record relevance for each impulse
|
|
1191
|
+
for (const impulseId of allImpulseIds) {
|
|
1192
|
+
const wasLoaded = loadedImpulseIds.includes(impulseId)
|
|
1193
|
+
await mcp.recordImpulseRelevance({
|
|
1194
|
+
impulseId,
|
|
1195
|
+
activityVariantId: templateId,
|
|
1196
|
+
wasLoaded,
|
|
1197
|
+
executionSucceeded,
|
|
1198
|
+
})
|
|
1199
|
+
}
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
// Non-blocking: log error but don't fail task
|
|
1202
|
+
console.warn(`[Impulse Filter] Failed to record relevance:`, error instanceof Error ? error.message : String(error))
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Run validation checks
|
|
1208
|
+
*/
|
|
1209
|
+
private async runValidation(validation: ActivityTask["validation"], taskOutput?: string): Promise<{ success: boolean; error?: string }> {
|
|
1210
|
+
if (!validation) return { success: true }
|
|
1211
|
+
|
|
1212
|
+
// Check required files exist
|
|
1213
|
+
if (validation.requiredFiles) {
|
|
1214
|
+
for (const filePath of validation.requiredFiles) {
|
|
1215
|
+
const file = safeReadFile(filePath)
|
|
1216
|
+
if (!(await file.exists())) {
|
|
1217
|
+
return { success: false, error: `Required file missing: ${filePath}` }
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Check required patterns
|
|
1223
|
+
if (validation.requiredPatterns) {
|
|
1224
|
+
for (const patternSpec of validation.requiredPatterns) {
|
|
1225
|
+
// Support both string patterns (checked in task output) and { file, pattern } objects
|
|
1226
|
+
if (typeof patternSpec === 'string') {
|
|
1227
|
+
// Simple string pattern - check in task output
|
|
1228
|
+
if (!taskOutput) {
|
|
1229
|
+
return { success: false, error: `Cannot validate pattern "${patternSpec}": task output not available` }
|
|
1230
|
+
}
|
|
1231
|
+
const regex = new RegExp(patternSpec)
|
|
1232
|
+
if (!regex.test(taskOutput)) {
|
|
1233
|
+
return { success: false, error: `Required pattern not found in task output: ${patternSpec}` }
|
|
1234
|
+
}
|
|
1235
|
+
} else {
|
|
1236
|
+
// Object format { file, pattern } - check in specific file
|
|
1237
|
+
const { file: filePath, pattern } = patternSpec
|
|
1238
|
+
const file = safeReadFile(filePath)
|
|
1239
|
+
if (!(await file.exists())) {
|
|
1240
|
+
return { success: false, error: `File missing for pattern check: ${filePath}` }
|
|
1241
|
+
}
|
|
1242
|
+
const content = await file.text()
|
|
1243
|
+
const regex = new RegExp(pattern)
|
|
1244
|
+
if (!regex.test(content)) {
|
|
1245
|
+
return { success: false, error: `Pattern not found in ${filePath}: ${pattern}` }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Check forbidden patterns
|
|
1252
|
+
if (validation.forbiddenPatterns) {
|
|
1253
|
+
for (const { file: filePath, pattern } of validation.forbiddenPatterns) {
|
|
1254
|
+
const file = safeReadFile(filePath)
|
|
1255
|
+
if (await file.exists()) {
|
|
1256
|
+
const content = await file.text()
|
|
1257
|
+
const regex = new RegExp(pattern)
|
|
1258
|
+
if (regex.test(content)) {
|
|
1259
|
+
return { success: false, error: `Forbidden pattern found in ${filePath}: ${pattern}` }
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Run validation commands
|
|
1266
|
+
if (validation.commands) {
|
|
1267
|
+
for (const { command, expectedOutput } of validation.commands) {
|
|
1268
|
+
const bashHandler = this.toolHandlers.bash
|
|
1269
|
+
if (!bashHandler) {
|
|
1270
|
+
return { success: false, error: "bash tool handler not available" }
|
|
1271
|
+
}
|
|
1272
|
+
const result = await bashHandler({ command })
|
|
1273
|
+
if (!result.success) {
|
|
1274
|
+
return { success: false, error: `Validation command failed: ${command}` }
|
|
1275
|
+
}
|
|
1276
|
+
if (expectedOutput && !result.output?.includes(expectedOutput)) {
|
|
1277
|
+
return { success: false, error: `Unexpected output from: ${command}` }
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return { success: true }
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Topological sort for task dependencies
|
|
1287
|
+
*/
|
|
1288
|
+
private topologicalSort(tasks: ActivityTask[]): ActivityTask[] {
|
|
1289
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]))
|
|
1290
|
+
const visited = new Set<string>()
|
|
1291
|
+
const result: ActivityTask[] = []
|
|
1292
|
+
|
|
1293
|
+
const visit = (taskId: string): void => {
|
|
1294
|
+
if (visited.has(taskId)) return
|
|
1295
|
+
visited.add(taskId)
|
|
1296
|
+
|
|
1297
|
+
const task = taskMap.get(taskId)
|
|
1298
|
+
if (!task) return
|
|
1299
|
+
|
|
1300
|
+
for (const depId of task.dependencies ?? []) {
|
|
1301
|
+
visit(depId)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
result.push(task)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
for (const task of tasks) {
|
|
1308
|
+
visit(task.id)
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return result
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Interpolate variables into template string
|
|
1316
|
+
*/
|
|
1317
|
+
private interpolate(template: string, variables: Record<string, unknown>): string {
|
|
1318
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
1319
|
+
const value = variables[key]
|
|
1320
|
+
if (value === undefined) return `{{${key}}}`
|
|
1321
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2)
|
|
1322
|
+
return String(value)
|
|
1323
|
+
})
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Estimate cost based on token usage
|
|
1328
|
+
*/
|
|
1329
|
+
private estimateCost(inputTokens: number, outputTokens: number): number {
|
|
1330
|
+
// Approximate costs for Claude 3.5 Sonnet
|
|
1331
|
+
const inputCostPerMillion = 3.0
|
|
1332
|
+
const outputCostPerMillion = 15.0
|
|
1333
|
+
|
|
1334
|
+
return (
|
|
1335
|
+
(inputTokens / 1_000_000) * inputCostPerMillion +
|
|
1336
|
+
(outputTokens / 1_000_000) * outputCostPerMillion
|
|
1337
|
+
)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Get default system prompt
|
|
1342
|
+
*
|
|
1343
|
+
* ARCHITECTURE: Enforces activity-first constraint per corrected trailblazing architecture.
|
|
1344
|
+
* MiniBob must search for existing activities and create new ones for non-trivial tasks.
|
|
1345
|
+
*/
|
|
1346
|
+
private getDefaultSystemPrompt(): string {
|
|
1347
|
+
return `You are minibob, an autonomous vessel that creates and composes activities instead of using direct tool calls.
|
|
1348
|
+
|
|
1349
|
+
**CORE PRINCIPLE**: You learn by creating reusable activities, not by executing one-off tool calls.
|
|
1350
|
+
|
|
1351
|
+
**CONSTRAINTS - ACTIVITY-FIRST APPROACH**:
|
|
1352
|
+
|
|
1353
|
+
For NON-TRIVIAL tasks (anything beyond simple reads/status checks):
|
|
1354
|
+
1. search_activities() to find existing templates
|
|
1355
|
+
2. If match found: Execute the activity
|
|
1356
|
+
3. If no match: create_activity_goal_seeking() to create new template
|
|
1357
|
+
4. Record your reasoning for the choice
|
|
1358
|
+
|
|
1359
|
+
For TRIVIAL tasks (simple operations that don't benefit from templating):
|
|
1360
|
+
- Simple file reads (read a single file to check content)
|
|
1361
|
+
- Basic git status checks (git status, git diff)
|
|
1362
|
+
- Simple list operations (ls, find single file)
|
|
1363
|
+
✅ Direct tool use allowed
|
|
1364
|
+
|
|
1365
|
+
**EXAMPLES**:
|
|
1366
|
+
|
|
1367
|
+
Non-Trivial (MUST use activities):
|
|
1368
|
+
❌ "Fix all TypeScript errors" → search_activities → create if no match
|
|
1369
|
+
❌ "Add authentication" → search_activities → create if no match
|
|
1370
|
+
❌ "Refactor component" → search_activities → create if no match
|
|
1371
|
+
❌ "Deploy to production" → search_activities → create if no match
|
|
1372
|
+
|
|
1373
|
+
Trivial (Direct tools OK):
|
|
1374
|
+
✅ "Read src/index.ts to see imports" → read tool
|
|
1375
|
+
✅ "Check git status" → bash git status
|
|
1376
|
+
✅ "List files in src/" → bash ls src/
|
|
1377
|
+
|
|
1378
|
+
**WORKFLOW**:
|
|
1379
|
+
1. Analyze task complexity
|
|
1380
|
+
2. If non-trivial: search_activities() first
|
|
1381
|
+
- Found match? Execute it
|
|
1382
|
+
- No match? create_activity_goal_seeking()
|
|
1383
|
+
3. If trivial: Use direct tools (read, bash, git)
|
|
1384
|
+
4. Always explain your reasoning
|
|
1385
|
+
|
|
1386
|
+
**GUIDELINES**:
|
|
1387
|
+
- Prefer activity composition over direct execution
|
|
1388
|
+
- Create reusable templates from complex workflows
|
|
1389
|
+
- Search before creating (avoid duplicate activities)
|
|
1390
|
+
- Report activity execution results clearly
|
|
1391
|
+
- When stuck, ask specific targeted questions
|
|
1392
|
+
|
|
1393
|
+
Be autonomous, learn from execution, and build reusable knowledge.`
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Load an activity template from a JSON file
|
|
1399
|
+
*/
|
|
1400
|
+
export async function loadTemplate(path: string): Promise<ActivityTemplate> {
|
|
1401
|
+
const file = safeReadFile(path)
|
|
1402
|
+
if (!(await file.exists())) {
|
|
1403
|
+
throw new Error(`Template not found: ${path}`)
|
|
1404
|
+
}
|
|
1405
|
+
const content = await file.json()
|
|
1406
|
+
return content as ActivityTemplate
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Load activity template from MCP backend or local file
|
|
1411
|
+
*
|
|
1412
|
+
* If templateIdOrPath looks like a file path (contains / or .json), load from file.
|
|
1413
|
+
* Otherwise, try to load from MCP backend first, then fall back to local templates/ directory.
|
|
1414
|
+
*/
|
|
1415
|
+
export async function loadTemplateFromMCPOrLocal(templateIdOrPath: string): Promise<ActivityTemplate> {
|
|
1416
|
+
// Check if it's a file path
|
|
1417
|
+
if (templateIdOrPath.includes("/") || templateIdOrPath.endsWith(".json")) {
|
|
1418
|
+
return loadTemplate(templateIdOrPath)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Try MCP first if enabled
|
|
1422
|
+
if (isMCPEnabled()) {
|
|
1423
|
+
const mcp = getMCPClient()
|
|
1424
|
+
if (mcp) {
|
|
1425
|
+
console.log(`[Activity] Fetching template "${templateIdOrPath}" from MCP backend...`)
|
|
1426
|
+
const template = await mcp.getActivityTemplate(templateIdOrPath)
|
|
1427
|
+
if (template) {
|
|
1428
|
+
console.log(`[Activity] ✓ Template loaded from MCP backend`)
|
|
1429
|
+
return template
|
|
1430
|
+
}
|
|
1431
|
+
console.log(`[Activity] Template not found in MCP, trying local...`)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Fall back to local templates directory
|
|
1436
|
+
const localPath = `templates/${templateIdOrPath}.json`
|
|
1437
|
+
return loadTemplate(localPath)
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Create executor and run activity
|
|
1442
|
+
*/
|
|
1443
|
+
export async function runActivity(
|
|
1444
|
+
config: ExecutorConfig,
|
|
1445
|
+
templatePath: string,
|
|
1446
|
+
variables: Record<string, unknown>,
|
|
1447
|
+
reason?: string
|
|
1448
|
+
): Promise<ActivityExecution> {
|
|
1449
|
+
const template = await loadTemplate(templatePath)
|
|
1450
|
+
const executor = new ActivityExecutor(config)
|
|
1451
|
+
return executor.execute({ template, variables, reason })
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Format execution duration in human-readable format
|
|
1456
|
+
* @param ms - Duration in milliseconds
|
|
1457
|
+
* @returns Formatted string (e.g., "1.2s" or "345ms")
|
|
1458
|
+
*/
|
|
1459
|
+
export function formatDuration(ms: number): string {
|
|
1460
|
+
if (ms >= 1000) {
|
|
1461
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
1462
|
+
}
|
|
1463
|
+
return `${Math.round(ms)}ms`
|
|
1464
|
+
}
|