@johpaz/hive-sdk 0.0.14 → 0.0.15
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/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -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 +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentService — Wrapper del nuevo agent-loop nativo.
|
|
3
|
+
*
|
|
4
|
+
* Reemplaza la clase Agent legacy.
|
|
5
|
+
* Mantiene compatibilidad con server.ts pero usa el agent-loop nativo por debajo.
|
|
6
|
+
*
|
|
7
|
+
* Responsabilidades:
|
|
8
|
+
* - Cargar agente desde DB
|
|
9
|
+
* - Cargar ética desde DB
|
|
10
|
+
* - Obtener MCP Manager
|
|
11
|
+
* - Hot reload (MCP, skills, ethics)
|
|
12
|
+
* - Eventos (cron, etc.)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getDb } from "../storage/SQLiteStorage.ts"
|
|
16
|
+
import { logger } from "../utils/logger.ts"
|
|
17
|
+
import { buildSystemPromptWithProjects } from "./PromptBuilder"
|
|
18
|
+
import { getAgentLoop, rebuildAgentLoop } from "./AgentRunner"
|
|
19
|
+
import type { MCPClientManager } from "../mcp/index.ts"
|
|
20
|
+
import { resolveAgentId, resolveUserId } from "../storage/onboarding.ts"
|
|
21
|
+
import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
|
|
22
|
+
import type { ContentPart } from "./providers/LLMClient"
|
|
23
|
+
|
|
24
|
+
const log = logger.child("agent-service")
|
|
25
|
+
|
|
26
|
+
// Event handler types
|
|
27
|
+
type CronHandler = (sessionId: string, task: string, jobId?: string, context?: any) => Promise<void>
|
|
28
|
+
|
|
29
|
+
export interface AgentServiceConfig {
|
|
30
|
+
agentId?: string
|
|
31
|
+
workspacePath?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AgentDBRecord {
|
|
35
|
+
id: string
|
|
36
|
+
user_id: string
|
|
37
|
+
name: string
|
|
38
|
+
description: string | null
|
|
39
|
+
system_prompt: string | null
|
|
40
|
+
tone: string | null
|
|
41
|
+
role: string
|
|
42
|
+
status: string
|
|
43
|
+
enabled: number
|
|
44
|
+
provider_id: string
|
|
45
|
+
model_id: string
|
|
46
|
+
tools_json: string | null
|
|
47
|
+
skills_json: string | null
|
|
48
|
+
parent_id: string | null
|
|
49
|
+
max_iterations: number
|
|
50
|
+
headers_encrypted: string | null
|
|
51
|
+
headers_iv: string | null
|
|
52
|
+
created_at: number
|
|
53
|
+
updated_at: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class AgentService {
|
|
57
|
+
private agentId: string
|
|
58
|
+
private workspacePath: string
|
|
59
|
+
private mcpManager: MCPClientManager | null = null
|
|
60
|
+
private cronHandlers: CronHandler[] = []
|
|
61
|
+
private initialized: boolean = false
|
|
62
|
+
|
|
63
|
+
constructor(config?: AgentServiceConfig) {
|
|
64
|
+
// Resolve agentId from database if not provided
|
|
65
|
+
this.agentId = config?.agentId || resolveAgentId(null) || "main"
|
|
66
|
+
this.workspacePath = config?.workspacePath || ""
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inicializa el servicio del agente
|
|
71
|
+
* - Carga el MCP Manager
|
|
72
|
+
* - Configura el supervisor graph
|
|
73
|
+
*/
|
|
74
|
+
async initialize(): Promise<void> {
|
|
75
|
+
if (this.initialized) {
|
|
76
|
+
log.debug("AgentService already initialized")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Obtener MCP Manager del agent loop
|
|
82
|
+
const agentLoop = getAgentLoop()
|
|
83
|
+
if (agentLoop) {
|
|
84
|
+
// MCP Manager se inicializa en el agent-loop
|
|
85
|
+
log.info("AgentService: MCP Manager available from agent-loop")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.initialized = true
|
|
89
|
+
log.info(`AgentService initialized for agent=${this.agentId}`)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
log.error(`Failed to initialize AgentService: ${(error as Error).message}`)
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Obtiene el registro del agente desde la DB
|
|
98
|
+
*/
|
|
99
|
+
async getAgent(agentId?: string): Promise<AgentDBRecord | null> {
|
|
100
|
+
const db = getDb()
|
|
101
|
+
const id = agentId || this.agentId
|
|
102
|
+
|
|
103
|
+
const agent = db.query<any, [string]>(
|
|
104
|
+
"SELECT * FROM agents WHERE id = ? LIMIT 1"
|
|
105
|
+
).get(id) as AgentDBRecord | undefined
|
|
106
|
+
|
|
107
|
+
return agent || null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Obtiene la ética desde la DB
|
|
112
|
+
*/
|
|
113
|
+
async getEthics(): Promise<string> {
|
|
114
|
+
const db = getDb()
|
|
115
|
+
const ethics = db.query<any, []>(
|
|
116
|
+
"SELECT content FROM ethics WHERE active = 1 LIMIT 1"
|
|
117
|
+
).get() as { content: string } | undefined
|
|
118
|
+
|
|
119
|
+
return ethics?.content || ""
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Obtiene el MCP Manager
|
|
124
|
+
*/
|
|
125
|
+
getMCPManager(): MCPClientManager | null {
|
|
126
|
+
const agentLoop = getAgentLoop()
|
|
127
|
+
if (agentLoop && (agentLoop as any).mcpManager) {
|
|
128
|
+
return (agentLoop as any).mcpManager as MCPClientManager
|
|
129
|
+
}
|
|
130
|
+
// Fallback to singleton
|
|
131
|
+
return getSingletonMCPManager()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Recarga la configuración del MCP
|
|
136
|
+
*/
|
|
137
|
+
async reloadMCP(): Promise<void> {
|
|
138
|
+
log.info("Reloading MCP configuration...")
|
|
139
|
+
const mcp = this.getMCPManager()
|
|
140
|
+
if (mcp) {
|
|
141
|
+
await mcp.reconnectAll().catch(err => {
|
|
142
|
+
log.warn(`Failed to reconnect MCP: ${(err as Error).message}`)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
log.info("MCP reloaded")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Recarga los skills desde la DB
|
|
150
|
+
*/
|
|
151
|
+
async reloadSkills(): Promise<void> {
|
|
152
|
+
log.info("Reloading skills...")
|
|
153
|
+
const { syncSkillsToFTS } = await import("./selectors/index.ts")
|
|
154
|
+
await syncSkillsToFTS()
|
|
155
|
+
log.info("Skills reloaded")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Recarga la ética desde la DB
|
|
160
|
+
*/
|
|
161
|
+
async reloadEthics(): Promise<void> {
|
|
162
|
+
log.info("Reloading ethics...")
|
|
163
|
+
// La ética se carga automáticamente en buildSystemPrompt()
|
|
164
|
+
// No hay acción necesaria aquí
|
|
165
|
+
log.info("Ethics reloaded (will be picked up on next agent call)")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Recarga el soul (system prompt del agente)
|
|
170
|
+
*/
|
|
171
|
+
async reloadSoul(): Promise<void> {
|
|
172
|
+
log.info("Reloading soul...")
|
|
173
|
+
// El soul se carga automáticamente desde DB en buildSystemPrompt()
|
|
174
|
+
log.info("Soul reloaded (will be picked up on next agent call)")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Recarga la configuración del usuario
|
|
179
|
+
*/
|
|
180
|
+
async reloadUser(): Promise<void> {
|
|
181
|
+
log.info("Reloading user configuration...")
|
|
182
|
+
// La configuración del usuario se carga desde DB en buildSystemPrompt()
|
|
183
|
+
log.info("User configuration reloaded (will be picked up on next agent call)")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Actualiza la configuración del agente
|
|
188
|
+
*/
|
|
189
|
+
async updateConfig(config: any): Promise<void> {
|
|
190
|
+
log.info("Updating agent configuration...")
|
|
191
|
+
// La configuración ahora se carga desde DB dinámicamente
|
|
192
|
+
// No hay acción necesaria aquí
|
|
193
|
+
log.info("Configuration updated (will be picked up from DB)")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Recarga el agente (hot reload)
|
|
198
|
+
*/
|
|
199
|
+
async reload(): Promise<void> {
|
|
200
|
+
log.info("Reloading agent...")
|
|
201
|
+
await this.reloadMCP()
|
|
202
|
+
await this.reloadSkills()
|
|
203
|
+
await this.reloadEthics()
|
|
204
|
+
log.info("Agent reloaded")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Registra un handler para eventos cron
|
|
209
|
+
*/
|
|
210
|
+
on(event: 'cron', handler: CronHandler): void {
|
|
211
|
+
if (event === 'cron') {
|
|
212
|
+
this.cronHandlers.push(handler)
|
|
213
|
+
log.debug(`Registered cron handler, total=${this.cronHandlers.length}`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Emite un evento cron
|
|
219
|
+
*/
|
|
220
|
+
emit(event: 'cron', sessionId: string, task: string, jobId?: string, context?: any): void {
|
|
221
|
+
if (event === 'cron') {
|
|
222
|
+
log.debug(`Emitting cron event: task=${task}, sessionId=${sessionId}, jobId=${jobId}`)
|
|
223
|
+
for (const handler of this.cronHandlers) {
|
|
224
|
+
handler(sessionId, task, jobId, context).catch(err => {
|
|
225
|
+
log.error(`Cron handler error: ${(err as Error).message}`)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Obtiene el system prompt para un agente
|
|
233
|
+
*/
|
|
234
|
+
async getSystemPrompt(agentId?: string, userId?: string): Promise<string> {
|
|
235
|
+
const id = agentId || this.agentId
|
|
236
|
+
const uid = userId || resolveUserId({}) || "default"
|
|
237
|
+
return buildSystemPromptWithProjects({ agentId: id, userId: uid })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Ejecuta un agente con un mensaje
|
|
242
|
+
*/
|
|
243
|
+
async runAgent(message: string | ContentPart[], threadId: string, userId?: string): Promise<string> {
|
|
244
|
+
const { runAgentIsolated } = await import("./AgentRunner.ts")
|
|
245
|
+
const result = await runAgentIsolated({
|
|
246
|
+
agentId: this.agentId,
|
|
247
|
+
taskDescription: message,
|
|
248
|
+
threadId,
|
|
249
|
+
})
|
|
250
|
+
return result
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Singleton para compatibilidad
|
|
255
|
+
let _agentService: AgentService | null = null
|
|
256
|
+
|
|
257
|
+
export function getAgentService(): AgentService {
|
|
258
|
+
if (!_agentService) {
|
|
259
|
+
_agentService = new AgentService()
|
|
260
|
+
}
|
|
261
|
+
return _agentService
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createAgentService(config?: AgentServiceConfig): AgentService {
|
|
265
|
+
_agentService = new AgentService(config)
|
|
266
|
+
return _agentService
|
|
267
|
+
}
|
|
@@ -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,12 @@
|
|
|
1
|
+
export * from "./AgentRunner.ts";
|
|
2
|
+
export * from "./Compaction.ts";
|
|
3
|
+
export * from "./ContextCompiler.ts";
|
|
4
|
+
export * from "./ContextGuard.ts";
|
|
5
|
+
export * from "./ConversationStore.ts";
|
|
6
|
+
export * from "./Hooks.ts";
|
|
7
|
+
export * from "./NativeTools.ts";
|
|
8
|
+
export * from "./PromptBuilder.ts";
|
|
9
|
+
export * from "./Service.ts";
|
|
10
|
+
export * from "./StuckLoop.ts";
|
|
11
|
+
export * from "./providers/index.ts";
|
|
12
|
+
export * from "./selectors/index.ts";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM client — direct official SDKs, no abstraction layers.
|
|
3
|
+
*
|
|
4
|
+
* gemini / google → native Gemini REST API (v1beta, ?key=)
|
|
5
|
+
* anthropic → @anthropic-ai/sdk
|
|
6
|
+
* ollama → ollama npm package
|
|
7
|
+
* everything else → openai npm package (OpenAI-compatible endpoint)
|
|
8
|
+
*
|
|
9
|
+
* Public interface (LLMMessage, callLLM, resolveProviderConfig) is stable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from "../../utils/logger.ts"
|
|
13
|
+
import { GeminiProvider } from "./gemini.ts"
|
|
14
|
+
import { AnthropicProvider } from "./anthropic.ts"
|
|
15
|
+
import { OllamaProvider } from "./ollama.ts"
|
|
16
|
+
import { OpenAICompatProvider } from "./openai-compat.ts"
|
|
17
|
+
import type { LLMProvider } from "./interface.ts"
|
|
18
|
+
|
|
19
|
+
const log = logger.child("llm-client")
|
|
20
|
+
|
|
21
|
+
// ─── Canonical types ───────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface LLMToolCall {
|
|
24
|
+
id: string
|
|
25
|
+
type: "function"
|
|
26
|
+
function: { name: string; arguments: string }
|
|
27
|
+
/** Gemini 3.x thought signature — must be round-tripped for tool-calling. */
|
|
28
|
+
thought_signature?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ContentPart =
|
|
32
|
+
| { type: "text"; text: string }
|
|
33
|
+
| { type: "image_url"; image_url: { url: string } }
|
|
34
|
+
| { type: "image_base64"; base64: string; mimeType: string }
|
|
35
|
+
| { type: "document"; base64: string; mimeType: string; fileName?: string }
|
|
36
|
+
|
|
37
|
+
export interface LLMMessage {
|
|
38
|
+
role: "system" | "user" | "assistant" | "tool"
|
|
39
|
+
content: string | ContentPart[]
|
|
40
|
+
tool_calls?: LLMToolCall[]
|
|
41
|
+
tool_call_id?: string
|
|
42
|
+
name?: string
|
|
43
|
+
/** Kimi K2 thinking mode — must be round-tripped when tool calls are present. */
|
|
44
|
+
reasoning_content?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LLMToolDef {
|
|
48
|
+
type: "function"
|
|
49
|
+
function: {
|
|
50
|
+
name: string
|
|
51
|
+
description: string
|
|
52
|
+
parameters: Record<string, unknown>
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LLMCallOptions {
|
|
57
|
+
provider: string
|
|
58
|
+
model: string
|
|
59
|
+
apiKey: string
|
|
60
|
+
baseUrl?: string
|
|
61
|
+
numCtx?: number
|
|
62
|
+
messages: LLMMessage[]
|
|
63
|
+
tools?: LLMToolDef[]
|
|
64
|
+
temperature?: number
|
|
65
|
+
maxTokens?: number
|
|
66
|
+
numGpu?: number
|
|
67
|
+
onToken?: (token: string) => void
|
|
68
|
+
signal?: AbortSignal
|
|
69
|
+
/** Enable extended thinking for supported models (Anthropic Claude 3.7+). */
|
|
70
|
+
thinking?: { enabled: boolean; budget_tokens?: number }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LLMResponse {
|
|
74
|
+
content: string
|
|
75
|
+
tool_calls?: LLMToolCall[]
|
|
76
|
+
stop_reason: "stop" | "tool_calls" | "max_tokens" | "error"
|
|
77
|
+
usage?: { input_tokens: number; output_tokens: number; thinking_tokens?: number }
|
|
78
|
+
/** Kimi K2 / DeepSeek thinking mode — must be round-tripped in assistant messages. */
|
|
79
|
+
reasoning_content?: string
|
|
80
|
+
/** Anthropic extended thinking content (not sent to LLM, for display only). */
|
|
81
|
+
thinking_content?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Provider factory ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const GEMINI_PROVIDERS = new Set(["gemini", "google"])
|
|
87
|
+
|
|
88
|
+
const KNOWN_PROVIDERS = new Set(["anthropic", "gemini", "google", "ollama", "openai", "groq", "mistral", "openrouter", "deepseek", "kimi", "local-llama", "nvidia"])
|
|
89
|
+
|
|
90
|
+
function getProvider(provider: string): LLMProvider {
|
|
91
|
+
if (GEMINI_PROVIDERS.has(provider)) return new GeminiProvider()
|
|
92
|
+
if (provider === "anthropic") return new AnthropicProvider()
|
|
93
|
+
if (provider === "ollama") return new OllamaProvider()
|
|
94
|
+
if (!KNOWN_PROVIDERS.has(provider)) {
|
|
95
|
+
log.warn(`[llm-client] Unknown provider "${provider}" — falling back to OpenAI-compatible endpoint`)
|
|
96
|
+
}
|
|
97
|
+
return new OpenAICompatProvider()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Call any LLM provider. Returns a canonical LLMResponse regardless of provider.
|
|
104
|
+
*/
|
|
105
|
+
export async function callLLM(options: LLMCallOptions): Promise<LLMResponse> {
|
|
106
|
+
try {
|
|
107
|
+
return await getProvider(options.provider).call(options)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const msg = (err as Error).message
|
|
110
|
+
const cleanModel = options.model.replace(new RegExp(`^${options.provider}\\/`), "")
|
|
111
|
+
log.error(`[llm-client] Error calling ${options.provider}/${cleanModel}: ${msg}`, err)
|
|
112
|
+
return { content: `[LLM Error] ${msg}`, stop_reason: "error" }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve provider config from DB (decrypts API key).
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveProviderConfig(
|
|
120
|
+
providerId: string,
|
|
121
|
+
modelId: string
|
|
122
|
+
): Promise<Pick<LLMCallOptions, "provider" | "model" | "apiKey" | "baseUrl" | "numCtx" | "numGpu">> {
|
|
123
|
+
const { getDb } = await import("../../storage/SQLiteStorage.ts")
|
|
124
|
+
const { decryptApiKey } = await import("../../storage/crypto.ts")
|
|
125
|
+
|
|
126
|
+
const db = getDb()
|
|
127
|
+
const providerRow = db
|
|
128
|
+
.query<any, [string]>("SELECT * FROM providers WHERE id = ? AND enabled = 1")
|
|
129
|
+
.get(providerId)
|
|
130
|
+
|
|
131
|
+
let apiKey = ""
|
|
132
|
+
if (providerRow?.api_key_encrypted && providerRow?.api_key_iv) {
|
|
133
|
+
try {
|
|
134
|
+
apiKey = await decryptApiKey(providerRow.api_key_encrypted, providerRow.api_key_iv)
|
|
135
|
+
} catch { /* fall through to env var */ }
|
|
136
|
+
}
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
apiKey = process.env[`${providerId.toUpperCase()}_API_KEY`] || ""
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
provider: providerId,
|
|
143
|
+
model: modelId,
|
|
144
|
+
apiKey,
|
|
145
|
+
baseUrl: providerRow?.base_url || undefined,
|
|
146
|
+
numCtx: providerRow?.num_ctx ?? undefined,
|
|
147
|
+
numGpu: providerRow?.num_gpu ?? undefined,
|
|
148
|
+
}
|
|
149
|
+
}
|