@metabob/minibob 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/ARCHITECTURE.md +255 -0
  2. package/CHANGELOG.md +112 -0
  3. package/README.md +380 -0
  4. package/bin/minibob.js +36 -0
  5. package/dist/acp-gossip.d.ts +72 -0
  6. package/dist/acp-gossip.d.ts.map +1 -0
  7. package/dist/acp-gossip.js +156 -0
  8. package/dist/acp-gossip.js.map +1 -0
  9. package/dist/acp.d.ts +62 -0
  10. package/dist/acp.d.ts.map +1 -0
  11. package/dist/acp.js +292 -0
  12. package/dist/acp.js.map +1 -0
  13. package/dist/activity.d.ts +157 -0
  14. package/dist/activity.d.ts.map +1 -0
  15. package/dist/activity.js +518 -0
  16. package/dist/activity.js.map +1 -0
  17. package/dist/agent-runtime.d.ts +104 -0
  18. package/dist/agent-runtime.d.ts.map +1 -0
  19. package/dist/boredom.d.ts +125 -0
  20. package/dist/boredom.d.ts.map +1 -0
  21. package/dist/boredom.js +244 -0
  22. package/dist/boredom.js.map +1 -0
  23. package/dist/cli/acp-server.d.ts +23 -0
  24. package/dist/cli/acp-server.d.ts.map +1 -0
  25. package/dist/cli/burrow.d.ts +26 -0
  26. package/dist/cli/burrow.d.ts.map +1 -0
  27. package/dist/cli/doctor.d.ts +22 -0
  28. package/dist/cli/doctor.d.ts.map +1 -0
  29. package/dist/cli/goal.d.ts +22 -0
  30. package/dist/cli/goal.d.ts.map +1 -0
  31. package/dist/cli/index.d.ts +47 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/instance-registry.d.ts +78 -0
  34. package/dist/cli/instance-registry.d.ts.map +1 -0
  35. package/dist/cli/observe.d.ts +35 -0
  36. package/dist/cli/observe.d.ts.map +1 -0
  37. package/dist/cli/vessel.d.ts +14 -0
  38. package/dist/cli/vessel.d.ts.map +1 -0
  39. package/dist/composition-observer.d.ts +96 -0
  40. package/dist/composition-observer.d.ts.map +1 -0
  41. package/dist/config.d.ts +36 -0
  42. package/dist/config.d.ts.map +1 -0
  43. package/dist/config.js +128 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/docker/Dockerfile +35 -0
  46. package/dist/environment.d.ts +72 -0
  47. package/dist/environment.d.ts.map +1 -0
  48. package/dist/environment.js +142 -0
  49. package/dist/environment.js.map +1 -0
  50. package/dist/goal-processor.d.ts +165 -0
  51. package/dist/goal-processor.d.ts.map +1 -0
  52. package/dist/helm/minibob-cluster/Chart.yaml +13 -0
  53. package/dist/helm/minibob-cluster/templates/_helpers.tpl +60 -0
  54. package/dist/helm/minibob-cluster/templates/configmap.yaml +11 -0
  55. package/dist/helm/minibob-cluster/templates/deployment.yaml +108 -0
  56. package/dist/helm/minibob-cluster/templates/secret.yaml +10 -0
  57. package/dist/helm/minibob-cluster/templates/service.yaml +37 -0
  58. package/dist/helm/minibob-cluster/values-local.yaml +41 -0
  59. package/dist/helm/minibob-cluster/values-production.yaml +57 -0
  60. package/dist/helm/minibob-cluster/values-testing-cluster.yaml +43 -0
  61. package/dist/helm/minibob-cluster/values.yaml +127 -0
  62. package/dist/improviser.d.ts +74 -0
  63. package/dist/improviser.d.ts.map +1 -0
  64. package/dist/impulse-filter.d.ts +74 -0
  65. package/dist/impulse-filter.d.ts.map +1 -0
  66. package/dist/impulse.d.ts +92 -0
  67. package/dist/impulse.d.ts.map +1 -0
  68. package/dist/impulse.js +234 -0
  69. package/dist/impulse.js.map +1 -0
  70. package/dist/lib.d.ts +29 -0
  71. package/dist/lib.d.ts.map +1 -0
  72. package/dist/lib.js +18561 -0
  73. package/dist/lib.js.map +98 -0
  74. package/dist/lifecycle-hooks.d.ts +99 -0
  75. package/dist/lifecycle-hooks.d.ts.map +1 -0
  76. package/dist/lifecycle-hooks.js +135 -0
  77. package/dist/lifecycle-hooks.js.map +1 -0
  78. package/dist/llm.d.ts +31 -0
  79. package/dist/llm.d.ts.map +1 -0
  80. package/dist/llm.js +349 -0
  81. package/dist/llm.js.map +1 -0
  82. package/dist/mcp-activity-bridge.d.ts +66 -0
  83. package/dist/mcp-activity-bridge.d.ts.map +1 -0
  84. package/dist/mcp-activity-bridge.js +126 -0
  85. package/dist/mcp-activity-bridge.js.map +1 -0
  86. package/dist/mcp.d.ts +216 -0
  87. package/dist/mcp.d.ts.map +1 -0
  88. package/dist/mcp.js +292 -0
  89. package/dist/mcp.js.map +1 -0
  90. package/dist/memory-agent.d.ts +92 -0
  91. package/dist/memory-agent.d.ts.map +1 -0
  92. package/dist/memory-agent.js +277 -0
  93. package/dist/memory-agent.js.map +1 -0
  94. package/dist/runtime-mapping.d.ts +97 -0
  95. package/dist/runtime-mapping.d.ts.map +1 -0
  96. package/dist/search-first-executor.d.ts +113 -0
  97. package/dist/search-first-executor.d.ts.map +1 -0
  98. package/dist/session.d.ts +48 -0
  99. package/dist/session.d.ts.map +1 -0
  100. package/dist/template-extractor.d.ts +9 -0
  101. package/dist/template-extractor.d.ts.map +1 -0
  102. package/dist/template-generator.d.ts +12 -0
  103. package/dist/template-generator.d.ts.map +1 -0
  104. package/dist/tools.d.ts +58 -0
  105. package/dist/tools.d.ts.map +1 -0
  106. package/dist/tools.js +771 -0
  107. package/dist/tools.js.map +1 -0
  108. package/dist/types.d.ts +503 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +8 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/understanding/analyzer.d.ts +55 -0
  113. package/dist/understanding/analyzer.d.ts.map +1 -0
  114. package/dist/understanding/explorer.d.ts +73 -0
  115. package/dist/understanding/explorer.d.ts.map +1 -0
  116. package/dist/understanding/index.d.ts +7 -0
  117. package/dist/understanding/index.d.ts.map +1 -0
  118. package/dist/understanding/types.d.ts +136 -0
  119. package/dist/understanding/types.d.ts.map +1 -0
  120. package/dist/validation.d.ts +29 -0
  121. package/dist/validation.d.ts.map +1 -0
  122. package/dist/validation.js +106 -0
  123. package/dist/validation.js.map +1 -0
  124. package/dist/vessel-bootstrap.d.ts +190 -0
  125. package/dist/vessel-bootstrap.d.ts.map +1 -0
  126. package/dist/vessel-registry.d.ts +229 -0
  127. package/dist/vessel-registry.d.ts.map +1 -0
  128. package/index.ts +1329 -0
  129. package/package.json +54 -0
  130. package/src/acp-gossip.ts +193 -0
  131. package/src/acp.ts +362 -0
  132. package/src/activity.ts +1464 -0
  133. package/src/agent-runtime.ts +365 -0
  134. package/src/boredom.ts +423 -0
  135. package/src/cli/acp-server.ts +377 -0
  136. package/src/cli/burrow.ts +896 -0
  137. package/src/cli/doctor.ts +526 -0
  138. package/src/cli/goal.ts +224 -0
  139. package/src/cli/index.ts +147 -0
  140. package/src/cli/instance-registry.ts +271 -0
  141. package/src/cli/observe.ts +682 -0
  142. package/src/cli/vessel.ts +287 -0
  143. package/src/components/SystemOverview.tsx +331 -0
  144. package/src/composition-observer.ts +449 -0
  145. package/src/config.ts +172 -0
  146. package/src/environment.ts +167 -0
  147. package/src/goal-processor.ts +654 -0
  148. package/src/improviser.ts +591 -0
  149. package/src/impulse-filter.ts +273 -0
  150. package/src/impulse.ts +311 -0
  151. package/src/lib.ts +147 -0
  152. package/src/lifecycle-hooks.ts +181 -0
  153. package/src/llm.ts +434 -0
  154. package/src/mcp-activity-bridge.ts +158 -0
  155. package/src/mcp.ts +747 -0
  156. package/src/memory-agent.ts +316 -0
  157. package/src/runtime-mapping.ts +527 -0
  158. package/src/search-first-executor.ts +666 -0
  159. package/src/session.ts +141 -0
  160. package/src/template-extractor.ts +256 -0
  161. package/src/template-generator.ts +130 -0
  162. package/src/tools.ts +924 -0
  163. package/src/types.ts +497 -0
  164. package/src/understanding/analyzer.ts +354 -0
  165. package/src/understanding/explorer.ts +488 -0
  166. package/src/understanding/index.ts +27 -0
  167. package/src/understanding/types.ts +153 -0
  168. package/src/validation.ts +125 -0
  169. package/src/vessel-bootstrap.ts +440 -0
  170. package/src/vessel-registry.ts +621 -0
  171. package/templates/core/edit-file.json +85 -0
  172. package/templates/understanding/diagnose-problem.json +32 -0
  173. package/templates/understanding/explore-codebase-v2.json +57 -0
  174. package/templates/understanding/explore-codebase.json +37 -0
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
+ }