@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/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metabob/minibob",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Minimal vessel for the process-of-becoming",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/lib.js",
|
|
7
|
+
"types": "./dist/lib.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"minibob": "./bin/minibob.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/lib.d.ts",
|
|
14
|
+
"import": "./dist/lib.js"
|
|
15
|
+
},
|
|
16
|
+
"./cli": {
|
|
17
|
+
"import": "./index.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"index.ts",
|
|
24
|
+
"src",
|
|
25
|
+
"templates",
|
|
26
|
+
"README.md",
|
|
27
|
+
"ARCHITECTURE.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "bun run index.ts",
|
|
32
|
+
"dev": "bun --watch run index.ts",
|
|
33
|
+
"build": "bun build src/lib.ts --outdir dist --target node --format esm --sourcemap=external && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
34
|
+
"prepublishOnly": "chmod +x bin/minibob.js && bun run build",
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"vessel",
|
|
40
|
+
"activity",
|
|
41
|
+
"impulse",
|
|
42
|
+
"acp",
|
|
43
|
+
"agent"
|
|
44
|
+
],
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/bun": "^1.3.10"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"typescript": "^5"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"zod": "^4.3.6"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Gossip Protocol
|
|
3
|
+
*
|
|
4
|
+
* Implements DNS-based peer discovery and periodic health broadcasts
|
|
5
|
+
* for minibob vessels running in cluster mode.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - DNS lookup discovers peer IPs via K8s headless service
|
|
9
|
+
* - Periodic health broadcasts to all peers
|
|
10
|
+
* - Peer state tracking (last seen, health status)
|
|
11
|
+
*
|
|
12
|
+
* This enables vessel-to-vessel coordination and awareness.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { exec } from "child_process"
|
|
16
|
+
import { promisify } from "util"
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec)
|
|
19
|
+
|
|
20
|
+
export interface PeerInfo {
|
|
21
|
+
ip: string
|
|
22
|
+
lastSeen: Date
|
|
23
|
+
healthy: boolean
|
|
24
|
+
capabilities?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GossipConfig {
|
|
28
|
+
serviceName: string // e.g., "minibob-cluster.default.svc.cluster.local"
|
|
29
|
+
broadcastInterval: number // ms between health broadcasts
|
|
30
|
+
peerTimeout: number // ms before marking peer as unhealthy
|
|
31
|
+
acpPort: number // Port for ACP communication
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ACPGossipProtocol {
|
|
35
|
+
private config: GossipConfig
|
|
36
|
+
private peers: Map<string, PeerInfo> = new Map()
|
|
37
|
+
private isRunning = false
|
|
38
|
+
private intervalId?: Timer
|
|
39
|
+
|
|
40
|
+
constructor(config: GossipConfig) {
|
|
41
|
+
this.config = config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Start gossip protocol
|
|
46
|
+
*
|
|
47
|
+
* Begins periodic peer discovery and health broadcasts
|
|
48
|
+
*/
|
|
49
|
+
async start(): Promise<void> {
|
|
50
|
+
if (this.isRunning) {
|
|
51
|
+
console.log("[ACP Gossip] Already running")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("[ACP Gossip] Starting protocol")
|
|
56
|
+
console.log(`[ACP Gossip] Service: ${this.config.serviceName}`)
|
|
57
|
+
console.log(`[ACP Gossip] Broadcast interval: ${this.config.broadcastInterval}ms`)
|
|
58
|
+
|
|
59
|
+
this.isRunning = true
|
|
60
|
+
|
|
61
|
+
// Initial peer discovery
|
|
62
|
+
await this.discoverPeers()
|
|
63
|
+
|
|
64
|
+
// Start periodic health broadcasts
|
|
65
|
+
this.intervalId = setInterval(async () => {
|
|
66
|
+
await this.broadcastHealth()
|
|
67
|
+
await this.discoverPeers()
|
|
68
|
+
this.cleanupStalePeers()
|
|
69
|
+
}, this.config.broadcastInterval)
|
|
70
|
+
|
|
71
|
+
console.log("[ACP Gossip] Protocol started")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop gossip protocol
|
|
76
|
+
*/
|
|
77
|
+
stop(): void {
|
|
78
|
+
if (!this.isRunning) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("[ACP Gossip] Stopping protocol")
|
|
83
|
+
|
|
84
|
+
if (this.intervalId) {
|
|
85
|
+
clearInterval(this.intervalId)
|
|
86
|
+
this.intervalId = undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.isRunning = false
|
|
90
|
+
console.log("[ACP Gossip] Protocol stopped")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Discover peers via DNS lookup
|
|
95
|
+
*
|
|
96
|
+
* Uses getent/nslookup to resolve all IPs for the headless service
|
|
97
|
+
*/
|
|
98
|
+
private async discoverPeers(): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
// Try getent first (more reliable in K8s)
|
|
101
|
+
const { stdout } = await execAsync(`getent hosts ${this.config.serviceName}`)
|
|
102
|
+
const lines = stdout.trim().split('\n')
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const match = line.match(/^(\d+\.\d+\.\d+\.\d+)/)
|
|
106
|
+
if (match && match[1]) {
|
|
107
|
+
const ip = match[1]
|
|
108
|
+
|
|
109
|
+
if (!this.peers.has(ip)) {
|
|
110
|
+
console.log(`[ACP Gossip] Discovered new peer: ${ip}`)
|
|
111
|
+
this.peers.set(ip, {
|
|
112
|
+
ip,
|
|
113
|
+
lastSeen: new Date(),
|
|
114
|
+
healthy: true,
|
|
115
|
+
capabilities: []
|
|
116
|
+
})
|
|
117
|
+
} else {
|
|
118
|
+
// Update last seen
|
|
119
|
+
const peer = this.peers.get(ip)
|
|
120
|
+
if (peer) {
|
|
121
|
+
peer.lastSeen = new Date()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log(`[ACP Gossip] Peer discovery failed: ${error}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Broadcast health to all peers
|
|
133
|
+
*
|
|
134
|
+
* TODO: Implement actual health broadcast via HTTP POST to peer ACP endpoints
|
|
135
|
+
* For now, this is a placeholder that logs the intent
|
|
136
|
+
*/
|
|
137
|
+
private async broadcastHealth(): Promise<void> {
|
|
138
|
+
const peerCount = this.peers.size
|
|
139
|
+
const healthyCount = Array.from(this.peers.values()).filter(p => p.healthy).length
|
|
140
|
+
|
|
141
|
+
console.log(`[ACP Gossip] Health broadcast: ${healthyCount}/${peerCount} peers healthy`)
|
|
142
|
+
|
|
143
|
+
// TODO: Implement actual HTTP POST to each peer's /acp/gossip endpoint
|
|
144
|
+
// For now, we just log that we would broadcast
|
|
145
|
+
for (const [ip, peer] of this.peers.entries()) {
|
|
146
|
+
// Future: POST http://${ip}:${this.config.acpPort}/acp/gossip
|
|
147
|
+
// with payload: { from: myIP, timestamp: Date.now(), capabilities: [...] }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Remove peers that haven't been seen recently
|
|
153
|
+
*/
|
|
154
|
+
private cleanupStalePeers(): void {
|
|
155
|
+
const now = Date.now()
|
|
156
|
+
const staleThreshold = now - this.config.peerTimeout
|
|
157
|
+
|
|
158
|
+
for (const [ip, peer] of this.peers.entries()) {
|
|
159
|
+
if (peer.lastSeen.getTime() < staleThreshold) {
|
|
160
|
+
console.log(`[ACP Gossip] Peer ${ip} is stale, removing`)
|
|
161
|
+
this.peers.delete(ip)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get current peer list
|
|
168
|
+
*/
|
|
169
|
+
getPeers(): PeerInfo[] {
|
|
170
|
+
return Array.from(this.peers.values())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get peer count
|
|
175
|
+
*/
|
|
176
|
+
getPeerCount(): number {
|
|
177
|
+
return this.peers.size
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Initialize ACP Gossip for minibob cluster
|
|
183
|
+
*/
|
|
184
|
+
export function initializeACPGossip(serviceName: string, acpPort: number): ACPGossipProtocol {
|
|
185
|
+
const config: GossipConfig = {
|
|
186
|
+
serviceName,
|
|
187
|
+
broadcastInterval: 30000, // 30 seconds
|
|
188
|
+
peerTimeout: 90000, // 90 seconds (3 missed broadcasts)
|
|
189
|
+
acpPort
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new ACPGossipProtocol(config)
|
|
193
|
+
}
|
package/src/acp.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minibob ACP Protocol Handler
|
|
3
|
+
*
|
|
4
|
+
* Agent Client Protocol support for vessel-to-vessel communication.
|
|
5
|
+
* Supports both receiving delegated tasks and delegating to other vessels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ACPMessage,
|
|
10
|
+
ACPDelegateRequest,
|
|
11
|
+
ACPDelegateResponse,
|
|
12
|
+
Message,
|
|
13
|
+
ToolResult,
|
|
14
|
+
} from "./types"
|
|
15
|
+
import { createLLMClient, type LLMClient } from "./llm"
|
|
16
|
+
import { createToolHandlers, getAllToolDefinitions } from "./tools"
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// ACP SERVER (Receive delegated tasks)
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export interface ACPServerConfig {
|
|
23
|
+
provider: "anthropic" | "openai"
|
|
24
|
+
apiKey: string
|
|
25
|
+
model: string
|
|
26
|
+
workingDirectory: string
|
|
27
|
+
systemPrompt?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ACP session handler for streaming connections
|
|
32
|
+
*/
|
|
33
|
+
export class ACPSession {
|
|
34
|
+
private llm: LLMClient
|
|
35
|
+
private toolHandlers: ReturnType<typeof createToolHandlers>
|
|
36
|
+
private config: ACPServerConfig
|
|
37
|
+
private sessionId: string
|
|
38
|
+
private messages: Message[] = []
|
|
39
|
+
private toolsUsed: string[] = []
|
|
40
|
+
|
|
41
|
+
constructor(config: ACPServerConfig, sessionId: string) {
|
|
42
|
+
this.config = config
|
|
43
|
+
this.sessionId = sessionId
|
|
44
|
+
this.llm = createLLMClient(config.provider, config.apiKey)
|
|
45
|
+
this.toolHandlers = createToolHandlers(config.workingDirectory)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handle an incoming ACP message
|
|
50
|
+
*/
|
|
51
|
+
async handleMessage(message: ACPMessage): Promise<ACPMessage[]> {
|
|
52
|
+
const responses: ACPMessage[] = []
|
|
53
|
+
|
|
54
|
+
switch (message.type) {
|
|
55
|
+
case "hello":
|
|
56
|
+
// Respond with our capabilities
|
|
57
|
+
responses.push({
|
|
58
|
+
type: "hello",
|
|
59
|
+
version: "1.0",
|
|
60
|
+
capabilities: ["prompt", "tool_call"],
|
|
61
|
+
})
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
case "prompt":
|
|
65
|
+
// Execute the prompt with tools
|
|
66
|
+
try {
|
|
67
|
+
this.messages = message.messages
|
|
68
|
+
|
|
69
|
+
// Add system prompt if not present
|
|
70
|
+
if (!this.messages.find((m) => m.role === "system")) {
|
|
71
|
+
this.messages.unshift({
|
|
72
|
+
role: "system",
|
|
73
|
+
content: this.config.systemPrompt ?? this.getDefaultSystemPrompt(),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await this.llm.completeWithTools(
|
|
78
|
+
{
|
|
79
|
+
model: this.config.model,
|
|
80
|
+
messages: this.messages,
|
|
81
|
+
tools: message.tools ?? getAllToolDefinitions(),
|
|
82
|
+
maxTokens: 4096,
|
|
83
|
+
},
|
|
84
|
+
this.toolHandlers
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
this.toolsUsed = result.toolsUsed
|
|
88
|
+
|
|
89
|
+
responses.push({
|
|
90
|
+
type: "response",
|
|
91
|
+
sessionId: this.sessionId,
|
|
92
|
+
content: result.content,
|
|
93
|
+
finishReason: "stop",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
responses.push({
|
|
97
|
+
type: "complete",
|
|
98
|
+
sessionId: this.sessionId,
|
|
99
|
+
metrics: {
|
|
100
|
+
duration: 0, // Would need timing
|
|
101
|
+
tokens: { input: result.usage.inputTokens, output: result.usage.outputTokens },
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
} catch (error) {
|
|
105
|
+
responses.push({
|
|
106
|
+
type: "error",
|
|
107
|
+
sessionId: this.sessionId,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
case "tool_call":
|
|
114
|
+
// Execute a tool call
|
|
115
|
+
try {
|
|
116
|
+
const handler = this.toolHandlers[message.name]
|
|
117
|
+
let result: ToolResult
|
|
118
|
+
|
|
119
|
+
if (!handler) {
|
|
120
|
+
result = {
|
|
121
|
+
success: false,
|
|
122
|
+
error: `Unknown tool: ${message.name}`,
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
result = await handler(message.arguments)
|
|
126
|
+
this.toolsUsed.push(message.name)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
responses.push({
|
|
130
|
+
type: "tool_result",
|
|
131
|
+
sessionId: this.sessionId,
|
|
132
|
+
toolCallId: message.toolCallId,
|
|
133
|
+
result,
|
|
134
|
+
})
|
|
135
|
+
} catch (error) {
|
|
136
|
+
responses.push({
|
|
137
|
+
type: "tool_result",
|
|
138
|
+
sessionId: this.sessionId,
|
|
139
|
+
toolCallId: message.toolCallId,
|
|
140
|
+
result: {
|
|
141
|
+
success: false,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return responses
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get tools used in this session
|
|
154
|
+
*/
|
|
155
|
+
getToolsUsed(): string[] {
|
|
156
|
+
return [...new Set(this.toolsUsed)]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private getDefaultSystemPrompt(): string {
|
|
160
|
+
return `You are minibob, a minimal vessel for executing delegated tasks.
|
|
161
|
+
You have access to tools for file operations, shell commands, and git.
|
|
162
|
+
Execute the requested task efficiently and report the results.`
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handle WebSocket upgrade for ACP streaming
|
|
168
|
+
*/
|
|
169
|
+
export function createACPWebSocketHandler(config: ACPServerConfig) {
|
|
170
|
+
const sessions = new Map<string, ACPSession>()
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
upgrade(request: Request): Response | undefined {
|
|
174
|
+
const url = new URL(request.url)
|
|
175
|
+
if (url.pathname !== "/acp/stream") {
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
180
|
+
const session = new ACPSession(config, sessionId)
|
|
181
|
+
sessions.set(sessionId, session)
|
|
182
|
+
|
|
183
|
+
return undefined // Let Bun handle the upgrade
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
websocket: {
|
|
187
|
+
async message(ws: WebSocket & { data: { sessionId?: string } }, message: string | Buffer) {
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(typeof message === "string" ? message : message.toString()) as ACPMessage
|
|
190
|
+
|
|
191
|
+
// Get or create session
|
|
192
|
+
let sessionId = ws.data?.sessionId
|
|
193
|
+
if (!sessionId) {
|
|
194
|
+
sessionId = `acp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
195
|
+
ws.data = { sessionId }
|
|
196
|
+
sessions.set(sessionId, new ACPSession(config, sessionId))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const session = sessions.get(sessionId)
|
|
200
|
+
if (!session) {
|
|
201
|
+
ws.send(JSON.stringify({ type: "error", error: "Session not found" }))
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const responses = await session.handleMessage(data)
|
|
206
|
+
for (const response of responses) {
|
|
207
|
+
ws.send(JSON.stringify(response))
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
ws.send(JSON.stringify({
|
|
211
|
+
type: "error",
|
|
212
|
+
error: error instanceof Error ? error.message : String(error),
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
close(ws: WebSocket & { data: { sessionId?: string } }) {
|
|
218
|
+
if (ws.data?.sessionId) {
|
|
219
|
+
sessions.delete(ws.data.sessionId)
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// ACP CLIENT (Delegate to other vessels)
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Delegate a task to another vessel via ACP
|
|
232
|
+
*/
|
|
233
|
+
export async function delegateTask(request: ACPDelegateRequest): Promise<ACPDelegateResponse> {
|
|
234
|
+
const startTime = Date.now()
|
|
235
|
+
const sessionId = `delegate_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Parse target URL
|
|
239
|
+
const targetUrl = parseTargetUrl(request.target)
|
|
240
|
+
|
|
241
|
+
// Connect via HTTP streaming or WebSocket
|
|
242
|
+
const response = await fetch(targetUrl, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
type: "prompt",
|
|
249
|
+
sessionId,
|
|
250
|
+
messages: [
|
|
251
|
+
{
|
|
252
|
+
role: "user",
|
|
253
|
+
content: request.prompt,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
throw new Error(`ACP request failed: ${response.status}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = (await response.json()) as {
|
|
264
|
+
content?: string
|
|
265
|
+
toolsUsed?: string[]
|
|
266
|
+
error?: string
|
|
267
|
+
metrics?: {
|
|
268
|
+
duration: number
|
|
269
|
+
tokens: { input: number; output: number }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: !result.error,
|
|
275
|
+
sessionId,
|
|
276
|
+
response: result.content,
|
|
277
|
+
toolsUsed: result.toolsUsed,
|
|
278
|
+
error: result.error,
|
|
279
|
+
metrics: {
|
|
280
|
+
duration: Date.now() - startTime,
|
|
281
|
+
tokens: result.metrics?.tokens ?? { input: 0, output: 0 },
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
sessionId,
|
|
288
|
+
error: error instanceof Error ? error.message : String(error),
|
|
289
|
+
metrics: {
|
|
290
|
+
duration: Date.now() - startTime,
|
|
291
|
+
tokens: { input: 0, output: 0 },
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse ACP target URL
|
|
299
|
+
*/
|
|
300
|
+
function parseTargetUrl(target: string): string {
|
|
301
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
302
|
+
return target
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (target.startsWith("docker://")) {
|
|
306
|
+
const container = target.replace("docker://", "")
|
|
307
|
+
// For Docker, we'd need to resolve the container IP
|
|
308
|
+
// For now, assume localhost with a port mapping
|
|
309
|
+
return `http://${container}:8080/acp`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (target.startsWith("tcp://")) {
|
|
313
|
+
const hostPort = target.replace("tcp://", "")
|
|
314
|
+
return `http://${hostPort}/acp`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new Error(`Unsupported ACP target: ${target}`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// =============================================================================
|
|
321
|
+
// HTTP ENDPOINT HANDLER
|
|
322
|
+
// =============================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Handle ACP HTTP POST requests (non-streaming)
|
|
326
|
+
*/
|
|
327
|
+
export async function handleACPRequest(
|
|
328
|
+
config: ACPServerConfig,
|
|
329
|
+
request: Request
|
|
330
|
+
): Promise<Response> {
|
|
331
|
+
const sessionId = `http_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
332
|
+
const session = new ACPSession(config, sessionId)
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const message = (await request.json()) as ACPMessage
|
|
336
|
+
const responses = await session.handleMessage(message)
|
|
337
|
+
|
|
338
|
+
// For HTTP, we return the last meaningful response
|
|
339
|
+
const lastResponse = responses.find(
|
|
340
|
+
(r) => r.type === "response" || r.type === "error"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return new Response(JSON.stringify({
|
|
344
|
+
success: lastResponse?.type === "response",
|
|
345
|
+
content: lastResponse?.type === "response" ? (lastResponse as { content: string }).content : undefined,
|
|
346
|
+
error: lastResponse?.type === "error" ? (lastResponse as { error: string }).error : undefined,
|
|
347
|
+
toolsUsed: session.getToolsUsed(),
|
|
348
|
+
sessionId,
|
|
349
|
+
}), {
|
|
350
|
+
headers: { "Content-Type": "application/json" },
|
|
351
|
+
})
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return new Response(JSON.stringify({
|
|
354
|
+
success: false,
|
|
355
|
+
error: error instanceof Error ? error.message : String(error),
|
|
356
|
+
sessionId,
|
|
357
|
+
}), {
|
|
358
|
+
status: 500,
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
}
|