@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.
- package/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- 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
|
+
}
|