@johpaz/hive 1.1.0

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 (156) hide show
  1. package/CONTRIBUTING.md +44 -0
  2. package/README.md +310 -0
  3. package/package.json +96 -0
  4. package/packages/cli/package.json +28 -0
  5. package/packages/cli/src/commands/agent-run.ts +168 -0
  6. package/packages/cli/src/commands/agents.ts +398 -0
  7. package/packages/cli/src/commands/chat.ts +142 -0
  8. package/packages/cli/src/commands/config.ts +50 -0
  9. package/packages/cli/src/commands/cron.ts +161 -0
  10. package/packages/cli/src/commands/dev.ts +95 -0
  11. package/packages/cli/src/commands/doctor.ts +133 -0
  12. package/packages/cli/src/commands/gateway.ts +443 -0
  13. package/packages/cli/src/commands/logs.ts +57 -0
  14. package/packages/cli/src/commands/mcp.ts +175 -0
  15. package/packages/cli/src/commands/message.ts +77 -0
  16. package/packages/cli/src/commands/onboard.ts +1868 -0
  17. package/packages/cli/src/commands/security.ts +144 -0
  18. package/packages/cli/src/commands/service.ts +50 -0
  19. package/packages/cli/src/commands/sessions.ts +116 -0
  20. package/packages/cli/src/commands/skills.ts +187 -0
  21. package/packages/cli/src/commands/update.ts +25 -0
  22. package/packages/cli/src/index.ts +185 -0
  23. package/packages/cli/src/utils/token.ts +6 -0
  24. package/packages/code-bridge/README.md +78 -0
  25. package/packages/code-bridge/package.json +18 -0
  26. package/packages/code-bridge/src/index.ts +95 -0
  27. package/packages/code-bridge/src/process-manager.ts +212 -0
  28. package/packages/code-bridge/src/schemas.ts +133 -0
  29. package/packages/core/package.json +46 -0
  30. package/packages/core/src/agent/agent-loop.ts +369 -0
  31. package/packages/core/src/agent/compaction.ts +140 -0
  32. package/packages/core/src/agent/context-compiler.ts +378 -0
  33. package/packages/core/src/agent/context-guard.ts +91 -0
  34. package/packages/core/src/agent/context.ts +138 -0
  35. package/packages/core/src/agent/conversation-store.ts +198 -0
  36. package/packages/core/src/agent/curator.ts +158 -0
  37. package/packages/core/src/agent/hooks.ts +166 -0
  38. package/packages/core/src/agent/index.ts +116 -0
  39. package/packages/core/src/agent/llm-client.ts +503 -0
  40. package/packages/core/src/agent/native-tools.ts +505 -0
  41. package/packages/core/src/agent/prompt-builder.ts +532 -0
  42. package/packages/core/src/agent/providers/index.ts +167 -0
  43. package/packages/core/src/agent/providers.ts +1 -0
  44. package/packages/core/src/agent/reflector.ts +170 -0
  45. package/packages/core/src/agent/service.ts +64 -0
  46. package/packages/core/src/agent/stuck-loop.ts +133 -0
  47. package/packages/core/src/agent/supervisor.ts +39 -0
  48. package/packages/core/src/agent/tracer.ts +102 -0
  49. package/packages/core/src/agent/workspace.ts +110 -0
  50. package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
  51. package/packages/core/src/canvas/canvas-manager.ts +319 -0
  52. package/packages/core/src/canvas/canvas-tools.ts +420 -0
  53. package/packages/core/src/canvas/emitter.ts +115 -0
  54. package/packages/core/src/canvas/index.ts +2 -0
  55. package/packages/core/src/channels/base.ts +138 -0
  56. package/packages/core/src/channels/discord.ts +260 -0
  57. package/packages/core/src/channels/index.ts +7 -0
  58. package/packages/core/src/channels/manager.ts +383 -0
  59. package/packages/core/src/channels/slack.ts +287 -0
  60. package/packages/core/src/channels/telegram.ts +502 -0
  61. package/packages/core/src/channels/webchat.ts +128 -0
  62. package/packages/core/src/channels/whatsapp.ts +375 -0
  63. package/packages/core/src/config/index.ts +12 -0
  64. package/packages/core/src/config/loader.ts +529 -0
  65. package/packages/core/src/events/event-bus.ts +169 -0
  66. package/packages/core/src/gateway/index.ts +5 -0
  67. package/packages/core/src/gateway/initializer.ts +290 -0
  68. package/packages/core/src/gateway/lane-queue.ts +169 -0
  69. package/packages/core/src/gateway/resolver.ts +108 -0
  70. package/packages/core/src/gateway/router.ts +124 -0
  71. package/packages/core/src/gateway/server.ts +3317 -0
  72. package/packages/core/src/gateway/session.ts +95 -0
  73. package/packages/core/src/gateway/slash-commands.ts +192 -0
  74. package/packages/core/src/heartbeat/index.ts +157 -0
  75. package/packages/core/src/index.ts +19 -0
  76. package/packages/core/src/integrations/catalog.ts +286 -0
  77. package/packages/core/src/integrations/env.ts +64 -0
  78. package/packages/core/src/integrations/index.ts +2 -0
  79. package/packages/core/src/memory/index.ts +1 -0
  80. package/packages/core/src/memory/notes.ts +68 -0
  81. package/packages/core/src/plugins/api.ts +128 -0
  82. package/packages/core/src/plugins/index.ts +2 -0
  83. package/packages/core/src/plugins/loader.ts +365 -0
  84. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  85. package/packages/core/src/security/google-chat.ts +269 -0
  86. package/packages/core/src/security/index.ts +192 -0
  87. package/packages/core/src/security/pairing.ts +250 -0
  88. package/packages/core/src/security/rate-limit.ts +270 -0
  89. package/packages/core/src/security/signal.ts +321 -0
  90. package/packages/core/src/state/store.ts +312 -0
  91. package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
  92. package/packages/core/src/storage/crypto.ts +101 -0
  93. package/packages/core/src/storage/db-context.ts +333 -0
  94. package/packages/core/src/storage/onboarding.ts +1087 -0
  95. package/packages/core/src/storage/schema.ts +541 -0
  96. package/packages/core/src/storage/seed.ts +571 -0
  97. package/packages/core/src/storage/sqlite.ts +387 -0
  98. package/packages/core/src/storage/usage.ts +212 -0
  99. package/packages/core/src/tools/bridge-events.ts +74 -0
  100. package/packages/core/src/tools/browser.ts +275 -0
  101. package/packages/core/src/tools/codebridge.ts +421 -0
  102. package/packages/core/src/tools/coordinator-tools.ts +179 -0
  103. package/packages/core/src/tools/cron.ts +611 -0
  104. package/packages/core/src/tools/exec.ts +140 -0
  105. package/packages/core/src/tools/fs.ts +364 -0
  106. package/packages/core/src/tools/index.ts +12 -0
  107. package/packages/core/src/tools/memory.ts +176 -0
  108. package/packages/core/src/tools/notify.ts +113 -0
  109. package/packages/core/src/tools/project-management.ts +376 -0
  110. package/packages/core/src/tools/project.ts +375 -0
  111. package/packages/core/src/tools/read.ts +158 -0
  112. package/packages/core/src/tools/web.ts +436 -0
  113. package/packages/core/src/tools/workspace.ts +171 -0
  114. package/packages/core/src/utils/benchmark.ts +80 -0
  115. package/packages/core/src/utils/crypto.ts +73 -0
  116. package/packages/core/src/utils/date.ts +42 -0
  117. package/packages/core/src/utils/index.ts +4 -0
  118. package/packages/core/src/utils/logger.ts +388 -0
  119. package/packages/core/src/utils/retry.ts +70 -0
  120. package/packages/core/src/voice/index.ts +583 -0
  121. package/packages/core/tsconfig.json +9 -0
  122. package/packages/mcp/package.json +26 -0
  123. package/packages/mcp/src/config.ts +13 -0
  124. package/packages/mcp/src/index.ts +1 -0
  125. package/packages/mcp/src/logger.ts +42 -0
  126. package/packages/mcp/src/manager.ts +434 -0
  127. package/packages/mcp/src/transports/index.ts +67 -0
  128. package/packages/mcp/src/transports/sse.ts +241 -0
  129. package/packages/mcp/src/transports/websocket.ts +159 -0
  130. package/packages/skills/package.json +21 -0
  131. package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
  132. package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
  133. package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
  134. package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
  135. package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
  136. package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
  137. package/packages/skills/src/bundled/memory/SKILL.md +42 -0
  138. package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
  139. package/packages/skills/src/bundled/shell/SKILL.md +43 -0
  140. package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
  141. package/packages/skills/src/bundled/voice/SKILL.md +25 -0
  142. package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
  143. package/packages/skills/src/index.ts +1 -0
  144. package/packages/skills/src/loader.ts +282 -0
  145. package/packages/tools/package.json +43 -0
  146. package/packages/tools/src/browser/browser.test.ts +111 -0
  147. package/packages/tools/src/browser/index.ts +272 -0
  148. package/packages/tools/src/canvas/index.ts +220 -0
  149. package/packages/tools/src/cron/cron.test.ts +164 -0
  150. package/packages/tools/src/cron/index.ts +304 -0
  151. package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
  152. package/packages/tools/src/filesystem/index.ts +379 -0
  153. package/packages/tools/src/git/index.ts +239 -0
  154. package/packages/tools/src/index.ts +4 -0
  155. package/packages/tools/src/shell/detect-env.ts +70 -0
  156. package/packages/tools/tsconfig.json +9 -0
