@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,212 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
3
|
+
import type { ContentPart, LLMMessage } from "./LLMClient.ts"
|
|
4
|
+
|
|
5
|
+
const log = logger.child("llm-client")
|
|
6
|
+
|
|
7
|
+
// Models that support extended thinking (claude-3-7+ and claude-4.x).
|
|
8
|
+
const THINKING_CAPABLE_MODELS = new Set([
|
|
9
|
+
"claude-3-7-sonnet-20250219",
|
|
10
|
+
"claude-sonnet-4-5",
|
|
11
|
+
"claude-sonnet-4-6",
|
|
12
|
+
"claude-opus-4-5",
|
|
13
|
+
"claude-opus-4-6",
|
|
14
|
+
"claude-opus-4-7",
|
|
15
|
+
"claude-haiku-4-5",
|
|
16
|
+
"claude-haiku-4-5-20251001",
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
function supportsThinking(model: string): boolean {
|
|
20
|
+
if (THINKING_CAPABLE_MODELS.has(model)) return true
|
|
21
|
+
// Also match any claude-4.x or claude-3-7+ by prefix
|
|
22
|
+
return /^claude-(4|3-7)/.test(model)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AnthropicProvider implements LLMProvider {
|
|
26
|
+
private _convertContentPart(part: ContentPart): any {
|
|
27
|
+
switch (part.type) {
|
|
28
|
+
case "text":
|
|
29
|
+
return { type: "text", text: part.text }
|
|
30
|
+
case "image_url": {
|
|
31
|
+
const url = part.image_url.url
|
|
32
|
+
if (url.startsWith("data:")) {
|
|
33
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/)
|
|
34
|
+
if (match) return { type: "image", source: { type: "base64", media_type: match[1], data: match[2] } }
|
|
35
|
+
}
|
|
36
|
+
return { type: "image", source: { type: "url", url } }
|
|
37
|
+
}
|
|
38
|
+
case "image_base64":
|
|
39
|
+
return { type: "image", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
|
|
40
|
+
case "document":
|
|
41
|
+
return { type: "document", source: { type: "base64", media_type: part.mimeType, data: part.base64 } }
|
|
42
|
+
default:
|
|
43
|
+
return { type: "text", text: JSON.stringify(part) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private _convertUserContent(msg: LLMMessage): any[] {
|
|
48
|
+
if (Array.isArray(msg.content)) {
|
|
49
|
+
return msg.content.map(p => this._convertContentPart(p))
|
|
50
|
+
}
|
|
51
|
+
return [{ type: "text", text: msg.content }]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async call(options: LLMCallOptions): Promise<LLMResponse> {
|
|
55
|
+
const Anthropic = await import("@anthropic-ai/sdk")
|
|
56
|
+
const client = new Anthropic.default({ apiKey: options.apiKey })
|
|
57
|
+
|
|
58
|
+
const systemText = options.messages
|
|
59
|
+
.filter((m) => m.role === "system")
|
|
60
|
+
.map((m) => m.content)
|
|
61
|
+
.join("\n\n")
|
|
62
|
+
|
|
63
|
+
const anthropicMessages: any[] = []
|
|
64
|
+
|
|
65
|
+
for (const msg of options.messages) {
|
|
66
|
+
if (msg.role === "system") continue
|
|
67
|
+
|
|
68
|
+
if (msg.role === "tool") {
|
|
69
|
+
const block = { type: "tool_result", tool_use_id: msg.tool_call_id, content: msg.content }
|
|
70
|
+
const last = anthropicMessages[anthropicMessages.length - 1]
|
|
71
|
+
if (last?.role === "user" && Array.isArray(last.content)) {
|
|
72
|
+
last.content.push(block)
|
|
73
|
+
} else {
|
|
74
|
+
anthropicMessages.push({ role: "user", content: [block] })
|
|
75
|
+
}
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (msg.role === "assistant" && msg.tool_calls?.length) {
|
|
80
|
+
const content: any[] = []
|
|
81
|
+
if (msg.content) content.push({ type: "text", text: msg.content })
|
|
82
|
+
for (const tc of msg.tool_calls) {
|
|
83
|
+
let input: Record<string, unknown>
|
|
84
|
+
try { input = JSON.parse(tc.function.arguments || "{}") } catch { input = {} }
|
|
85
|
+
content.push({ type: "tool_use", id: tc.id, name: tc.function.name, input })
|
|
86
|
+
}
|
|
87
|
+
anthropicMessages.push({ role: "assistant", content })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
anthropicMessages.push({ role: msg.role, content: Array.isArray(msg.content) ? this._convertUserContent(msg) : msg.content })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const tools: any[] = (options.tools ?? []).map((t) => ({
|
|
95
|
+
name: t.function.name,
|
|
96
|
+
description: t.function.description,
|
|
97
|
+
input_schema: t.function.parameters,
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const body: any = {
|
|
101
|
+
model: options.model,
|
|
102
|
+
max_tokens: options.maxTokens ?? 16384,
|
|
103
|
+
messages: anthropicMessages,
|
|
104
|
+
}
|
|
105
|
+
if (systemText) body.system = systemText
|
|
106
|
+
if (tools.length) body.tools = tools
|
|
107
|
+
|
|
108
|
+
// Extended thinking — only for supported models
|
|
109
|
+
const thinkingEnabled = options.thinking?.enabled && supportsThinking(options.model)
|
|
110
|
+
if (thinkingEnabled) {
|
|
111
|
+
body.thinking = { type: "enabled", budget_tokens: options.thinking?.budget_tokens ?? 10000 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
log.info(
|
|
115
|
+
`[llm-client] anthropic/${options.model} — ${anthropicMessages.length} msgs, ${tools.length} tools` +
|
|
116
|
+
(thinkingEnabled ? ` thinking=${body.thinking.budget_tokens}tok` : "")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
let content = ""
|
|
120
|
+
let thinking_content = ""
|
|
121
|
+
const tool_calls: LLMToolCall[] = []
|
|
122
|
+
|
|
123
|
+
// Streaming via messages.stream()
|
|
124
|
+
const useStream = true // Always stream for better UX
|
|
125
|
+
if (useStream) {
|
|
126
|
+
const stream = client.messages.stream({ ...body, ...(options.signal ? {} : {}) })
|
|
127
|
+
|
|
128
|
+
// Track partial tool inputs by index
|
|
129
|
+
const partialInputs: Record<number, string> = {}
|
|
130
|
+
const toolMeta: Record<number, { id: string; name: string }> = {}
|
|
131
|
+
|
|
132
|
+
for await (const event of stream) {
|
|
133
|
+
if (event.type === "content_block_start") {
|
|
134
|
+
if (event.content_block.type === "tool_use") {
|
|
135
|
+
toolMeta[event.index] = { id: event.content_block.id, name: event.content_block.name }
|
|
136
|
+
partialInputs[event.index] = ""
|
|
137
|
+
}
|
|
138
|
+
} else if (event.type === "content_block_delta") {
|
|
139
|
+
if (event.delta.type === "text_delta") {
|
|
140
|
+
content += event.delta.text
|
|
141
|
+
if (options.onToken) options.onToken(event.delta.text)
|
|
142
|
+
} else if (event.delta.type === "thinking_delta") {
|
|
143
|
+
thinking_content += event.delta.thinking
|
|
144
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
145
|
+
if (partialInputs[event.index] !== undefined) {
|
|
146
|
+
partialInputs[event.index] += event.delta.partial_json
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const finalMsg = await stream.finalMessage()
|
|
153
|
+
|
|
154
|
+
// Build tool_calls from accumulated partial inputs
|
|
155
|
+
for (const [idx, meta] of Object.entries(toolMeta)) {
|
|
156
|
+
const args = partialInputs[Number(idx)] ?? "{}"
|
|
157
|
+
tool_calls.push({
|
|
158
|
+
id: meta.id,
|
|
159
|
+
type: "function",
|
|
160
|
+
function: { name: meta.name, arguments: args },
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const usage = finalMsg.usage
|
|
165
|
+
return {
|
|
166
|
+
content,
|
|
167
|
+
thinking_content: thinking_content || undefined,
|
|
168
|
+
tool_calls: tool_calls.length ? tool_calls : undefined,
|
|
169
|
+
stop_reason:
|
|
170
|
+
finalMsg.stop_reason === "tool_use" ? "tool_calls"
|
|
171
|
+
: finalMsg.stop_reason === "max_tokens" ? "max_tokens"
|
|
172
|
+
: "stop",
|
|
173
|
+
usage: {
|
|
174
|
+
input_tokens: usage.input_tokens,
|
|
175
|
+
output_tokens: usage.output_tokens,
|
|
176
|
+
thinking_tokens: (usage as any).thinking_tokens ?? 0,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Non-streaming fallback (kept for reference, unreachable with useStream=true)
|
|
182
|
+
const response = await client.messages.create(body)
|
|
183
|
+
|
|
184
|
+
for (const block of response.content) {
|
|
185
|
+
if (block.type === "text") content = block.text
|
|
186
|
+
if (block.type === "thinking") thinking_content = (block as any).thinking ?? ""
|
|
187
|
+
if (block.type === "tool_use") {
|
|
188
|
+
let args: string
|
|
189
|
+
try { args = JSON.stringify(block.input) } catch { args = "{}" }
|
|
190
|
+
tool_calls.push({
|
|
191
|
+
id: block.id,
|
|
192
|
+
type: "function",
|
|
193
|
+
function: { name: block.name, arguments: args },
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
content,
|
|
200
|
+
thinking_content: thinking_content || undefined,
|
|
201
|
+
tool_calls: tool_calls.length ? tool_calls : undefined,
|
|
202
|
+
stop_reason:
|
|
203
|
+
response.stop_reason === "tool_use" ? "tool_calls"
|
|
204
|
+
: response.stop_reason === "max_tokens" ? "max_tokens"
|
|
205
|
+
: "stop",
|
|
206
|
+
usage: {
|
|
207
|
+
input_tokens: response.usage.input_tokens,
|
|
208
|
+
output_tokens: response.usage.output_tokens,
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import { sanitizeMessages } from "./interface"
|
|
3
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
4
|
+
import type { ContentPart, LLMMessage } from "./LLMClient.ts"
|
|
5
|
+
|
|
6
|
+
const log = logger.child("llm-client")
|
|
7
|
+
|
|
8
|
+
export class GeminiProvider implements LLMProvider {
|
|
9
|
+
private _convertContentPart(part: ContentPart): any {
|
|
10
|
+
switch (part.type) {
|
|
11
|
+
case "text":
|
|
12
|
+
return { text: part.text }
|
|
13
|
+
case "image_url": {
|
|
14
|
+
const url = part.image_url.url
|
|
15
|
+
if (url.startsWith("data:")) {
|
|
16
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/)
|
|
17
|
+
if (match) return { inlineData: { mimeType: match[1], data: match[2] } }
|
|
18
|
+
}
|
|
19
|
+
return { text: `[Image URL: ${url}]` }
|
|
20
|
+
}
|
|
21
|
+
case "image_base64":
|
|
22
|
+
return { inlineData: { mimeType: part.mimeType, data: part.base64 } }
|
|
23
|
+
case "document":
|
|
24
|
+
return { inlineData: { mimeType: part.mimeType, data: part.base64 } }
|
|
25
|
+
default:
|
|
26
|
+
return { text: JSON.stringify(part) }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private _convertUserParts(msg: LLMMessage): any[] {
|
|
31
|
+
if (Array.isArray(msg.content)) {
|
|
32
|
+
return msg.content.map(p => this._convertContentPart(p))
|
|
33
|
+
}
|
|
34
|
+
return [{ text: msg.content }]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async call(options: LLMCallOptions): Promise<LLMResponse> {
|
|
38
|
+
const { GoogleGenAI } = await import("@google/genai")
|
|
39
|
+
|
|
40
|
+
const clientOpts: any = { apiKey: options.apiKey }
|
|
41
|
+
if (options.baseUrl?.trim()) clientOpts.httpOptions = { baseUrl: options.baseUrl.trim() }
|
|
42
|
+
|
|
43
|
+
const ai = new GoogleGenAI(clientOpts)
|
|
44
|
+
|
|
45
|
+
const cleanMessages = sanitizeMessages(options.messages)
|
|
46
|
+
|
|
47
|
+
// Build toolCallId → name map for converting tool results
|
|
48
|
+
const toolNameMap = new Map<string, string>()
|
|
49
|
+
for (const msg of cleanMessages) {
|
|
50
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
51
|
+
for (const tc of msg.tool_calls) toolNameMap.set(tc.id, tc.function.name)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Convert canonical messages → Gemini Content[]
|
|
56
|
+
let systemText = ""
|
|
57
|
+
const rawContents: any[] = []
|
|
58
|
+
|
|
59
|
+
for (const msg of cleanMessages) {
|
|
60
|
+
if (msg.role === "system") {
|
|
61
|
+
systemText += (systemText ? "\n\n" : "") + msg.content
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (msg.role === "user") {
|
|
65
|
+
rawContents.push({ role: "user", parts: this._convertUserParts(msg) })
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (msg.role === "assistant") {
|
|
69
|
+
const parts: any[] = []
|
|
70
|
+
if (msg.content) parts.push({ text: msg.content })
|
|
71
|
+
for (const tc of msg.tool_calls ?? []) {
|
|
72
|
+
const fcPart: any = { functionCall: { name: tc.function.name, args: JSON.parse(tc.function.arguments || "{}") } }
|
|
73
|
+
if (tc.thought_signature) fcPart.thoughtSignature = tc.thought_signature
|
|
74
|
+
parts.push(fcPart)
|
|
75
|
+
}
|
|
76
|
+
if (parts.length) rawContents.push({ role: "model", parts })
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (msg.role === "tool") {
|
|
80
|
+
const fnName = toolNameMap.get(msg.tool_call_id || "") || msg.name || "tool"
|
|
81
|
+
const frPart = { functionResponse: { name: fnName, response: { output: msg.content } } }
|
|
82
|
+
const last = rawContents[rawContents.length - 1]
|
|
83
|
+
if (last?.role === "user" && Array.isArray(last.parts)) {
|
|
84
|
+
last.parts.push(frPart)
|
|
85
|
+
} else {
|
|
86
|
+
rawContents.push({ role: "user", parts: [frPart] })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Gemini constraint enforcement
|
|
92
|
+
const contents: any[] = rawContents
|
|
93
|
+
|
|
94
|
+
while (contents.length > 0 && contents[0].role === "model") {
|
|
95
|
+
log.warn(`[llm-client] Gemini: removed leading model turn (no preceding user turn)`)
|
|
96
|
+
contents.shift()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let changed = true
|
|
100
|
+
let safetyLimit = 10
|
|
101
|
+
while (changed && safetyLimit-- > 0) {
|
|
102
|
+
changed = false
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < contents.length; i++) {
|
|
105
|
+
const turn = contents[i]
|
|
106
|
+
const prev = i > 0 ? contents[i - 1] : null
|
|
107
|
+
|
|
108
|
+
// INV-3: merge consecutive model turns
|
|
109
|
+
if (turn.role === "model" && prev?.role === "model") {
|
|
110
|
+
prev.parts.push(...(turn.parts ?? []))
|
|
111
|
+
contents.splice(i, 1)
|
|
112
|
+
i--
|
|
113
|
+
changed = true
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// INV-1: model(fc) must come after user
|
|
118
|
+
if (turn.role === "model") {
|
|
119
|
+
const hasFc = turn.parts?.some((p: any) => p.functionCall)
|
|
120
|
+
if (hasFc && prev?.role !== "user") {
|
|
121
|
+
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionCall)
|
|
122
|
+
log.warn(`[llm-client] Gemini: stripped functionCall not after user turn (i=${i})`)
|
|
123
|
+
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
124
|
+
changed = true
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// INV-2: user(fr) must come after model(fc)
|
|
130
|
+
if (turn.role === "user") {
|
|
131
|
+
const hasFr = turn.parts?.some((p: any) => p.functionResponse)
|
|
132
|
+
const prevHasFc = prev?.role === "model" && prev?.parts?.some((p: any) => p.functionCall)
|
|
133
|
+
if (hasFr && !prevHasFc) {
|
|
134
|
+
turn.parts = (turn.parts ?? []).filter((p: any) => !p.functionResponse)
|
|
135
|
+
log.warn(`[llm-client] Gemini: stripped orphaned functionResponse (i=${i})`)
|
|
136
|
+
if (turn.parts.length === 0) { contents.splice(i, 1); i-- }
|
|
137
|
+
changed = true
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (safetyLimit <= 0) {
|
|
145
|
+
log.error(`[llm-client] Gemini: constraint enforcement loop exhausted — message history may still violate Gemini constraints`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const config: any = {}
|
|
149
|
+
if (systemText) config.systemInstruction = systemText
|
|
150
|
+
if (options.maxTokens) config.maxOutputTokens = options.maxTokens
|
|
151
|
+
if (options.temperature !== undefined) config.temperature = options.temperature
|
|
152
|
+
if (options.tools?.length) {
|
|
153
|
+
config.tools = [{
|
|
154
|
+
functionDeclarations: options.tools.map((t) => ({
|
|
155
|
+
name: t.function.name,
|
|
156
|
+
description: t.function.description,
|
|
157
|
+
parameters: t.function.parameters,
|
|
158
|
+
})),
|
|
159
|
+
}]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log.info(`[llm-client] gemini/${options.model} — ${contents.length} turns, ${options.tools?.length ?? 0} tools`)
|
|
163
|
+
|
|
164
|
+
const response = await ai.models.generateContent({ model: options.model, contents, config })
|
|
165
|
+
|
|
166
|
+
const candidate = response.candidates?.[0]
|
|
167
|
+
|
|
168
|
+
// Handle safety blocks explicitly
|
|
169
|
+
if (candidate?.finishReason === "SAFETY") {
|
|
170
|
+
log.warn(`[llm-client] Gemini: response blocked by safety filters (model=${options.model})`)
|
|
171
|
+
return {
|
|
172
|
+
content: "[Response blocked by Gemini safety filters]",
|
|
173
|
+
stop_reason: "stop",
|
|
174
|
+
usage: response.usageMetadata ? {
|
|
175
|
+
input_tokens: response.usageMetadata.promptTokenCount ?? 0,
|
|
176
|
+
output_tokens: 0,
|
|
177
|
+
} : undefined,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const parts: any[] = candidate?.content?.parts ?? []
|
|
182
|
+
|
|
183
|
+
let content = ""
|
|
184
|
+
const tool_calls: LLMToolCall[] = []
|
|
185
|
+
|
|
186
|
+
for (const part of parts) {
|
|
187
|
+
if (part.text) content += part.text
|
|
188
|
+
if (part.functionCall) {
|
|
189
|
+
tool_calls.push({
|
|
190
|
+
id: crypto.randomUUID(),
|
|
191
|
+
type: "function",
|
|
192
|
+
function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args ?? {}) },
|
|
193
|
+
thought_signature: part.thoughtSignature ?? undefined,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const stop_reason: LLMResponse["stop_reason"] =
|
|
199
|
+
tool_calls.length > 0 ? "tool_calls"
|
|
200
|
+
: candidate?.finishReason === "MAX_TOKENS" ? "max_tokens"
|
|
201
|
+
: "stop"
|
|
202
|
+
|
|
203
|
+
const usageMeta = response.usageMetadata
|
|
204
|
+
return {
|
|
205
|
+
content,
|
|
206
|
+
tool_calls: tool_calls.length ? tool_calls : undefined,
|
|
207
|
+
stop_reason,
|
|
208
|
+
usage: usageMeta ? {
|
|
209
|
+
input_tokens: usageMeta.promptTokenCount ?? 0,
|
|
210
|
+
output_tokens: usageMeta.candidatesTokenCount ?? 0,
|
|
211
|
+
thinking_tokens: (usageMeta as any).thoughtsTokenCount ?? 0,
|
|
212
|
+
} : undefined,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRunner — thin wrapper over the native AgentLoop.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the same public API (generate()) so server.ts doesn't need changes.
|
|
5
|
+
* Internally uses agent-loop.ts instead of LangGraph.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Config } from "../../config/loader.ts"
|
|
9
|
+
import { logger } from "../../utils/logger.ts"
|
|
10
|
+
import { getDb } from "../../storage/SQLiteStorage.ts"
|
|
11
|
+
import { getAgentLoop } from "../AgentRunner.ts"
|
|
12
|
+
import { resolveUserId, resolveAgentId } from "../../storage/onboarding.ts"
|
|
13
|
+
import type { ContentPart } from "../../multimodal/types.ts"
|
|
14
|
+
|
|
15
|
+
export type Provider = "openai" | "anthropic" | "gemini" | "mistral" | "kimi" | "ollama" | "openrouter" | "deepseek" | "nvidia"
|
|
16
|
+
|
|
17
|
+
import type { StepEvent } from "../AgentRunner.ts"
|
|
18
|
+
|
|
19
|
+
export interface ModelOptions {
|
|
20
|
+
provider?: Provider
|
|
21
|
+
model?: string
|
|
22
|
+
maxTokens?: number
|
|
23
|
+
temperature?: number
|
|
24
|
+
system?: string
|
|
25
|
+
messages: Array<{ role: string; content: string | ContentPart[] }>
|
|
26
|
+
tools?: Record<string, any>
|
|
27
|
+
maxSteps?: number
|
|
28
|
+
onToken?: (token: string) => void
|
|
29
|
+
onStep?: (step: StepEvent) => Promise<void>
|
|
30
|
+
threadId?: string
|
|
31
|
+
userId?: string
|
|
32
|
+
channel?: string
|
|
33
|
+
rawUserMessage?: string
|
|
34
|
+
signal?: AbortSignal
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ModelResponse {
|
|
38
|
+
content: string
|
|
39
|
+
toolCalls?: Array<{
|
|
40
|
+
id: string
|
|
41
|
+
name: string
|
|
42
|
+
args: Record<string, unknown>
|
|
43
|
+
}>
|
|
44
|
+
usage?: {
|
|
45
|
+
promptTokens: number
|
|
46
|
+
completionTokens: number
|
|
47
|
+
totalTokens: number
|
|
48
|
+
}
|
|
49
|
+
finishReason?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class AgentRunner {
|
|
53
|
+
private config: Config
|
|
54
|
+
|
|
55
|
+
constructor(config: Config) {
|
|
56
|
+
this.config = config
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async generate(options: ModelOptions): Promise<ModelResponse> {
|
|
60
|
+
const db = getDb()
|
|
61
|
+
// Resolve agentId from database (coordinator or first enabled)
|
|
62
|
+
const agentId = resolveAgentId(null) || "main"
|
|
63
|
+
|
|
64
|
+
// Resolve userId from database
|
|
65
|
+
const userId = options.userId || resolveUserId({})
|
|
66
|
+
if (!userId) {
|
|
67
|
+
throw new Error("No userId provided. Please complete onboarding first.")
|
|
68
|
+
}
|
|
69
|
+
const threadId = options.threadId || userId
|
|
70
|
+
|
|
71
|
+
const agentLoop = getAgentLoop()
|
|
72
|
+
if (!agentLoop) {
|
|
73
|
+
throw new Error("AgentLoop not initialized")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let lastAgentContent = ""
|
|
77
|
+
let accumulatedAgentContent = "" // Accumulate content from all agent chunks
|
|
78
|
+
let toolCalls: ModelResponse["toolCalls"] = []
|
|
79
|
+
let totalInputTokens = 0
|
|
80
|
+
let totalOutputTokens = 0
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const stream = agentLoop.stream(
|
|
84
|
+
{ messages: options.messages },
|
|
85
|
+
{
|
|
86
|
+
configurable: {
|
|
87
|
+
thread_id: threadId,
|
|
88
|
+
agent_id: agentId,
|
|
89
|
+
user_id: userId,
|
|
90
|
+
// system_prompt intentionally omitted — context-compiler builds it
|
|
91
|
+
channel: options.channel,
|
|
92
|
+
raw_user_message: options.rawUserMessage,
|
|
93
|
+
},
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
let chunkCount = 0
|
|
99
|
+
for await (const chunk of stream) {
|
|
100
|
+
chunkCount++
|
|
101
|
+
|
|
102
|
+
if (chunk.agent?.messages) {
|
|
103
|
+
const lastMsg = chunk.agent.messages[chunk.agent.messages.length - 1]
|
|
104
|
+
const hasToolCalls = (lastMsg as any)?.tool_calls?.length > 0
|
|
105
|
+
const contentLen = lastMsg?.content?.length ?? 0
|
|
106
|
+
const contentType = typeof lastMsg?.content
|
|
107
|
+
logger.info(
|
|
108
|
+
`[STREAM] chunk#${chunkCount} agent: contentLen=${contentLen} hasToolCalls=${hasToolCalls} contentType=${contentType}`
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (lastMsg?.content) {
|
|
112
|
+
const content = typeof lastMsg.content === "string"
|
|
113
|
+
? lastMsg.content
|
|
114
|
+
: Array.isArray(lastMsg.content)
|
|
115
|
+
? lastMsg.content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
|
|
116
|
+
: ""
|
|
117
|
+
lastAgentContent = content
|
|
118
|
+
// Accumulate non-empty content that's not just whitespace
|
|
119
|
+
if (content && content.trim().length > 0) {
|
|
120
|
+
accumulatedAgentContent += (accumulatedAgentContent ? "\n" : "") + content
|
|
121
|
+
logger.debug(`[STREAM] Accumulated content: total length=${accumulatedAgentContent.length}`)
|
|
122
|
+
} else {
|
|
123
|
+
logger.debug(`[STREAM] Content empty or whitespace only, skipping accumulation`)
|
|
124
|
+
}
|
|
125
|
+
if (options.onToken) options.onToken(content)
|
|
126
|
+
} else {
|
|
127
|
+
logger.debug(`[STREAM] No content in chunk, lastMsg.content is falsy`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (hasToolCalls) {
|
|
131
|
+
toolCalls = (lastMsg as any).tool_calls.map((tc: any) => ({
|
|
132
|
+
id: tc.id || tc.function?.name,
|
|
133
|
+
name: tc.function?.name || tc.name,
|
|
134
|
+
args: tc.function?.arguments
|
|
135
|
+
? (typeof tc.function.arguments === "string"
|
|
136
|
+
? JSON.parse(tc.function.arguments)
|
|
137
|
+
: tc.function.arguments)
|
|
138
|
+
: {},
|
|
139
|
+
}))
|
|
140
|
+
|
|
141
|
+
const narration = lastMsg?.content || ""
|
|
142
|
+
if (options.onStep && narration) {
|
|
143
|
+
await options.onStep({ type: "text", message: narration })
|
|
144
|
+
}
|
|
145
|
+
if (options.onStep) {
|
|
146
|
+
for (const tc of toolCalls) {
|
|
147
|
+
await options.onStep({
|
|
148
|
+
type: "tool_call",
|
|
149
|
+
toolName: tc.name,
|
|
150
|
+
message: `Calling tool: \`${tc.name}\``,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (chunk.tools?.messages) {
|
|
158
|
+
const lastMsg = chunk.tools.messages[chunk.tools.messages.length - 1]
|
|
159
|
+
if (lastMsg?.content && options.onStep) {
|
|
160
|
+
await options.onStep({
|
|
161
|
+
type: "tool_result",
|
|
162
|
+
message: typeof lastMsg.content === "string" ? lastMsg.content : "",
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (chunk.usage) {
|
|
168
|
+
totalInputTokens += chunk.usage.input_tokens
|
|
169
|
+
totalOutputTokens += chunk.usage.output_tokens
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logger.debug(`[STREAM] done. totalChunks=${chunkCount} lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
|
|
174
|
+
|
|
175
|
+
// Use accumulated content if lastAgentContent is empty (handles case where final chunk has no text)
|
|
176
|
+
const finalContent = lastAgentContent || accumulatedAgentContent
|
|
177
|
+
|
|
178
|
+
logger.info(`[STREAM] Returning response: finalContent length=${finalContent.length}, lastAgentContent length=${lastAgentContent.length}, accumulated length=${accumulatedAgentContent.length}`)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content: finalContent,
|
|
182
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
183
|
+
usage: {
|
|
184
|
+
promptTokens: totalInputTokens,
|
|
185
|
+
completionTokens: totalOutputTokens,
|
|
186
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
187
|
+
},
|
|
188
|
+
finishReason: "stop",
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.error("AgentRunner error:", error)
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function createAgentRunner(config: Config): AgentRunner {
|
|
198
|
+
return new AgentRunner(config)
|
|
199
|
+
}
|