@johpaz/hive-sdk 0.1.6 → 0.3.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/CHANGELOG.md +441 -0
- package/README.md +21 -3
- package/package.json +27 -5
- package/packages/core/src/agent/acceptance-checks.ts +9 -9
- package/packages/core/src/agent/agent-catalog.ts +82 -25
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +30 -10
- package/packages/core/src/agent/context-compiler.ts +63 -36
- package/packages/core/src/agent/conversation-store.ts +167 -9
- package/packages/core/src/agent/curator.ts +16 -7
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +40 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +47 -7
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +22 -7
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +39 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +38 -15
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +293 -0
- package/packages/core/src/agent/tool-selector.ts +9 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +67 -2
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +161 -5
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/canvas/index.ts +9 -0
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +14 -5
- package/packages/core/src/ethics/EthicsGuard.ts +7 -1
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/index.ts +18 -0
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/events/tool-narration.ts +4 -0
- package/packages/core/src/gateway/channel-notify.ts +103 -6
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +18 -6
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +11 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/harness/executors.ts +493 -0
- package/packages/core/src/harness/index.ts +12 -2
- package/packages/core/src/hooks/index.ts +203 -0
- package/packages/core/src/images/index.ts +161 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +51 -19
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +54 -27
- package/packages/core/src/scheduler/cron/expression.ts +165 -0
- package/packages/core/src/scheduler/cron/index.ts +10 -0
- package/packages/core/src/scheduler/cron/job.ts +339 -0
- package/packages/core/src/scheduler/cron/next-run.ts +121 -0
- package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
- package/packages/core/src/scheduler/index.ts +21 -3
- package/packages/core/src/scheduler/integration.ts +25 -14
- package/packages/core/src/scheduler/types.ts +3 -18
- package/packages/core/src/services/agents.ts +268 -0
- package/packages/core/src/services/cron.ts +257 -0
- package/packages/core/src/services/endpoints.ts +289 -0
- package/packages/core/src/services/ethics.ts +107 -0
- package/packages/core/src/services/images.ts +212 -0
- package/packages/core/src/services/index.ts +112 -0
- package/packages/core/src/services/mcp.ts +201 -0
- package/packages/core/src/services/memory.ts +133 -0
- package/packages/core/src/services/models.ts +179 -0
- package/packages/core/src/services/providers.ts +152 -0
- package/packages/core/src/services/setup.ts +222 -0
- package/packages/core/src/services/skills.ts +241 -0
- package/packages/core/src/services/swarms.ts +307 -0
- package/packages/core/src/services/tools.ts +106 -0
- package/packages/core/src/sessions/index.ts +268 -0
- package/packages/core/src/sessions/resolve.ts +108 -0
- package/packages/core/src/skills/SkillLoader.ts +8 -1
- package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
- package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
- package/packages/core/src/skills/bundled-data.generated.ts +110 -12
- package/packages/core/src/storage/bootstrap.ts +107 -12
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +138 -2
- package/packages/core/src/storage/crypto.ts +35 -4
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +61 -45
- package/packages/core/src/storage/reconcile.ts +11 -6
- package/packages/core/src/storage/seed.ts +191 -23
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/swarm/types.ts +3 -18
- package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
- package/packages/core/src/tool-runtime/index.ts +129 -14
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +52 -4
- package/packages/core/src/tools/cron/index.ts +8 -8
- package/packages/core/src/tools/images/index.ts +130 -0
- package/packages/core/src/tools/index.ts +14 -1
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
- package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
- package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +141 -44
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +2 -2
- package/packages/core/src/tools/web/browser-navigate.ts +2 -2
- package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.ts +63 -384
- package/packages/core/src/tools/web/browser-session.ts +125 -0
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/computer-use.ts +553 -0
- package/packages/core/src/tools/web/index.ts +8 -1
- package/packages/core/src/tools/web/webview-backend.ts +460 -21
- package/packages/core/src/utils/index.ts +1 -0
- package/packages/core/src/utils/logger.ts +12 -4
- package/packages/core/src/utils/redact-binary.ts +17 -0
- package/packages/core/src/utils/toon.ts +1 -1
- package/packages/core/src/voice/index.ts +6 -6
- package/bun.lock +0 -859
- package/bunfig.toml +0 -9
- package/docs/API-AGENTS.md +0 -367
- package/docs/API-CONTEXT-COMPILER.md +0 -249
- package/docs/API-DAG-SCHEDULER.md +0 -273
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
- package/docs/API-WORKERS-EVENTS.md +0 -299
- package/docs/HIVE-HARNESS.md +0 -113
- package/docs/INDEX.md +0 -190
- package/docs/TEMPLATE-HIVE-APP.md +0 -360
- package/packages/cli/package.json +0 -17
- package/packages/cli/src/commands/create-app.test.ts +0 -180
- package/packages/core/package.json +0 -70
- package/packages/core/src/api/createAgent.test.ts +0 -160
- package/packages/core/src/canvas/canvas.test.ts +0 -36
- package/packages/core/src/channels/channels.test.ts +0 -18
- package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
- package/packages/core/src/gateway/gateway.test.ts +0 -38
- package/packages/core/src/memory/Scratchpad.test.ts +0 -68
- package/packages/core/src/scheduler/scheduler.test.ts +0 -15
- package/packages/core/src/skills/skills.test.ts +0 -62
- package/packages/core/src/swarm/swarm.test.ts +0 -24
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
- package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
- package/packages/core/src/tools/api/api-request.test.ts +0 -164
- package/packages/core/src/tools/web/browser-service.test.ts +0 -83
- package/packages/core/src/workers/workers.test.ts +0 -41
- package/scripts/bump-version.ts +0 -248
- package/scripts/generate-skill-bundle.ts +0 -108
- package/test/acceptance-checks.test.ts +0 -403
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/browser-backend.test.ts +0 -308
- package/test/catalog-agents-stay-enabled.test.ts +0 -117
- package/test/causal-events.test.ts +0 -117
- package/test/compaction.test.ts +0 -105
- package/test/context-compiler.test.ts +0 -269
- package/test/curator.test.ts +0 -130
- package/test/durable-queue.test.ts +0 -114
- package/test/harness-barrel.test.ts +0 -64
- package/test/hive-helpers.test.ts +0 -130
- package/test/hivedb-search.test.ts +0 -189
- package/test/internal-turns.test.ts +0 -166
- package/test/job-idempotency.test.ts +0 -68
- package/test/job-retry-backoff.test.ts +0 -184
- package/test/job-store.test.ts +0 -381
- package/test/llm-retry.test.ts +0 -97
- package/test/memory-perf.test.ts +0 -774
- package/test/minimal-loadout.test.ts +0 -78
- package/test/model-catalog.test.ts +0 -105
- package/test/preload.ts +0 -12
- package/test/reflector.test.ts +0 -320
- package/test/retention-cap.test.ts +0 -91
- package/test/retired-capabilities-pruned.test.ts +0 -192
- package/test/run-store.test.ts +0 -355
- package/test/scratchpad.test.ts +0 -74
- package/test/secrets-durability.test.ts +0 -119
- package/test/seed-model-reseed.test.ts +0 -155
- package/test/setup-agent-seed.test.ts +0 -264
- package/test/tool-inventory.test.ts +0 -65
- package/test/tool-runtime.test.ts +0 -258
- package/test/tool-selector-runtime-tools.test.ts +0 -117
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -11,26 +11,26 @@
|
|
|
11
11
|
* Public interface (LLMMessage, callLLM, resolveProviderConfig) is stable.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { logger } from "../utils/logger"
|
|
15
|
-
import { loadConfig } from "../config/loader"
|
|
16
|
-
import { withRetry, isRetryableError, type RetryPolicy } from "../resilience/retry"
|
|
17
|
-
import { GeminiProvider } from "./llm-providers/gemini"
|
|
18
|
-
import { AnthropicProvider } from "./llm-providers/anthropic"
|
|
19
|
-
import { OllamaProvider } from "./llm-providers/ollama"
|
|
20
|
-
import { OpenAIProvider } from "./llm-providers/openai"
|
|
21
|
-
import { GroqProvider } from "./llm-providers/groq"
|
|
22
|
-
import { MistralProvider } from "./llm-providers/mistral"
|
|
23
|
-
import { OpenRouterProvider } from "./llm-providers/openrouter"
|
|
24
|
-
import { DeepSeekProvider } from "./llm-providers/deepseek"
|
|
25
|
-
import { KimiProvider } from "./llm-providers/kimi"
|
|
26
|
-
import { NvidiaProvider } from "./llm-providers/nvidia"
|
|
27
|
-
import { QwenProvider } from "./llm-providers/qwen"
|
|
28
|
-
import { MiniMaxProvider } from "./llm-providers/minimax"
|
|
29
|
-
import { OpenCodeGoProvider } from "./llm-providers/opencode-go"
|
|
30
|
-
import { HiveAgentsProvider } from "./llm-providers/hiveagents"
|
|
31
|
-
import { ZaiProvider } from "./llm-providers/z-ai"
|
|
32
|
-
import { ModelScopeProvider } from "./llm-providers/modelscope"
|
|
33
|
-
import type { LLMProvider } from "./llm-providers/interface"
|
|
14
|
+
import { logger } from "../utils/logger.ts"
|
|
15
|
+
import { loadConfig } from "../config/loader.ts"
|
|
16
|
+
import { withRetry, isRetryableError, type RetryPolicy } from "../resilience/retry.ts"
|
|
17
|
+
import { GeminiProvider } from "./llm-providers/gemini.ts"
|
|
18
|
+
import { AnthropicProvider } from "./llm-providers/anthropic.ts"
|
|
19
|
+
import { OllamaProvider } from "./llm-providers/ollama.ts"
|
|
20
|
+
import { OpenAIProvider } from "./llm-providers/openai.ts"
|
|
21
|
+
import { GroqProvider } from "./llm-providers/groq.ts"
|
|
22
|
+
import { MistralProvider } from "./llm-providers/mistral.ts"
|
|
23
|
+
import { OpenRouterProvider } from "./llm-providers/openrouter.ts"
|
|
24
|
+
import { DeepSeekProvider } from "./llm-providers/deepseek.ts"
|
|
25
|
+
import { KimiProvider } from "./llm-providers/kimi.ts"
|
|
26
|
+
import { NvidiaProvider } from "./llm-providers/nvidia.ts"
|
|
27
|
+
import { QwenProvider } from "./llm-providers/qwen.ts"
|
|
28
|
+
import { MiniMaxProvider } from "./llm-providers/minimax.ts"
|
|
29
|
+
import { OpenCodeGoProvider } from "./llm-providers/opencode-go.ts"
|
|
30
|
+
import { HiveAgentsProvider } from "./llm-providers/hiveagents.ts"
|
|
31
|
+
import { ZaiProvider } from "./llm-providers/z-ai.ts"
|
|
32
|
+
import { ModelScopeProvider } from "./llm-providers/modelscope.ts"
|
|
33
|
+
import type { LLMProvider } from "./llm-providers/interface.ts"
|
|
34
34
|
|
|
35
35
|
const log = logger.child("llm-client")
|
|
36
36
|
|
|
@@ -182,20 +182,55 @@ export async function callLLM(options: LLMCallOptions): Promise<LLMResponse> {
|
|
|
182
182
|
const cleanModel = options.model.replace(new RegExp(`^${options.provider}\\/`), "")
|
|
183
183
|
const status = extractErrorStatus(err)
|
|
184
184
|
const modelUnavailable = status === 404 || status === 410
|
|
185
|
-
const msg =
|
|
186
|
-
? `El modelo "${cleanModel}" ya no existe en ${options.provider} (HTTP ${status}). `
|
|
187
|
-
+ `El proveedor lo retiró de su catálogo; reintentar no sirve. `
|
|
188
|
-
+ `Elegí otro modelo en Ajustes → Proveedores.`
|
|
189
|
-
: (err as Error).message
|
|
185
|
+
const msg = describeProviderFailure(err, status, options.provider, cleanModel)
|
|
190
186
|
log.error(`[llm-client] Error calling ${options.provider}/${cleanModel}: ${msg}`, err)
|
|
191
187
|
return {
|
|
192
|
-
content:
|
|
188
|
+
content: `${LLM_ERROR_PREFIX} ${msg}`,
|
|
193
189
|
stop_reason: "error",
|
|
194
190
|
error: { message: msg, status, modelUnavailable },
|
|
195
191
|
}
|
|
196
192
|
}
|
|
197
193
|
}
|
|
198
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Marks content that is a provider failure rather than something the agent
|
|
197
|
+
* said. webchat-turn.ts keys the failed-turn UI off this prefix, and
|
|
198
|
+
* agent-loop.ts keeps such content out of the conversation history.
|
|
199
|
+
*/
|
|
200
|
+
export const LLM_ERROR_PREFIX = "[LLM Error]"
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Turns a provider failure into something the user can act on.
|
|
204
|
+
*
|
|
205
|
+
* The raw SDK text is written for whoever is reading a stack trace: "429 status
|
|
206
|
+
* code (no body)" showed up verbatim in the chat and says nothing about what
|
|
207
|
+
* happened (the account hit its rate limit) or what to do about it. Statuses
|
|
208
|
+
* without a specific mapping keep the original message — a wrong explanation is
|
|
209
|
+
* worse than a technical one.
|
|
210
|
+
*/
|
|
211
|
+
export function describeProviderFailure(
|
|
212
|
+
err: unknown,
|
|
213
|
+
status: number | undefined,
|
|
214
|
+
provider: string,
|
|
215
|
+
cleanModel: string,
|
|
216
|
+
): string {
|
|
217
|
+
if (status === 404 || status === 410) {
|
|
218
|
+
return `El modelo "${cleanModel}" ya no existe en ${provider} (HTTP ${status}). `
|
|
219
|
+
+ `El proveedor lo retiró de su catálogo; reintentar no sirve. `
|
|
220
|
+
+ `Elige otro modelo en Ajustes → Proveedores.`
|
|
221
|
+
}
|
|
222
|
+
if (status === 429) {
|
|
223
|
+
return `${provider} está limitando las peticiones (HTTP 429) y los reintentos tampoco pasaron. `
|
|
224
|
+
+ `Es el límite de uso de tu cuenta, no un problema del modelo: esperá unos minutos, `
|
|
225
|
+
+ `revisa tu cuota con el proveedor, o cambiá de modelo en Ajustes → Proveedores.`
|
|
226
|
+
}
|
|
227
|
+
if (status === 401 || status === 403) {
|
|
228
|
+
return `${provider} rechazó la API key (HTTP ${status}). `
|
|
229
|
+
+ `Revisa que siga siendo válida y tenga acceso a "${cleanModel}" en Ajustes → Proveedores.`
|
|
230
|
+
}
|
|
231
|
+
return (err as Error).message
|
|
232
|
+
}
|
|
233
|
+
|
|
199
234
|
/** Provider SDKs disagree on where the HTTP status lands; check every shape we've seen. */
|
|
200
235
|
function extractErrorStatus(err: unknown): number | undefined {
|
|
201
236
|
const e = err as { status?: number; statusCode?: number; response?: { status?: number } }
|
|
@@ -208,10 +243,10 @@ function extractErrorStatus(err: unknown): number | undefined {
|
|
|
208
243
|
* Returns null when the DB has no usable LLM (e.g. fresh install before setup).
|
|
209
244
|
*/
|
|
210
245
|
export async function getDefaultLLM(): Promise<{ provider: string; model: string } | null> {
|
|
211
|
-
const { col, fromIndexable } = await import("../storage/hive")
|
|
212
|
-
const agentsCol = await col<import("../storage/collections").AgentDoc>("agents")
|
|
213
|
-
const modelsCol = await col<import("../storage/collections").ModelDoc>("models")
|
|
214
|
-
const providersCol = await col<import("../storage/collections").ProviderDoc>("providers")
|
|
246
|
+
const { col, fromIndexable } = await import("../storage/hive.ts")
|
|
247
|
+
const agentsCol = await col<import("../storage/collections.ts").AgentDoc>("agents")
|
|
248
|
+
const modelsCol = await col<import("../storage/collections.ts").ModelDoc>("models")
|
|
249
|
+
const providersCol = await col<import("../storage/collections.ts").ProviderDoc>("providers")
|
|
215
250
|
|
|
216
251
|
const coordinators = await agentsCol.findBy("role", "coordinator")
|
|
217
252
|
const coordinator = coordinators[0]
|
|
@@ -234,14 +269,34 @@ export async function getDefaultLLM(): Promise<{ provider: string; model: string
|
|
|
234
269
|
/**
|
|
235
270
|
* Resolve provider config from DB (decrypts API key).
|
|
236
271
|
*/
|
|
272
|
+
/**
|
|
273
|
+
* Credenciales y endpoint resueltos por quien llama, en vez de por el proceso.
|
|
274
|
+
*
|
|
275
|
+
* Sin esto, la única forma de elegir la key era el secret store de HiveDB o
|
|
276
|
+
* `process.env[PROVIDER_API_KEY]` — ambos globales al proceso. Un backend
|
|
277
|
+
* multi-tenant no tiene un proceso por inquilino: o mutaba `process.env` antes
|
|
278
|
+
* de cada llamada (y entonces dos workspaces concurrentes se pisaban la key), o
|
|
279
|
+
* levantaba un proceso por llamada sólo para aislarla. Pasar la credencial en la
|
|
280
|
+
* llamada elimina las dos cosas.
|
|
281
|
+
*
|
|
282
|
+
* Todo es opcional: lo que no venga acá se resuelve como siempre.
|
|
283
|
+
*/
|
|
284
|
+
export interface ProviderCredentials {
|
|
285
|
+
/** Reemplaza la key del secret store y la de `process.env`. */
|
|
286
|
+
apiKey?: string
|
|
287
|
+
/** Reemplaza el `base_url` de la fila del proveedor. */
|
|
288
|
+
baseUrl?: string
|
|
289
|
+
}
|
|
290
|
+
|
|
237
291
|
export async function resolveProviderConfig(
|
|
238
292
|
providerId: string,
|
|
239
|
-
modelId: string
|
|
293
|
+
modelId: string,
|
|
294
|
+
credentials?: ProviderCredentials
|
|
240
295
|
): Promise<Pick<LLMCallOptions, "provider" | "model" | "apiKey" | "baseUrl" | "numCtx" | "numGpu" | "contextWindow">> {
|
|
241
|
-
const { col } = await import("../storage/hive")
|
|
242
|
-
const { loadProviderApiKey } = await import("../storage/crypto")
|
|
243
|
-
const providersCol = await col<import("../storage/collections").ProviderDoc>("providers")
|
|
244
|
-
const modelsCol = await col<import("../storage/collections").ModelDoc>("models")
|
|
296
|
+
const { col } = await import("../storage/hive.ts")
|
|
297
|
+
const { loadProviderApiKey } = await import("../storage/crypto.ts")
|
|
298
|
+
const providersCol = await col<import("../storage/collections.ts").ProviderDoc>("providers")
|
|
299
|
+
const modelsCol = await col<import("../storage/collections.ts").ModelDoc>("models")
|
|
245
300
|
|
|
246
301
|
const providerEntry = await providersCol.get(providerId)
|
|
247
302
|
const providerRow = (providerEntry?.doc.enabled && providerEntry?.doc.active) ? providerEntry.doc : undefined
|
|
@@ -249,7 +304,13 @@ export async function resolveProviderConfig(
|
|
|
249
304
|
// Load model's context window for token budget management
|
|
250
305
|
const modelEntry = await modelsCol.get(modelId)
|
|
251
306
|
|
|
252
|
-
|
|
307
|
+
// La credencial de la llamada gana, y corta acá: si vino una key explícita no
|
|
308
|
+
// se consulta el secret store ni el entorno, así que un fallback global no
|
|
309
|
+
// puede colarse en el lugar de la del inquilino.
|
|
310
|
+
let apiKey = credentials?.apiKey
|
|
311
|
+
if (!apiKey) {
|
|
312
|
+
apiKey = await loadProviderApiKey(providerId)
|
|
313
|
+
}
|
|
253
314
|
if (!apiKey) {
|
|
254
315
|
apiKey = process.env[`${providerId.toUpperCase()}_API_KEY`] || ""
|
|
255
316
|
}
|
|
@@ -258,7 +319,7 @@ export async function resolveProviderConfig(
|
|
|
258
319
|
provider: providerId,
|
|
259
320
|
model: modelId,
|
|
260
321
|
apiKey,
|
|
261
|
-
baseUrl: providerRow?.base_url || undefined,
|
|
322
|
+
baseUrl: credentials?.baseUrl || providerRow?.base_url || undefined,
|
|
262
323
|
numCtx: providerRow?.num_ctx ?? undefined,
|
|
263
324
|
numGpu: providerRow?.num_gpu ?? undefined,
|
|
264
325
|
contextWindow: modelEntry?.doc.context_window ?? undefined,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger"
|
|
2
|
-
import { normalizeToolName, resolveMaxTokens, ensureArrayItems } from "./interface"
|
|
3
|
-
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall, ThinkingBlock } from "./interface"
|
|
4
|
-
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import { normalizeToolName, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
|
|
3
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall, ThinkingBlock } from "./interface.ts"
|
|
4
|
+
import type { ContentPart, LLMMessage } from "../llm-client.ts"
|
|
5
5
|
|
|
6
6
|
const log = logger.child("llm-client")
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger"
|
|
2
|
-
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
|
|
3
|
-
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
4
|
-
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
|
|
3
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
|
|
4
|
+
import type { ContentPart, LLMMessage } from "../llm-client.ts"
|
|
5
5
|
|
|
6
6
|
const log = logger.child("llm-client")
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger"
|
|
2
|
-
import { OpenAICompatBase } from "./openai-compat-base"
|
|
3
|
-
import type { LLMCallOptions, LLMResponse } from "./interface"
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import { OpenAICompatBase } from "./openai-compat-base.ts"
|
|
3
|
+
import type { LLMCallOptions, LLMResponse } from "./interface.ts"
|
|
4
4
|
|
|
5
5
|
const log = logger.child("llm-client")
|
|
6
6
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared types and utilities for LLM providers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock } from "../llm-client"
|
|
5
|
+
import type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock } from "../llm-client.ts"
|
|
6
6
|
export type { LLMCallOptions, LLMMessage, LLMResponse, LLMToolCall, ContentPart, ThinkingBlock }
|
|
7
7
|
|
|
8
|
-
import { logger } from "../../utils/logger"
|
|
8
|
+
import { logger } from "../../utils/logger.ts"
|
|
9
9
|
const log = logger.child("llm-client")
|
|
10
10
|
|
|
11
11
|
// ─── Provider interface ────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,44 @@
|
|
|
1
|
-
import { OpenAICompatBase } from "./openai-compat-base"
|
|
1
|
+
import { OpenAICompatBase } from "./openai-compat-base.ts"
|
|
2
|
+
import type { LLMCallOptions } from "../llm-client.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NIM mantiene el razonamiento APAGADO por defecto en su endpoint compatible
|
|
6
|
+
* con OpenAI, y el interruptor no es el `reasoning_effort` de OpenAI sino
|
|
7
|
+
* `chat_template_kwargs`, con una clave distinta por familia de modelo.
|
|
8
|
+
*
|
|
9
|
+
* Verificado en vivo contra integrate.api.nvidia.com (2026-08-15):
|
|
10
|
+
* `z-ai/glm-5.2` no emite un solo `reasoning_content` sin esto — el delta trae
|
|
11
|
+
* únicamente `role` y `content` — y sí lo emite con `enable_thinking`. Por eso
|
|
12
|
+
* la app no mostraba razonamiento con los modelos de NVIDIA: no llegaba.
|
|
13
|
+
*
|
|
14
|
+
* Las familias que no están acá se dejan en paz a propósito. Nemotron 3 ya
|
|
15
|
+
* emite `reasoning_content` sin ningún kwarg (también verificado), y mandarle
|
|
16
|
+
* una clave que su plantilla no acepta es justamente lo que rompe la llamada.
|
|
17
|
+
*/
|
|
18
|
+
const THINKING_KWARGS: Array<{ pattern: RegExp; kwargs: Record<string, unknown> }> = [
|
|
19
|
+
// Verificado con z-ai/glm-5.2.
|
|
20
|
+
{ pattern: /glm/i, kwargs: { enable_thinking: true, clear_thinking: false } },
|
|
21
|
+
// Verificado con minimaxai/minimax-m3.
|
|
22
|
+
{ pattern: /minimax/i, kwargs: { thinking_mode: "enabled" } },
|
|
23
|
+
// Documentado por NVIDIA, sin verificar: moonshotai/kimi-k2.6 responde 404
|
|
24
|
+
// en la cuenta con la que se probó. Si la plantilla lo rechaza, el reintento
|
|
25
|
+
// de openai-compat-base repite la llamada sin kwargs.
|
|
26
|
+
{ pattern: /kimi|deepseek/i, kwargs: { thinking: true } },
|
|
27
|
+
{ pattern: /qwen|qwq/i, kwargs: { enable_thinking: true } },
|
|
28
|
+
]
|
|
2
29
|
|
|
3
30
|
export class NvidiaProvider extends OpenAICompatBase {
|
|
4
31
|
constructor() { super("nvidia") }
|
|
32
|
+
|
|
33
|
+
protected modifyRequestBody(body: any, options: LLMCallOptions): any {
|
|
34
|
+
if (!options.thinking?.enabled) return body
|
|
35
|
+
|
|
36
|
+
const match = THINKING_KWARGS.find(({ pattern }) => pattern.test(options.model))
|
|
37
|
+
if (!match) return body
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...body,
|
|
41
|
+
chat_template_kwargs: { ...(body.chat_template_kwargs ?? {}), ...match.kwargs },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
5
44
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger"
|
|
2
|
-
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface"
|
|
3
|
-
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
4
|
-
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
|
+
import { sanitizeMessages, resolveMaxTokens, ensureArrayItems } from "./interface.ts"
|
|
3
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
|
|
4
|
+
import type { ContentPart, LLMMessage } from "../llm-client.ts"
|
|
5
5
|
|
|
6
6
|
const log = logger.child("llm-client")
|
|
7
7
|
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
import { logger } from "../../utils/logger"
|
|
1
|
+
import { logger } from "../../utils/logger.ts"
|
|
2
2
|
import {
|
|
3
3
|
sanitizeMessages, requiresTemperature1, OPENAI_COMPAT_BASE_URLS,
|
|
4
4
|
getProviderProfile, modelSupportsTools, normalizeToolName, normalizeToolSchema,
|
|
5
5
|
resolveMaxTokens,
|
|
6
|
-
} from "./interface"
|
|
7
|
-
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface"
|
|
8
|
-
import type { ContentPart, LLMMessage } from "../llm-client"
|
|
6
|
+
} from "./interface.ts"
|
|
7
|
+
import type { LLMCallOptions, LLMProvider, LLMResponse, LLMToolCall } from "./interface.ts"
|
|
8
|
+
import type { ContentPart, LLMMessage } from "../llm-client.ts"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Statuses that mean "this body is malformed or unsupported" — the only ones
|
|
12
|
+
* where dropping the provider extras can possibly help.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately narrow. A first version retried on any status and immediately
|
|
15
|
+
* misfired in production against a 429: a rate limit is not a body problem, so
|
|
16
|
+
* the retry spent a second request against the very limit that rejected the
|
|
17
|
+
* first, and turned thinking off for nothing. Same for 401/403 (auth) and 404
|
|
18
|
+
* (unknown model).
|
|
19
|
+
*/
|
|
20
|
+
const EXTRAS_REJECTED_CODES = [400, 422]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Drops the non-standard fields a provider added on top of an OpenAI-shaped
|
|
24
|
+
* body, for use on a retry after the body itself was rejected. Right now that
|
|
25
|
+
* is only NIM's `chat_template_kwargs` (nvidia.ts): it enables the model's
|
|
26
|
+
* thinking, so losing it costs the reasoning display and nothing else — a far
|
|
27
|
+
* better outcome than a turn that dies because one model's chat template did
|
|
28
|
+
* not recognize the switch.
|
|
29
|
+
*/
|
|
30
|
+
function stripProviderExtras(body: any): any {
|
|
31
|
+
if (!body?.chat_template_kwargs) return body
|
|
32
|
+
const { chat_template_kwargs: _dropped, ...rest } = body
|
|
33
|
+
return rest
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
const log = logger.child("llm-client")
|
|
11
37
|
|
|
@@ -44,7 +70,11 @@ function compactBodyForContextOverflow(body: any, err: any): void {
|
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
export abstract class OpenAICompatBase implements LLMProvider {
|
|
47
|
-
|
|
73
|
+
protected readonly providerName: string
|
|
74
|
+
|
|
75
|
+
constructor(providerName: string) {
|
|
76
|
+
this.providerName = providerName
|
|
77
|
+
}
|
|
48
78
|
|
|
49
79
|
/** Override to true when the provider requires reasoning_content to be round-tripped. */
|
|
50
80
|
protected needsReasoningRoundtrip(): boolean { return false }
|
|
@@ -183,7 +213,14 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
183
213
|
delete bodyNoTools.tools
|
|
184
214
|
delete bodyNoTools.tool_choice
|
|
185
215
|
delete bodyNoTools.parallel_tool_calls
|
|
186
|
-
response = await client.chat.completions.create(this.modifyRequestBody(bodyNoTools, options), { signal: options.signal })
|
|
216
|
+
response = await client.chat.completions.create(stripProviderExtras(this.modifyRequestBody(bodyNoTools, options)), { signal: options.signal })
|
|
217
|
+
}
|
|
218
|
+
// Retry 3: the provider-specific extras are the only other thing we added
|
|
219
|
+
// to an otherwise standard body (NIM's chat_template_kwargs — see
|
|
220
|
+
// nvidia.ts). Losing the reasoning display beats failing the turn.
|
|
221
|
+
else if (EXTRAS_REJECTED_CODES.includes(status) && this.modifyRequestBody(body, options).chat_template_kwargs) {
|
|
222
|
+
log.warn(`[llm-client] ${this.providerName}: request rejected (HTTP ${status}) — retrying without chat_template_kwargs`)
|
|
223
|
+
response = await client.chat.completions.create(stripProviderExtras(this.modifyRequestBody(body, options)), { signal: options.signal })
|
|
187
224
|
}
|
|
188
225
|
else {
|
|
189
226
|
throw err
|
|
@@ -254,7 +291,10 @@ export abstract class OpenAICompatBase implements LLMProvider {
|
|
|
254
291
|
delete bodyNoTools.tools
|
|
255
292
|
delete bodyNoTools.tool_choice
|
|
256
293
|
delete bodyNoTools.parallel_tool_calls
|
|
257
|
-
stream = await client.chat.completions.create({ ...this.modifyRequestBody(bodyNoTools, options), stream: true }, { signal: options.signal })
|
|
294
|
+
stream = await client.chat.completions.create({ ...stripProviderExtras(this.modifyRequestBody(bodyNoTools, options)), stream: true }, { signal: options.signal })
|
|
295
|
+
} else if (EXTRAS_REJECTED_CODES.includes(status) && this.modifyRequestBody(body, options).chat_template_kwargs) {
|
|
296
|
+
log.warn(`[llm-client] ${this.providerName}: request rejected (HTTP ${status}) — retrying stream without chat_template_kwargs`)
|
|
297
|
+
stream = await client.chat.completions.create({ ...stripProviderExtras(this.modifyRequestBody(body, options)), stream: true }, { signal: options.signal })
|
|
258
298
|
} else {
|
|
259
299
|
throw err
|
|
260
300
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-result-normalizer — keeps large/binary MCP tool results out of the LLM
|
|
3
|
+
* context window.
|
|
4
|
+
*
|
|
5
|
+
* `MCPClientManager.callTool()` (packages/mcp/src/manager.ts) returns the raw
|
|
6
|
+
* `content` array from the MCP SDK's CallToolResult. Per the MCP spec that
|
|
7
|
+
* array can contain `image`/`audio` blocks (base64 `data`) or `resource`
|
|
8
|
+
* blocks (base64 `blob`) alongside plain `text` blocks. Nothing downstream
|
|
9
|
+
* (agent-loop.ts's formatToolResult) inspects those blocks — a base64 image
|
|
10
|
+
* would be serialized whole into a `role:"tool"` message and sent to the
|
|
11
|
+
* model, which is exactly what filled the context window and hung the agent
|
|
12
|
+
* in the incident this module fixes.
|
|
13
|
+
*
|
|
14
|
+
* This mirrors the pattern already used by browser-screenshot.ts: binary
|
|
15
|
+
* content gets persisted via createArtifact() (artifacts/store.ts) and the
|
|
16
|
+
* model only ever sees a lightweight { type: "artifact_ref", ... } reference.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createArtifact } from "../artifacts/store.ts";
|
|
20
|
+
import { logger } from "../utils/logger.ts";
|
|
21
|
+
|
|
22
|
+
const log = logger.child("mcp-result-normalizer");
|
|
23
|
+
|
|
24
|
+
/** Blocks longer than this (in chars) get materialized as a text artifact instead of inlined. Override via HIVE_MCP_INLINE_MAX_CHARS. */
|
|
25
|
+
const MCP_INLINE_MAX_CHARS = Number(process.env.HIVE_MCP_INLINE_MAX_CHARS) || 20_000;
|
|
26
|
+
|
|
27
|
+
export interface McpNormalizeContext {
|
|
28
|
+
userId?: string;
|
|
29
|
+
runId?: string | null;
|
|
30
|
+
taskId?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type McpContentBlock = Record<string, unknown> & { type?: unknown };
|
|
34
|
+
|
|
35
|
+
function isBinaryBlock(block: McpContentBlock): block is McpContentBlock & { type: "image" | "audio"; data: string; mimeType: string } {
|
|
36
|
+
return (block.type === "image" || block.type === "audio") && typeof block.data === "string";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isBlobResourceBlock(block: McpContentBlock): block is McpContentBlock & { type: "resource"; resource: { blob: string; mimeType?: string; uri?: string } } {
|
|
40
|
+
if (block.type !== "resource") return false;
|
|
41
|
+
const resource = block.resource as Record<string, unknown> | undefined;
|
|
42
|
+
return !!resource && typeof resource.blob === "string";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isOversizedTextBlock(block: McpContentBlock): block is McpContentBlock & { type: "text"; text: string } {
|
|
46
|
+
return block.type === "text" && typeof block.text === "string" && block.text.length > MCP_INLINE_MAX_CHARS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function materializeBinary(
|
|
50
|
+
bytesBase64: string,
|
|
51
|
+
mimeType: string,
|
|
52
|
+
kind: string,
|
|
53
|
+
ctx: McpNormalizeContext,
|
|
54
|
+
): Promise<Record<string, unknown>> {
|
|
55
|
+
if (!ctx.userId) {
|
|
56
|
+
// Without a user_id we can't set ownership on the artifact (inspectArtifact
|
|
57
|
+
// would reject any read against it later) — never let raw base64 through,
|
|
58
|
+
// just describe what was omitted.
|
|
59
|
+
const approxBytes = Math.floor((bytesBase64.length * 3) / 4);
|
|
60
|
+
log.warn(`[materializeBinary] No user_id in tool context — omitting ${mimeType} block (${approxBytes} bytes) instead of creating an artifact`);
|
|
61
|
+
return { type: "artifact_omitted", mime_type: mimeType, approx_size: approxBytes, reason: "no user_id in tool context" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const bytes = Buffer.from(bytesBase64, "base64");
|
|
65
|
+
const artifact = await createArtifact({
|
|
66
|
+
bytes,
|
|
67
|
+
mimeType,
|
|
68
|
+
kind,
|
|
69
|
+
userId: ctx.userId,
|
|
70
|
+
runId: ctx.runId ?? null,
|
|
71
|
+
taskId: ctx.taskId ?? null,
|
|
72
|
+
});
|
|
73
|
+
log.info(`[materializeBinary] Stored ${kind} as artifact ${artifact.id} (${artifact.size} bytes, ${artifact.mime_type})`);
|
|
74
|
+
return {
|
|
75
|
+
type: "artifact_ref",
|
|
76
|
+
artifact_id: artifact.id,
|
|
77
|
+
mime_type: artifact.mime_type,
|
|
78
|
+
size: artifact.size,
|
|
79
|
+
sha256: artifact.sha256,
|
|
80
|
+
expires_at: artifact.expires_at,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Above this, parsing the text just to describe it costs more than it explains. */
|
|
85
|
+
const MAX_SHAPE_PROBE_CHARS = 5_000_000;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Describes the shape of a JSON payload so the model can aim `artifact_read`
|
|
89
|
+
* instead of paging blind.
|
|
90
|
+
*
|
|
91
|
+
* The 500-char preview alone is close to useless on the payload this was
|
|
92
|
+
* written for — a Gmail MCP result where the first message's `received`
|
|
93
|
+
* headers eat the whole window before a single subject line appears. Knowing
|
|
94
|
+
* "12 items, keyed id/threadId/labelIds/headers" is what turns a search into
|
|
95
|
+
* one call.
|
|
96
|
+
*/
|
|
97
|
+
function describeJsonShape(text: string): Record<string, unknown> {
|
|
98
|
+
if (text.length > MAX_SHAPE_PROBE_CHARS) return {};
|
|
99
|
+
const trimmed = text.trimStart();
|
|
100
|
+
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return {};
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(text);
|
|
104
|
+
if (Array.isArray(parsed)) {
|
|
105
|
+
const first = parsed.find((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
106
|
+
return {
|
|
107
|
+
json_items: parsed.length,
|
|
108
|
+
...(first ? { json_item_keys: Object.keys(first as Record<string, unknown>).slice(0, 25) } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (parsed && typeof parsed === "object") {
|
|
112
|
+
return { json_keys: Object.keys(parsed as Record<string, unknown>).slice(0, 25) };
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Not JSON, or truncated JSON — the plain preview still stands.
|
|
116
|
+
}
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function materializeText(text: string, ctx: McpNormalizeContext): Promise<Record<string, unknown>> {
|
|
121
|
+
if (!ctx.userId) {
|
|
122
|
+
log.warn(`[materializeText] No user_id in tool context — truncating oversized text block (${text.length} chars) instead of creating an artifact`);
|
|
123
|
+
return { type: "text", text: `${text.slice(0, 500)}… [truncated: ${text.length} chars total, no user_id to persist full text as artifact]` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const artifact = await createArtifact({
|
|
127
|
+
bytes: Buffer.from(text, "utf-8"),
|
|
128
|
+
mimeType: "text/plain",
|
|
129
|
+
kind: "mcp_text_result",
|
|
130
|
+
userId: ctx.userId,
|
|
131
|
+
runId: ctx.runId ?? null,
|
|
132
|
+
taskId: ctx.taskId ?? null,
|
|
133
|
+
});
|
|
134
|
+
log.info(`[materializeText] Stored oversized text result as artifact ${artifact.id} (${artifact.size} bytes)`);
|
|
135
|
+
return {
|
|
136
|
+
type: "artifact_ref",
|
|
137
|
+
artifact_id: artifact.id,
|
|
138
|
+
mime_type: "text/plain",
|
|
139
|
+
size: artifact.size,
|
|
140
|
+
chars: text.length,
|
|
141
|
+
preview: text.length > 500 ? `${text.slice(0, 500)}…` : text,
|
|
142
|
+
...describeJsonShape(text),
|
|
143
|
+
// Without this the model only knows the data exists somewhere. It used to
|
|
144
|
+
// reach for artifact_inspect (metadata only), find nothing usable, and
|
|
145
|
+
// spend its remaining iterations guessing.
|
|
146
|
+
hint: "Full content is available via artifact_read (artifactId + offset/limit, or search).",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Normalizes a raw MCP CallToolResult.content array: binary blocks (image,
|
|
152
|
+
* audio, resource-with-blob) become { type: "artifact_ref", ... }, oversized
|
|
153
|
+
* text blocks are persisted the same way with a short preview kept inline,
|
|
154
|
+
* everything else (text, resource_link) passes through unchanged.
|
|
155
|
+
*
|
|
156
|
+
* Never pre-stringifies — returns JS values so formatToolResult (toon.ts)
|
|
157
|
+
* keeps doing the actual TOON/JSON encoding, same contract context-compiler.ts
|
|
158
|
+
* already documents for MCP tool executors.
|
|
159
|
+
*/
|
|
160
|
+
export async function normalizeMcpResult(content: unknown, ctx: McpNormalizeContext): Promise<unknown> {
|
|
161
|
+
if (!Array.isArray(content)) return content;
|
|
162
|
+
|
|
163
|
+
const out: unknown[] = [];
|
|
164
|
+
for (const raw of content) {
|
|
165
|
+
if (!raw || typeof raw !== "object") {
|
|
166
|
+
out.push(raw);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const block = raw as McpContentBlock;
|
|
170
|
+
try {
|
|
171
|
+
if (isBinaryBlock(block)) {
|
|
172
|
+
out.push(await materializeBinary(block.data, block.mimeType, "mcp_result", ctx));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (isBlobResourceBlock(block)) {
|
|
176
|
+
const mimeType = block.resource.mimeType || "application/octet-stream";
|
|
177
|
+
out.push(await materializeBinary(block.resource.blob, mimeType, "mcp_result", ctx));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (isOversizedTextBlock(block)) {
|
|
181
|
+
out.push(await materializeText(block.text, ctx));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
log.error(`[normalizeMcpResult] Failed to materialize block (type=${String(block.type)}): ${(err as Error).message}`);
|
|
186
|
+
out.push({ type: "artifact_error", mime_type: (block as { mimeType?: string }).mimeType, error: (err as Error).message });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
out.push(block);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|