@@ -0,0 +1,133 @@
1
+ import { z } from "zod";
2
+
3
+ // ─── Agent Roles ────────────────────────────────────────────────────────────
4
+
5
+ export const AgentRole = z.enum([
6
+ "architecture",
7
+ "development",
8
+ "testing",
9
+ "documentation",
10
+ ]);
11
+ export type AgentRole = z.infer<typeof AgentRole>;
12
+
13
+ // ─── Subagent Configuration ──────────────────────────────────────────────────
14
+
15
+ export const SubagentConfig = z.object({
16
+ role: AgentRole,
17
+ /** The CLI tool to use for this role (e.g. "opencode", "gemini", "qwen") */
18
+ cli: z.string(),
19
+ /** Extra args to pass to the CLI tool */
20
+ args: z.array(z.string()).default([]),
21
+ /** Working directory override. Defaults to monorepo root. */
22
+ cwd: z.string().optional(),
23
+ /** Max wallclock seconds before the process is auto-killed */
24
+ timeoutSeconds: z.number().default(600),
25
+ });
26
+ export type SubagentConfig = z.infer<typeof SubagentConfig>;
27
+
28
+ // ─── Telemetry event shapes sent over the local WebSocket mesh ──────────────
29
+
30
+ export const TelemetryEventType = z.enum([
31
+ "agent:started",
32
+ "agent:output", // stdout/stderr chunk
33
+ "agent:progress", // 0-100 progress hint emitted by the CLI tool
34
+ "agent:token_usage", // token counters
35
+ "agent:finished",
36
+ "agent:error",
37
+ "agent:cancelled",
38
+ "code-bridge:status",
39
+ ]);
40
+ export type TelemetryEventType = z.infer<typeof TelemetryEventType>;
41
+
42
+ const BaseEvent = z.object({
43
+ ts: z.number().default(() => Date.now()),
44
+ role: AgentRole,
45
+ });
46
+
47
+ export const AgentStartedEvent = BaseEvent.extend({
48
+ type: z.literal("agent:started"),
49
+ pid: z.number(),
50
+ cli: z.string(),
51
+ taskId: z.string(),
52
+ });
53
+
54
+ export const AgentOutputEvent = BaseEvent.extend({
55
+ type: z.literal("agent:output"),
56
+ taskId: z.string(),
57
+ stream: z.enum(["stdout", "stderr"]),
58
+ chunk: z.string(),
59
+ });
60
+
61
+ export const AgentProgressEvent = BaseEvent.extend({
62
+ type: z.literal("agent:progress"),
63
+ taskId: z.string(),
64
+ percent: z.number().min(0).max(100),
65
+ });
66
+
67
+ export const AgentTokenUsageEvent = BaseEvent.extend({
68
+ type: z.literal("agent:token_usage"),
69
+ taskId: z.string(),
70
+ inputTokens: z.number(),
71
+ outputTokens: z.number(),
72
+ model: z.string(),
73
+ });
74
+
75
+ export const AgentFinishedEvent = BaseEvent.extend({
76
+ type: z.literal("agent:finished"),
77
+ taskId: z.string(),
78
+ exitCode: z.number(),
79
+ });
80
+
81
+ export const AgentErrorEvent = BaseEvent.extend({
82
+ type: z.literal("agent:error"),
83
+ taskId: z.string(),
84
+ message: z.string(),
85
+ });
86
+
87
+ export const AgentCancelledEvent = BaseEvent.extend({
88
+ type: z.literal("agent:cancelled"),
89
+ taskId: z.string(),
90
+ });
91
+
92
+ export const CodeBridgeStatusEvent = z.object({
93
+ type: z.literal("code-bridge:status"),
94
+ ts: z.number().default(() => Date.now()),
95
+ agents: z.array(
96
+ z.object({
97
+ taskId: z.string(),
98
+ role: AgentRole,
99
+ cli: z.string(),
100
+ pid: z.number(),
101
+ state: z.enum(["running", "finished", "cancelled", "error"]),
102
+ progress: z.number(),
103
+ tokens: z.object({ input: z.number(), output: z.number() }),
104
+ model: z.string().optional(),
105
+ })
106
+ ),
107
+ });
108
+
109
+ export const TelemetryEvent = z.discriminatedUnion("type", [
110
+ AgentStartedEvent,
111
+ AgentOutputEvent,
112
+ AgentProgressEvent,
113
+ AgentTokenUsageEvent,
114
+ AgentFinishedEvent,
115
+ AgentErrorEvent,
116
+ AgentCancelledEvent,
117
+ CodeBridgeStatusEvent,
118
+ ]);
119
+ export type TelemetryEvent = z.infer<typeof TelemetryEvent>;
120
+
121
+ // ─── Dashboard → Code Bridge commands ──────────────────────────────────────
122
+
123
+ export const DashboardCommand = z.discriminatedUnion("cmd", [
124
+ z.object({
125
+ cmd: z.literal("launch"),
126
+ config: SubagentConfig,
127
+ taskId: z.string(),
128
+ prompt: z.string(),
129
+ }),
130
+ z.object({ cmd: z.literal("cancel"), taskId: z.string() }),
131
+ z.object({ cmd: z.literal("status") }),
132
+ ]);
133
+ export type DashboardCommand = z.infer<typeof DashboardCommand>;
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@johpaz/hive-core",
3
+ "version": "1.1.0",
4
+ "private": true,
5
+ "description": "Hive Gateway — Personal AI agent runtime",
6
+ "main": "./src/index.ts",
7
+ "module": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "license": "MIT",
10
+ "files": [
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "test": "bun test",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "@ag-ui/core": "^0.0.46",
19
+ "@johpaz/hive-mcp": "^1.1.0",
20
+ "@johpaz/hive-skills": "^1.1.0",
21
+ "@modelcontextprotocol/sdk": "latest",
22
+ "@sapphire/snowflake": "latest",
23
+ "@slack/bolt": "latest",
24
+ "@whiskeysockets/baileys": "latest",
25
+ "croner": "^10.0.1",
26
+ "discord.js": "latest",
27
+ "grammy": "latest",
28
+ "js-yaml": "latest",
29
+ "qrcode-terminal": "latest",
30
+ "toon-format-parser": "1.1.3",
31
+ "zod": "latest"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "latest",
35
+ "@types/bun": "latest"
36
+ },
37
+ "exports": {
38
+ ".": "./src/index.ts",
39
+ "./gateway": "./src/gateway/index.ts",
40
+ "./agent": "./src/agent/index.ts",
41
+ "./channels": "./src/channels/index.ts",
42
+ "./config": "./src/config/loader.ts",
43
+ "./utils": "./src/utils/logger.ts",
44
+ "./storage/sqlite": "./src/storage/sqlite.ts"
45
+ }
46
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Agent Loop — native implementation, no LangGraph.
3
+ *
4
+ * Replaces supervisor.ts + graph.ts.
5
+ *
6
+ * Pattern:
7
+ * user message → context compiler → model call → [tool call → model call]* → response
8
+ *
9
+ * Exposes an async generator compatible with the existing providers/index.ts stream API:
10
+ * yield { agent: { messages: [AIMessage] } }
11
+ * yield { tools: { messages: [ToolMessage] } }
12
+ *
13
+ * Also used directly by runAgentIsolated() for worker tasks.
14
+ */
15
+
16
+ import { logger } from "../utils/logger"
17
+ import { getDb } from "../storage/sqlite"
18
+ import { callLLM, resolveProviderConfig, type LLMMessage } from "./llm-client"
19
+ import { compileContext } from "./context-compiler"
20
+ import { executeTool } from "./native-tools"
21
+ import {
22
+ addMessage,
23
+ getRecentMessages,
24
+ toAPIMessages,
25
+ } from "./conversation-store"
26
+ import { saveTrace, recordLLMUsage } from "./tracer"
27
+ import { maybeCompact, clearOldToolResults } from "./compaction"
28
+ import { emitCanvas } from "../canvas/emitter"
29
+ import type { MCPClientManager } from "@johpaz/hive-mcp"
30
+
31
+ const log = logger.child("agent-loop")
32
+
33
+ // ─── Types ────────────────────────────────────────────────────────────────────
34
+
35
+ export interface AgentLoopOptions {
36
+ agentId: string
37
+ userMessage: string
38
+ threadId: string
39
+ channel?: string
40
+ mcpManager?: MCPClientManager | null
41
+ /** System prompt override (from server.ts config) */
42
+ systemPromptOverride?: string
43
+ /** Worker mode: isolated context + single-task execution */
44
+ isolated?: boolean
45
+ taskContext?: string
46
+ onStep?: (step: StepEvent) => Promise<void>
47
+ }
48
+
49
+ export interface StepEvent {
50
+ type: "text" | "tool_call" | "tool_result"
51
+ message: string
52
+ toolName?: string
53
+ isError?: boolean
54
+ }
55
+
56
+ // ─── Stream chunk types (compatible with providers/index.ts) ─────────────────
57
+
58
+ export interface StreamChunk {
59
+ agent?: { messages: any[] }
60
+ tools?: { messages: any[] }
61
+ }
62
+
63
+ // ─── Main agent loop ──────────────────────────────────────────────────────────
64
+
65
+ export async function* runAgent(
66
+ opts: AgentLoopOptions
67
+ ): AsyncGenerator<StreamChunk> {
68
+ const t0 = performance.now()
69
+ const db = getDb()
70
+
71
+ // Load agent config from DB
72
+ const agent = db.query<any, [string]>("SELECT * FROM agents WHERE id = ?").get(opts.agentId)
73
+ if (!agent) throw new Error(`Agent not found: ${opts.agentId}`)
74
+
75
+ const agentName = agent.name || opts.agentId
76
+ const maxIterations = agent.max_iterations || 10
77
+
78
+ // Resolve LLM provider config
79
+ const providerCfg = await resolveProviderConfig(
80
+ agent.provider_id || "openai",
81
+ agent.model_id || "gpt-4o-mini"
82
+ )
83
+
84
+ const cleanModel = providerCfg.model.replace(new RegExp(`^${providerCfg.provider}\\/`), "")
85
+ log.info(`[agent-loop] Starting: agent=${agentName} thread=${opts.threadId} provider=${providerCfg.provider}/${cleanModel}`)
86
+
87
+ emitCanvas("canvas:node_update", {
88
+ nodeId: agentName,
89
+ changes: { status: "thinking" },
90
+ })
91
+
92
+ // Store the user message in conversation history
93
+ if (!opts.isolated) {
94
+ addMessage(opts.threadId, "user", opts.userMessage, { channel: opts.channel })
95
+ // Run compaction if conversation history is getting large
96
+ await maybeCompact(opts.threadId)
97
+ }
98
+
99
+ // Compile context (system prompt + history + tools)
100
+ const ctx = await compileContext({
101
+ agentId: opts.agentId,
102
+ threadId: opts.threadId,
103
+ userMessage: opts.userMessage,
104
+ channel: opts.channel,
105
+ mcpManager: opts.mcpManager,
106
+ isolated: opts.isolated,
107
+ taskContext: opts.taskContext,
108
+ })
109
+
110
+ const systemPrompt = opts.systemPromptOverride || ctx.systemPrompt
111
+
112
+ // Build initial messages array for the model
113
+ let messages: LLMMessage[] = [
114
+ { role: "system", content: systemPrompt },
115
+ ...ctx.messages,
116
+ ]
117
+
118
+ // For isolated workers the user message is the task context, not from history
119
+ if (opts.isolated) {
120
+ messages.push({ role: "user", content: opts.userMessage })
121
+ }
122
+
123
+ let iterations = 0
124
+ let totalInputTokens = 0
125
+ let totalOutputTokens = 0
126
+ let finalContent = ""
127
+
128
+ // ── The loop ────────────────────────────────────────────────────────────
129
+ while (iterations < maxIterations) {
130
+ iterations++
131
+
132
+ const response = await callLLM({
133
+ ...providerCfg,
134
+ messages: clearOldToolResults(messages) as LLMMessage[],
135
+ tools: ctx.tools.length > 0 ? ctx.tools : undefined,
136
+ })
137
+
138
+ // Accumulate usage
139
+ if (response.usage) {
140
+ totalInputTokens += response.usage.input_tokens
141
+ totalOutputTokens += response.usage.output_tokens
142
+ }
143
+
144
+ // Emit agent chunk (compatible with providers/index.ts)
145
+ const agentMsg: any = { content: response.content }
146
+ if (response.tool_calls?.length) agentMsg.tool_calls = response.tool_calls
147
+ yield { agent: { messages: [agentMsg] } }
148
+
149
+ // Notify onStep for narration text
150
+ if (opts.onStep && response.content) {
151
+ await opts.onStep({ type: "text", message: response.content })
152
+ }
153
+
154
+ // ── No tool calls → final response ──────────────────────────────────
155
+ if (!response.tool_calls?.length || response.stop_reason !== "tool_calls") {
156
+ finalContent = response.content
157
+ // Save assistant message to history
158
+ if (!opts.isolated) {
159
+ addMessage(opts.threadId, "assistant", response.content)
160
+ }
161
+ break
162
+ }
163
+
164
+ // ── Tool calls → execute each tool ──────────────────────────────────
165
+ // Add assistant message with tool_calls to local messages array
166
+ messages.push({
167
+ role: "assistant",
168
+ content: response.content,
169
+ tool_calls: response.tool_calls,
170
+ })
171
+
172
+ for (const tc of response.tool_calls) {
173
+ const toolName = tc.function.name
174
+
175
+ emitCanvas("canvas:node_update", {
176
+ nodeId: agentName,
177
+ changes: { status: "tool_call", currentTool: toolName },
178
+ })
179
+
180
+ if (opts.onStep) {
181
+ if (response.content) {
182
+ await opts.onStep({ type: "text", message: response.content })
183
+ }
184
+ await opts.onStep({
185
+ type: "tool_call",
186
+ toolName,
187
+ message: `Calling tool: \`${toolName}\``,
188
+ })
189
+ }
190
+
191
+ const tTool = performance.now()
192
+ const toolResult = await executeTool(
193
+ ctx.allTools,
194
+ toolName,
195
+ tc.function.arguments,
196
+ {
197
+ user_id: process.env.HIVE_USER_ID || "",
198
+ thread_id: opts.threadId,
199
+ channel: opts.channel,
200
+ }
201
+ )
202
+ const toolMs = Math.round(performance.now() - tTool)
203
+
204
+ log.info(`[agent-loop] Tool ${toolName} completed in ${toolMs}ms`)
205
+
206
+ // Save tool call trace
207
+ saveTrace({
208
+ threadId: opts.threadId,
209
+ agentId: opts.agentId,
210
+ agentName,
211
+ toolUsed: toolName,
212
+ inputSummary: `${opts.userMessage.substring(0, 200)} → ${toolName}`,
213
+ outputSummary: toolResult.substring(0, 300),
214
+ success: !toolResult.startsWith("[Tool Error]"),
215
+ errorMessage: toolResult.startsWith("[Tool Error]") ? toolResult : null,
216
+ durationMs: toolMs,
217
+ })
218
+
219
+ // Emit tool chunk
220
+ yield { tools: { messages: [{ content: toolResult, tool_call_id: tc.id }] } }
221
+
222
+ if (opts.onStep) {
223
+ await opts.onStep({ type: "tool_result", message: toolResult })
224
+ }
225
+
226
+ // Add tool result to messages for next model call
227
+ messages.push({
228
+ role: "tool",
229
+ content: toolResult,
230
+ tool_call_id: tc.id,
231
+ })
232
+ }
233
+
234
+ emitCanvas("canvas:node_update", {
235
+ nodeId: agentName,
236
+ changes: { status: "thinking", currentTool: null },
237
+ })
238
+ }
239
+
240
+ // ── Post-loop ────────────────────────────────────────────────────────────
241
+ const durationMs = Math.round(performance.now() - t0)
242
+
243
+ emitCanvas("canvas:node_update", {
244
+ nodeId: agentName,
245
+ changes: { status: "idle", currentTool: null },
246
+ })
247
+
248
+ // Record usage
249
+ recordLLMUsage({
250
+ provider: providerCfg.provider,
251
+ model: providerCfg.model,
252
+ inputTokens: totalInputTokens,
253
+ outputTokens: totalOutputTokens,
254
+ })
255
+
256
+ // Save overall trace
257
+ saveTrace({
258
+ threadId: opts.threadId,
259
+ agentId: opts.agentId,
260
+ agentName,
261
+ inputSummary: opts.userMessage.substring(0, 300),
262
+ outputSummary: finalContent.substring(0, 300),
263
+ success: true,
264
+ durationMs,
265
+ tokensUsed: totalInputTokens + totalOutputTokens,
266
+ })
267
+
268
+ log.info(
269
+ `[agent-loop] Done: agent=${agentName} iterations=${iterations} ` +
270
+ `tokens=${totalInputTokens + totalOutputTokens} elapsed=${durationMs}ms`
271
+ )
272
+ }
273
+
274
+ // ─── Isolated worker execution (Fase 4.4) ───────────────────────────────────
275
+
276
+ /**
277
+ * Run a worker agent in an isolated context.
278
+ * Returns the final response string.
279
+ */
280
+ export async function runAgentIsolated(opts: {
281
+ agentId: string
282
+ taskDescription: string
283
+ threadId: string
284
+ mcpManager?: MCPClientManager | null
285
+ }): Promise<string> {
286
+ let lastContent = ""
287
+ for await (const chunk of runAgent({
288
+ agentId: opts.agentId,
289
+ userMessage: opts.taskDescription,
290
+ threadId: opts.threadId,
291
+ isolated: true,
292
+ taskContext: opts.taskDescription,
293
+ mcpManager: opts.mcpManager,
294
+ })) {
295
+ if (chunk.agent?.messages?.[0]?.content) {
296
+ lastContent = chunk.agent.messages[0].content
297
+ }
298
+ }
299
+ return lastContent
300
+ }
301
+
302
+ // ─── Shim: AgentLoop class with stream() compatible with providers/index.ts ──
303
+
304
+ export class AgentLoop {
305
+ private mcpManager: MCPClientManager | null = null
306
+
307
+ setMCPManager(m: MCPClientManager) {
308
+ this.mcpManager = m
309
+ }
310
+
311
+ /**
312
+ * Returns an async iterable that emits chunks compatible with
313
+ * the existing providers/index.ts stream consumer.
314
+ */
315
+ stream(
316
+ input: { messages: Array<{ role: string; content: string }> },
317
+ config: {
318
+ configurable?: {
319
+ thread_id?: string
320
+ agent_id?: string
321
+ user_id?: string
322
+ system_prompt?: string
323
+ channel?: string
324
+ }
325
+ }
326
+ ): AsyncIterable<StreamChunk> {
327
+ const threadId = config.configurable?.thread_id || process.env.HIVE_USER_ID || "default"
328
+ const agentId = config.configurable?.agent_id || this._resolveCoordinatorId()
329
+ const systemPromptOverride = config.configurable?.system_prompt
330
+ const channel = config.configurable?.channel
331
+
332
+ // Extract the last user message from the input
333
+ const lastUserMsg = [...input.messages].reverse().find((m) => m.role === "user")
334
+ const userMessage = lastUserMsg?.content || ""
335
+
336
+ return runAgent({
337
+ agentId,
338
+ userMessage,
339
+ threadId,
340
+ channel,
341
+ systemPromptOverride,
342
+ mcpManager: this.mcpManager,
343
+ })
344
+ }
345
+
346
+ private _resolveCoordinatorId(): string {
347
+ try {
348
+ const { getDb } = require("../storage/sqlite")
349
+ const db = getDb()
350
+ const row = db.query("SELECT id FROM agents WHERE is_coordinator = 1 LIMIT 1").get() as any
351
+ return row?.id || process.env.HIVE_AGENT_ID || "main"
352
+ } catch {
353
+ return process.env.HIVE_AGENT_ID || "main"
354
+ }
355
+ }
356
+ }
357
+
358
+ // Singleton
359
+ let _agentLoop: AgentLoop | null = null
360
+
361
+ export function getAgentLoop(): AgentLoop | null {
362
+ return _agentLoop
363
+ }
364
+
365
+ export function buildAgentLoop(opts: { mcpManager?: MCPClientManager | null } = {}): AgentLoop {
366
+ _agentLoop = new AgentLoop()
367
+ if (opts.mcpManager) _agentLoop.setMCPManager(opts.mcpManager)
368
+ return _agentLoop
369
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Compaction — Fase 6.
3
+ *
4
+ * Compresses conversation history when token count exceeds threshold.
5
+ * Uses the active LLM to summarize old messages, preserving:
6
+ * - User data and preferences
7
+ * - Decisions made
8
+ * - Tool results
9
+ * - Context needed to continue
10
+ *
11
+ * Saves summary to `summaries` table. Original messages are kept (audit trail)
12
+ * but the Context Compiler uses the summary instead of old messages.
13
+ *
14
+ * Also implements "tool result clearing": replaces old tool results with
15
+ * short summaries in the in-memory message array before model calls.
16
+ */
17
+
18
+ import { logger } from "../utils/logger"
19
+ import {
20
+ getTotalTokens,
21
+ getHistory,
22
+ getSummary,
23
+ saveSummary,
24
+ toAPIMessages,
25
+ estimateTokens,
26
+ getMessageCount,
27
+ } from "./conversation-store"
28
+ import { callLLM, resolveProviderConfig } from "./llm-client"
29
+ import { getDb } from "../storage/sqlite"
30
+
31
+ const log = logger.child("compaction")
32
+
33
+ // Token budget: compress when stored tokens exceed this threshold
34
+ const COMPACT_TOKEN_THRESHOLD = 6000 // ~60% of 10K context window
35
+ const KEEP_LAST_N_MESSAGES = 5 // always keep most recent N messages
36
+ const TOOL_RESULT_MAX_CHARS = 200 // max chars for old tool results after clearing
37
+ const MAX_TRANSCRIPT_MSGS = 30 // cap messages sent to summarizer (avoids OOM on small models)
38
+ const MAX_MSG_CHARS = 300 // chars per message in transcript
39
+
40
+ /**
41
+ * Check if compaction is needed and run it if so.
42
+ * Called at the start of each agent loop iteration.
43
+ */
44
+ export async function maybeCompact(threadId: string): Promise<void> {
45
+ try {
46
+ const totalTokens = getTotalTokens(threadId)
47
+ if (totalTokens < COMPACT_TOKEN_THRESHOLD) return
48
+
49
+ const summary = getSummary(threadId)
50
+ const totalMessages = getMessageCount(threadId)
51
+
52
+ // Already summarized up to near the current state
53
+ if (summary && summary.last_message_id > totalMessages - KEEP_LAST_N_MESSAGES) return
54
+
55
+ log.info(`[compaction] Compacting thread=${threadId} tokens=${totalTokens}`)
56
+ await compactThread(threadId)
57
+ } catch (err) {
58
+ log.warn("[compaction] Error during compaction check:", err)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Compress a thread's history into a summary.
64
+ */
65
+ export async function compactThread(threadId: string): Promise<void> {
66
+ const allMessages = getHistory(threadId)
67
+ if (allMessages.length <= KEEP_LAST_N_MESSAGES) return
68
+
69
+ const toSummarize = allMessages.slice(0, allMessages.length - KEEP_LAST_N_MESSAGES)
70
+ if (toSummarize.length === 0) return
71
+
72
+ const lastSummarizedId = toSummarize[toSummarize.length - 1].id
73
+
74
+ const existingSummary = getSummary(threadId)
75
+ if (existingSummary && existingSummary.last_message_id >= lastSummarizedId) return
76
+
77
+ // Cap transcript to avoid overflowing small model contexts
78
+ const capped = toSummarize.slice(-MAX_TRANSCRIPT_MSGS)
79
+ const apiMessages = toAPIMessages(capped)
80
+ const transcript = apiMessages
81
+ .map((m) => `[${m.role.toUpperCase()}]: ${m.content.substring(0, MAX_MSG_CHARS)}`)
82
+ .join("\n\n")
83
+
84
+ const db = getDb()
85
+ const coordinator = db.query<any, []>(
86
+ "SELECT provider_id, model_id FROM agents WHERE is_coordinator = 1 LIMIT 1"
87
+ ).get()
88
+
89
+ const providerCfg = await resolveProviderConfig(
90
+ coordinator?.provider_id || "openai",
91
+ coordinator?.model_id || "gpt-4o-mini"
92
+ )
93
+
94
+ const summaryResponse = await callLLM({
95
+ ...providerCfg,
96
+ messages: [
97
+ {
98
+ role: "system",
99
+ content:
100
+ "You are a conversation summarizer. Create a concise summary preserving: " +
101
+ "user preferences, decisions made, important facts, tool results, and context needed to continue.",
102
+ },
103
+ {
104
+ role: "user",
105
+ content: `Summarize this conversation (${toSummarize.length} messages) in 3-5 sentences:\n\n${transcript}`,
106
+ },
107
+ ],
108
+ })
109
+
110
+ const summary = summaryResponse.content.trim()
111
+ if (!summary) return
112
+
113
+ saveSummary(threadId, summary, toSummarize.length, lastSummarizedId)
114
+ log.info(
115
+ `[compaction] Thread ${threadId} compacted: ${toSummarize.length} msgs → ${estimateTokens(summary)} tokens`
116
+ )
117
+ }
118
+
119
+ /**
120
+ * Clear old tool results in-memory to reduce tokens before a model call.
121
+ * Does NOT modify the database — only the in-memory messages array.
122
+ */
123
+ export function clearOldToolResults<T extends { role: string; content: string }>(
124
+ messages: T[],
125
+ keepLastN = 6
126
+ ): T[] {
127
+ if (messages.length <= keepLastN) return messages
128
+ const cutoffIndex = messages.length - keepLastN
129
+
130
+ return messages.map((msg, i) => {
131
+ if (i >= cutoffIndex) return msg
132
+ if (msg.role === "tool" && msg.content.length > TOOL_RESULT_MAX_CHARS) {
133
+ return {
134
+ ...msg,
135
+ content: `[Result truncated: ${msg.content.substring(0, TOOL_RESULT_MAX_CHARS)}...]`,
136
+ }
137
+ }
138
+ return msg
139
+ })
140
+ }