@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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACE Reflector — analyzes recent traces and produces insights.
|
|
3
|
+
*
|
|
4
|
+
* Runs in the background (never blocks the main agent loop).
|
|
5
|
+
* Triggered by the tracer after N new traces.
|
|
6
|
+
*
|
|
7
|
+
* Output → `reflections` table → picked up by Curator.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logger } from "../utils/logger"
|
|
11
|
+
|
|
12
|
+
const log = logger.child("reflector")
|
|
13
|
+
|
|
14
|
+
const MAX_TRACES_TO_ANALYZE = 30
|
|
15
|
+
const MIN_TRACES_TO_RUN = 10
|
|
16
|
+
|
|
17
|
+
/** Main entry point — called from tracer.ts */
|
|
18
|
+
export async function runReflector(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const { getDb } = await import("../storage/sqlite")
|
|
21
|
+
const db = getDb()
|
|
22
|
+
|
|
23
|
+
// Fetch traces not yet covered by any reflection
|
|
24
|
+
const lastReflectionId = (db.query<any, []>(
|
|
25
|
+
"SELECT MAX(id) as mid FROM reflections"
|
|
26
|
+
).get() as any)?.mid ?? 0
|
|
27
|
+
|
|
28
|
+
// Get last processed trace ID from reflections
|
|
29
|
+
const lastProcessedTrace = (db.query<any, []>(
|
|
30
|
+
`SELECT MAX(CAST(json_each.value AS INTEGER)) as max_id
|
|
31
|
+
FROM reflections, json_each(trace_ids)
|
|
32
|
+
WHERE id = (SELECT MAX(id) FROM reflections)`
|
|
33
|
+
).get() as any)?.max_id ?? 0
|
|
34
|
+
|
|
35
|
+
const traces = db.query<any, [number, number]>(`
|
|
36
|
+
SELECT id, agent_id, agent_name, tool_used, input_summary,
|
|
37
|
+
output_summary, success, error_message, duration_ms, tokens_used, created_at
|
|
38
|
+
FROM traces
|
|
39
|
+
WHERE id > ?
|
|
40
|
+
ORDER BY id ASC
|
|
41
|
+
LIMIT ?
|
|
42
|
+
`).all(lastProcessedTrace, MAX_TRACES_TO_ANALYZE)
|
|
43
|
+
|
|
44
|
+
if (traces.length < MIN_TRACES_TO_RUN) {
|
|
45
|
+
log.debug(`[reflector] Not enough traces (${traces.length}/${MIN_TRACES_TO_RUN}), skipping`)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.info(`[reflector] Analyzing ${traces.length} traces...`)
|
|
50
|
+
|
|
51
|
+
const insights = analyzeTracesLocally(traces)
|
|
52
|
+
|
|
53
|
+
if (insights.length === 0) {
|
|
54
|
+
log.debug("[reflector] No insights generated")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const traceIds = JSON.stringify(traces.map((t: any) => t.id))
|
|
59
|
+
|
|
60
|
+
for (const insight of insights) {
|
|
61
|
+
db.query(`
|
|
62
|
+
INSERT INTO reflections (trace_ids, insight_type, description, affected_tools, affected_agents, confidence)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
64
|
+
`).run(
|
|
65
|
+
traceIds,
|
|
66
|
+
insight.type,
|
|
67
|
+
insight.description,
|
|
68
|
+
insight.affectedTools ? JSON.stringify(insight.affectedTools) : null,
|
|
69
|
+
insight.affectedAgents ? JSON.stringify(insight.affectedAgents) : null,
|
|
70
|
+
insight.confidence,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log.info(`[reflector] Generated ${insights.length} insights`)
|
|
75
|
+
|
|
76
|
+
// Trigger curator
|
|
77
|
+
const { runCurator } = await import("./curator")
|
|
78
|
+
await runCurator()
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log.warn("[reflector] Error during reflection:", err)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Local analysis (heuristic, no LLM call needed for basic patterns) ────────
|
|
85
|
+
|
|
86
|
+
interface Insight {
|
|
87
|
+
type: "success_pattern" | "failure_pattern" | "optimization" | "ethics_violation"
|
|
88
|
+
description: string
|
|
89
|
+
affectedTools?: string[]
|
|
90
|
+
affectedAgents?: string[]
|
|
91
|
+
confidence: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function analyzeTracesLocally(traces: any[]): Insight[] {
|
|
95
|
+
const insights: Insight[] = []
|
|
96
|
+
|
|
97
|
+
// ── Failure patterns ─────────────────────────────────────────────────────
|
|
98
|
+
const failures = traces.filter((t: any) => !t.success)
|
|
99
|
+
if (failures.length > 3) {
|
|
100
|
+
// Group by tool
|
|
101
|
+
const toolFailures: Record<string, number> = {}
|
|
102
|
+
for (const f of failures) {
|
|
103
|
+
if (f.tool_used) {
|
|
104
|
+
toolFailures[f.tool_used] = (toolFailures[f.tool_used] || 0) + 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const [tool, count] of Object.entries(toolFailures)) {
|
|
108
|
+
if (count >= 3) {
|
|
109
|
+
insights.push({
|
|
110
|
+
type: "failure_pattern",
|
|
111
|
+
description: `Tool '${tool}' failed ${count} times recently. Consider verifying its configuration or avoiding it for this type of task.`,
|
|
112
|
+
affectedTools: [tool],
|
|
113
|
+
confidence: Math.min(0.9, count / 10),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Slow tools ───────────────────────────────────────────────────────────
|
|
120
|
+
const slowThresholdMs = 5000
|
|
121
|
+
const slowTools: Record<string, number[]> = {}
|
|
122
|
+
for (const t of traces) {
|
|
123
|
+
if (t.tool_used && t.duration_ms > slowThresholdMs) {
|
|
124
|
+
if (!slowTools[t.tool_used]) slowTools[t.tool_used] = []
|
|
125
|
+
slowTools[t.tool_used].push(t.duration_ms)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const [tool, durations] of Object.entries(slowTools)) {
|
|
129
|
+
if (durations.length >= 3) {
|
|
130
|
+
const avg = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
|
|
131
|
+
insights.push({
|
|
132
|
+
type: "optimization",
|
|
133
|
+
description: `Tool '${tool}' is consistently slow (avg ${avg}ms). Cache results when possible or use a faster alternative.`,
|
|
134
|
+
affectedTools: [tool],
|
|
135
|
+
confidence: 0.6,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── High success pattern ─────────────────────────────────────────────────
|
|
141
|
+
const successByTool: Record<string, { ok: number; total: number }> = {}
|
|
142
|
+
for (const t of traces) {
|
|
143
|
+
if (!t.tool_used) continue
|
|
144
|
+
if (!successByTool[t.tool_used]) successByTool[t.tool_used] = { ok: 0, total: 0 }
|
|
145
|
+
successByTool[t.tool_used].total++
|
|
146
|
+
if (t.success) successByTool[t.tool_used].ok++
|
|
147
|
+
}
|
|
148
|
+
for (const [tool, stats] of Object.entries(successByTool)) {
|
|
149
|
+
if (stats.total >= 5 && stats.ok / stats.total >= 0.9) {
|
|
150
|
+
insights.push({
|
|
151
|
+
type: "success_pattern",
|
|
152
|
+
description: `Tool '${tool}' has a high success rate (${stats.ok}/${stats.total}). Prefer it for related tasks.`,
|
|
153
|
+
affectedTools: [tool],
|
|
154
|
+
confidence: stats.ok / stats.total,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── High token usage ─────────────────────────────────────────────────────
|
|
160
|
+
const highTokenTraces = traces.filter((t: any) => t.tokens_used > 4000)
|
|
161
|
+
if (highTokenTraces.length > 3) {
|
|
162
|
+
insights.push({
|
|
163
|
+
type: "optimization",
|
|
164
|
+
description: `${highTokenTraces.length} recent calls used >4000 tokens. Be more concise and use tool results as summaries, not raw dumps.`,
|
|
165
|
+
confidence: 0.7,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return insights
|
|
170
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service.ts — thin wrapper over the native AgentLoop.
|
|
3
|
+
* No LangChain dependency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getSupervisorGraph } from "./supervisor"
|
|
7
|
+
import { buildSystemPromptWithProjects } from "./prompt-builder"
|
|
8
|
+
import { getDb } from "../storage/sqlite"
|
|
9
|
+
|
|
10
|
+
export interface AgentRunOptions {
|
|
11
|
+
threadId: string
|
|
12
|
+
message: string
|
|
13
|
+
agentId?: string
|
|
14
|
+
userId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentRunResult {
|
|
18
|
+
content: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runAgent(options: AgentRunOptions): Promise<AgentRunResult> {
|
|
22
|
+
const { threadId, message, userId } = options
|
|
23
|
+
const db = getDb()
|
|
24
|
+
|
|
25
|
+
let agentId = options.agentId
|
|
26
|
+
if (!agentId) {
|
|
27
|
+
const coordinatorAgent = db
|
|
28
|
+
.query<any, []>("SELECT id FROM agents WHERE is_coordinator = 1 LIMIT 1")
|
|
29
|
+
.get()
|
|
30
|
+
agentId = coordinatorAgent?.id || "bee"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const systemPrompt = await buildSystemPromptWithProjects({ agentId, userId })
|
|
34
|
+
const agentLoop = getSupervisorGraph()
|
|
35
|
+
|
|
36
|
+
if (!agentLoop) {
|
|
37
|
+
return { content: "Agent not initialized. Please restart the gateway." }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let finalContent = ""
|
|
41
|
+
|
|
42
|
+
const stream = agentLoop.stream(
|
|
43
|
+
{ messages: [{ role: "user", content: message }] },
|
|
44
|
+
{
|
|
45
|
+
configurable: {
|
|
46
|
+
thread_id: threadId,
|
|
47
|
+
agent_id: agentId,
|
|
48
|
+
user_id: userId,
|
|
49
|
+
system_prompt: systemPrompt,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
for await (const chunk of stream) {
|
|
55
|
+
if (chunk.agent?.messages) {
|
|
56
|
+
const lastMsg = chunk.agent.messages[chunk.agent.messages.length - 1]
|
|
57
|
+
if (lastMsg?.content) {
|
|
58
|
+
finalContent = typeof lastMsg.content === "string" ? lastMsg.content : ""
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { content: finalContent }
|
|
64
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Config } from "../config/loader.ts";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { hashObject } from "../utils/crypto.ts";
|
|
4
|
+
|
|
5
|
+
interface ToolCallRecord {
|
|
6
|
+
toolName: string;
|
|
7
|
+
argsHash: string;
|
|
8
|
+
errorMessage?: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface StuckLoopState {
|
|
13
|
+
detected: boolean;
|
|
14
|
+
toolName: string;
|
|
15
|
+
count: number;
|
|
16
|
+
lastError?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class StuckLoopDetector {
|
|
20
|
+
private log = logger.child("stuck-loop");
|
|
21
|
+
private history: Map<string, ToolCallRecord[]> = new Map();
|
|
22
|
+
private readonly maxHistoryPerSession = 50;
|
|
23
|
+
private readonly triggerThreshold = 3;
|
|
24
|
+
|
|
25
|
+
constructor(_config: Config) {}
|
|
26
|
+
|
|
27
|
+
recordToolCall(
|
|
28
|
+
sessionId: string,
|
|
29
|
+
toolName: string,
|
|
30
|
+
args: Record<string, unknown>,
|
|
31
|
+
error?: string
|
|
32
|
+
): void {
|
|
33
|
+
let sessionHistory = this.history.get(sessionId);
|
|
34
|
+
if (!sessionHistory) {
|
|
35
|
+
sessionHistory = [];
|
|
36
|
+
this.history.set(sessionId, sessionHistory);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const record: ToolCallRecord = {
|
|
40
|
+
toolName,
|
|
41
|
+
argsHash: hashObject(args),
|
|
42
|
+
errorMessage: error,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
sessionHistory.push(record);
|
|
47
|
+
|
|
48
|
+
if (sessionHistory.length > this.maxHistoryPerSession) {
|
|
49
|
+
sessionHistory.shift();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.log.debug(`Recorded tool call: ${toolName} for session ${sessionId}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
check(sessionId: string): StuckLoopState {
|
|
56
|
+
const sessionHistory = this.history.get(sessionId) ?? [];
|
|
57
|
+
|
|
58
|
+
if (sessionHistory.length < this.triggerThreshold) {
|
|
59
|
+
return { detected: false, toolName: "", count: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const recent = sessionHistory.slice(-10);
|
|
63
|
+
const counts = new Map<string, { count: number; error?: string }>();
|
|
64
|
+
|
|
65
|
+
for (const record of recent) {
|
|
66
|
+
const key = `${record.toolName}:${record.argsHash}`;
|
|
67
|
+
const existing = counts.get(key);
|
|
68
|
+
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.count++;
|
|
71
|
+
if (record.errorMessage) {
|
|
72
|
+
existing.error = record.errorMessage;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
counts.set(key, { count: 1, error: record.errorMessage });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const [key, data] of counts) {
|
|
80
|
+
if (data.count >= this.triggerThreshold && data.error) {
|
|
81
|
+
const toolName = key.split(":")[0] ?? "unknown";
|
|
82
|
+
|
|
83
|
+
this.log.warn(`Stuck loop detected: ${toolName} called ${data.count} times with same args and error`);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
detected: true,
|
|
87
|
+
toolName,
|
|
88
|
+
count: data.count,
|
|
89
|
+
lastError: data.error,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { detected: false, toolName: "", count: 0 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getInterventionMessage(state: StuckLoopState): string | null {
|
|
98
|
+
if (!state.detected) return null;
|
|
99
|
+
|
|
100
|
+
if (state.count >= this.triggerThreshold + 1) {
|
|
101
|
+
return `CRITICAL: You have called ${state.toolName} ${state.count} times with the same arguments and it keeps failing with: "${state.lastError}". The user has been notified. You MUST try a completely different approach or ask the user for guidance.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `WARNING: You have called ${state.toolName} ${state.count} times with the same arguments and it keeps failing. You MUST try a completely different approach instead of repeating the same action.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
clear(sessionId: string): void {
|
|
108
|
+
this.history.delete(sessionId);
|
|
109
|
+
this.log.debug(`Cleared stuck loop history for session ${sessionId}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
prune(maxAgeMs: number = 30 * 60 * 1000): number {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
let pruned = 0;
|
|
115
|
+
|
|
116
|
+
for (const [sessionId, history] of this.history) {
|
|
117
|
+
const filtered = history.filter(r => now - r.timestamp < maxAgeMs);
|
|
118
|
+
|
|
119
|
+
if (filtered.length === 0) {
|
|
120
|
+
this.history.delete(sessionId);
|
|
121
|
+
pruned++;
|
|
122
|
+
} else if (filtered.length !== history.length) {
|
|
123
|
+
this.history.set(sessionId, filtered);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return pruned;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createStuckLoopDetector(config: Config): StuckLoopDetector {
|
|
132
|
+
return new StuckLoopDetector(config);
|
|
133
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supervisor.ts — compatibility shim over the native AgentLoop.
|
|
3
|
+
*
|
|
4
|
+
* Maintains the same external API used by initializer.ts and server.ts:
|
|
5
|
+
* buildSupervisorGraph({ mcpManager })
|
|
6
|
+
* getSupervisorGraph()
|
|
7
|
+
* rebuildSupervisorGraph({ mcpManager })
|
|
8
|
+
*
|
|
9
|
+
* Internally delegates to agent-loop.ts — no LangGraph dependency.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { buildAgentLoop, getAgentLoop } from "./agent-loop"
|
|
13
|
+
import type { MCPClientManager } from "@johpaz/hive-mcp"
|
|
14
|
+
import { logger } from "../utils/logger"
|
|
15
|
+
|
|
16
|
+
const log = logger.child("supervisor")
|
|
17
|
+
|
|
18
|
+
export async function buildSupervisorGraph(options: {
|
|
19
|
+
mcpManager?: MCPClientManager | null
|
|
20
|
+
}): Promise<ReturnType<typeof getAgentLoop>> {
|
|
21
|
+
log.info("[supervisor] Initializing native AgentLoop...")
|
|
22
|
+
const loop = buildAgentLoop({ mcpManager: options.mcpManager ?? null })
|
|
23
|
+
log.info("[supervisor] AgentLoop ready")
|
|
24
|
+
return loop
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getSupervisorGraph() {
|
|
28
|
+
return getAgentLoop()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function rebuildSupervisorGraph(options: {
|
|
32
|
+
mcpManager?: MCPClientManager | null
|
|
33
|
+
}): Promise<ReturnType<typeof getAgentLoop>> {
|
|
34
|
+
log.info("[supervisor] Rebuilding AgentLoop...")
|
|
35
|
+
return buildSupervisorGraph(options)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-export AgentStateType for any code that imports it (now a plain object)
|
|
39
|
+
export type AgentStateType = { messages: any[] }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracer — ACE Generator output.
|
|
3
|
+
*
|
|
4
|
+
* Records every agent execution to the `traces` table.
|
|
5
|
+
* Fire-and-forget (non-blocking). Also updates playbook helpful/harmful counts
|
|
6
|
+
* based on execution outcome.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from "../utils/logger"
|
|
10
|
+
|
|
11
|
+
const log = logger.child("tracer")
|
|
12
|
+
|
|
13
|
+
export interface TraceInput {
|
|
14
|
+
threadId: string
|
|
15
|
+
agentId: string
|
|
16
|
+
agentName: string
|
|
17
|
+
toolUsed?: string | null
|
|
18
|
+
inputSummary: string
|
|
19
|
+
outputSummary: string
|
|
20
|
+
success: boolean
|
|
21
|
+
errorMessage?: string | null
|
|
22
|
+
durationMs?: number
|
|
23
|
+
tokensUsed?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Save a trace record. Non-blocking — errors are swallowed so they never
|
|
28
|
+
* affect the main agent loop.
|
|
29
|
+
*/
|
|
30
|
+
export function saveTrace(trace: TraceInput): void {
|
|
31
|
+
// Run asynchronously so it never blocks the caller
|
|
32
|
+
Promise.resolve().then(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const { getDb } = await import("../storage/sqlite")
|
|
35
|
+
const db = getDb()
|
|
36
|
+
|
|
37
|
+
db.query(`
|
|
38
|
+
INSERT INTO traces
|
|
39
|
+
(thread_id, agent_id, agent_name, tool_used, input_summary,
|
|
40
|
+
output_summary, success, error_message, duration_ms, tokens_used)
|
|
41
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
42
|
+
`).run(
|
|
43
|
+
trace.threadId,
|
|
44
|
+
trace.agentId,
|
|
45
|
+
trace.agentName,
|
|
46
|
+
trace.toolUsed ?? null,
|
|
47
|
+
trace.inputSummary.substring(0, 500),
|
|
48
|
+
trace.outputSummary.substring(0, 500),
|
|
49
|
+
trace.success ? 1 : 0,
|
|
50
|
+
trace.errorMessage ?? null,
|
|
51
|
+
trace.durationMs ?? null,
|
|
52
|
+
trace.tokensUsed ?? null,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Trigger reflector check in background
|
|
56
|
+
checkReflectorTrigger().catch(() => { /* ignore */ })
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.warn("[tracer] Failed to save trace:", err)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Reflector trigger ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const REFLECTOR_TRACE_THRESHOLD = 20 // run reflector after N new traces
|
|
66
|
+
|
|
67
|
+
let _tracesSinceLastReflection = 0
|
|
68
|
+
|
|
69
|
+
async function checkReflectorTrigger(): Promise<void> {
|
|
70
|
+
_tracesSinceLastReflection++
|
|
71
|
+
if (_tracesSinceLastReflection < REFLECTOR_TRACE_THRESHOLD) return
|
|
72
|
+
_tracesSinceLastReflection = 0
|
|
73
|
+
|
|
74
|
+
// Lazy import to avoid circular deps
|
|
75
|
+
const { runReflector } = await import("./reflector")
|
|
76
|
+
runReflector().catch((err) => {
|
|
77
|
+
log.warn("[tracer] Reflector run failed:", err)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Usage recording ──────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export function recordLLMUsage(opts: {
|
|
84
|
+
provider: string
|
|
85
|
+
model: string
|
|
86
|
+
inputTokens: number
|
|
87
|
+
outputTokens: number
|
|
88
|
+
}): void {
|
|
89
|
+
Promise.resolve().then(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const { recordUsage } = await import("../storage/usage")
|
|
92
|
+
recordUsage({
|
|
93
|
+
provider: opts.provider,
|
|
94
|
+
model: opts.model,
|
|
95
|
+
inputTokens: opts.inputTokens,
|
|
96
|
+
outputTokens: opts.outputTokens,
|
|
97
|
+
toonSavedTokens: 0,
|
|
98
|
+
toonSavedCost: 0,
|
|
99
|
+
})
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
export interface WorkspaceLoader {
|
|
6
|
+
read(file: "soul" | "user" | "ethics"): Promise<string>;
|
|
7
|
+
write(file: "soul" | "user", content: string): Promise<void>;
|
|
8
|
+
patch(file: "soul" | "user", section: string, content: string): Promise<void>;
|
|
9
|
+
append(file: "user", section: string, content: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createWorkspaceLoader(workspacePath: string): WorkspaceLoader {
|
|
13
|
+
const log = logger.child("workspace");
|
|
14
|
+
|
|
15
|
+
function getFilePath(file: "soul" | "user" | "ethics"): string {
|
|
16
|
+
return path.join(workspacePath, `${file}.md`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readFile(file: "soul" | "user" | "ethics"): Promise<string> {
|
|
20
|
+
const filePath = getFilePath(file);
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function writeFile(file: "soul" | "user", content: string): Promise<void> {
|
|
28
|
+
const filePath = getFilePath(file);
|
|
29
|
+
const dir = path.dirname(filePath);
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
34
|
+
log.debug(`Wrote ${file}.md`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
read: async (file: "soul" | "user" | "ethics"): Promise<string> => {
|
|
39
|
+
return readFile(file);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
write: async (file: "soul" | "user", content: string): Promise<void> => {
|
|
43
|
+
await writeFile(file, content);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
patch: async (file: "soul" | "user", section: string, newContent: string): Promise<void> => {
|
|
47
|
+
const current = await readFile(file);
|
|
48
|
+
const sectionHeader = `## ${section}`;
|
|
49
|
+
|
|
50
|
+
let result: string;
|
|
51
|
+
const lines = current.split("\n");
|
|
52
|
+
const sectionStart = lines.findIndex((l) => l.trim() === sectionHeader);
|
|
53
|
+
|
|
54
|
+
if (sectionStart === -1) {
|
|
55
|
+
// Section doesn't exist, append at end
|
|
56
|
+
result = current + `\n\n${sectionHeader}\n\n${newContent}\n`;
|
|
57
|
+
} else {
|
|
58
|
+
// Find next section
|
|
59
|
+
let sectionEnd = lines.length;
|
|
60
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
61
|
+
if (lines[i].startsWith("## ")) {
|
|
62
|
+
sectionEnd = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Replace section content
|
|
68
|
+
const before = lines.slice(0, sectionStart + 1).join("\n");
|
|
69
|
+
const after = lines.slice(sectionEnd).join("\n");
|
|
70
|
+
result = `${before}\n${newContent}\n${after}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await writeFile(file, result);
|
|
74
|
+
log.debug(`Patched section "${section}" in ${file}.md`);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
append: async (file: "user", section: string, content: string): Promise<void> => {
|
|
78
|
+
const current = await readFile(file);
|
|
79
|
+
const sectionHeader = `## ${section}`;
|
|
80
|
+
|
|
81
|
+
let result: string;
|
|
82
|
+
const lines = current.split("\n");
|
|
83
|
+
const sectionStart = lines.findIndex((l) => l.trim() === sectionHeader);
|
|
84
|
+
|
|
85
|
+
if (sectionStart === -1) {
|
|
86
|
+
// Section doesn't exist, create it
|
|
87
|
+
result = current + `\n\n${sectionHeader}\n\n${content}\n`;
|
|
88
|
+
} else {
|
|
89
|
+
// Find next section
|
|
90
|
+
let sectionEnd = lines.length;
|
|
91
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
92
|
+
if (lines[i].startsWith("## ")) {
|
|
93
|
+
sectionEnd = i;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Append to section
|
|
99
|
+
const before = lines.slice(0, sectionEnd).join("\n");
|
|
100
|
+
const after = lines.slice(sectionEnd).join("\n");
|
|
101
|
+
const existingContent = lines.slice(sectionStart + 1, sectionEnd).join("\n").trim();
|
|
102
|
+
const newSectionContent = existingContent ? `${existingContent}\n\n${content}` : content;
|
|
103
|
+
result = `${before}\n${newSectionContent}\n${after}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await writeFile(file, result);
|
|
107
|
+
log.debug(`Appended to section "${section}" in ${file}.md`);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|