@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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks — engancharse al ciclo de vida sin bifurcar el SDK.
|
|
3
|
+
*
|
|
4
|
+
* `HooksConfigSchema` (`config/loader.ts`) declaraba 14 hooks y **ninguno se
|
|
5
|
+
* invocaba**: era un esquema sin implementación. Quien lo encontrara asumiría
|
|
6
|
+
* que funciona.
|
|
7
|
+
*
|
|
8
|
+
* Acá se implementan, con dos formas y una decisión de diseño detrás:
|
|
9
|
+
*
|
|
10
|
+
* - **Callbacks en proceso** (`registerHook`) — tipados, sin costo de arranque,
|
|
11
|
+
* y pueden **devolver una decisión**: `beforeToolCall` puede impedir que una
|
|
12
|
+
* tool se ejecute. Es el primitivo, porque quien consume el SDK ya está en el
|
|
13
|
+
* mismo proceso.
|
|
14
|
+
* - **Scripts externos** (`hooks.scripts` en la configuración) — para quien no
|
|
15
|
+
* escribe TypeScript. Se montan encima del primitivo: cada script declarado
|
|
16
|
+
* se registra como un callback que lo ejecuta. Cuestan un `Bun.spawn` por
|
|
17
|
+
* invocación, así que conviene reservarlos para lo que no ocurre en cada
|
|
18
|
+
* tool call.
|
|
19
|
+
*
|
|
20
|
+
* Sólo están los hooks que tienen un uso claro. Los otros nueve del esquema
|
|
21
|
+
* original se dejaron fuera a propósito: cada hook es una promesa que después
|
|
22
|
+
* hay que sostener, y uno que nadie usa es superficie que envejece mal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { logger } from "../utils/logger.ts";
|
|
26
|
+
import { loadConfig } from "../config/loader.ts";
|
|
27
|
+
|
|
28
|
+
const log = logger.child("hooks");
|
|
29
|
+
|
|
30
|
+
/** Lo que recibe un hook de tool. */
|
|
31
|
+
export interface ToolCallContext {
|
|
32
|
+
toolName: string;
|
|
33
|
+
args: Record<string, unknown>;
|
|
34
|
+
agentId?: string;
|
|
35
|
+
userId?: string;
|
|
36
|
+
threadId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ToolResultContext extends ToolCallContext {
|
|
40
|
+
result: unknown;
|
|
41
|
+
ok: boolean;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CompactionContext {
|
|
46
|
+
threadId: string;
|
|
47
|
+
messageCount: number;
|
|
48
|
+
totalTokens: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SessionContext {
|
|
52
|
+
threadId: string;
|
|
53
|
+
userId?: string;
|
|
54
|
+
channel?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lo que puede devolver `beforeToolCall`.
|
|
59
|
+
*
|
|
60
|
+
* `void` o `undefined` = seguir adelante. Devolver `{ block }` impide la
|
|
61
|
+
* ejecución y el motivo le llega al modelo como resultado de la tool, para que
|
|
62
|
+
* sepa por qué no se hizo en vez de reintentar a ciegas.
|
|
63
|
+
*/
|
|
64
|
+
export type BeforeToolCallResult = void | undefined | { block: true; reason: string };
|
|
65
|
+
|
|
66
|
+
export interface HookMap {
|
|
67
|
+
beforeToolCall: (ctx: ToolCallContext) => BeforeToolCallResult | Promise<BeforeToolCallResult>;
|
|
68
|
+
afterToolCall: (ctx: ToolResultContext) => void | Promise<void>;
|
|
69
|
+
beforeCompaction: (ctx: CompactionContext) => void | Promise<void>;
|
|
70
|
+
sessionStart: (ctx: SessionContext) => void | Promise<void>;
|
|
71
|
+
sessionEnd: (ctx: SessionContext) => void | Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type HookName = keyof HookMap;
|
|
75
|
+
|
|
76
|
+
const registry: { [K in HookName]: Array<HookMap[K]> } = {
|
|
77
|
+
beforeToolCall: [],
|
|
78
|
+
afterToolCall: [],
|
|
79
|
+
beforeCompaction: [],
|
|
80
|
+
sessionStart: [],
|
|
81
|
+
sessionEnd: [],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Registra un hook. Devuelve la función para quitarlo.
|
|
86
|
+
*
|
|
87
|
+
* Se pueden registrar varios del mismo tipo: corren en orden de registro.
|
|
88
|
+
*/
|
|
89
|
+
export function registerHook<K extends HookName>(name: K, fn: HookMap[K]): () => void {
|
|
90
|
+
registry[name].push(fn);
|
|
91
|
+
return () => {
|
|
92
|
+
const i = registry[name].indexOf(fn);
|
|
93
|
+
if (i >= 0) registry[name].splice(i, 1);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Quita todos los hooks. Pensado para tests. */
|
|
98
|
+
export function clearHooks(name?: HookName): void {
|
|
99
|
+
if (name) registry[name] = [];
|
|
100
|
+
else for (const k of Object.keys(registry) as HookName[]) registry[k] = [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function hasHooks(name: HookName): boolean {
|
|
104
|
+
return registry[name].length > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Corre los `beforeToolCall` y devuelve el motivo del bloqueo, si alguno objeta.
|
|
109
|
+
*
|
|
110
|
+
* El primero que bloquea gana: no tiene sentido seguir preguntando cuando ya
|
|
111
|
+
* hay una negativa. Un hook que lanza **no** bloquea la ejecución —un error en
|
|
112
|
+
* el observador no debería impedir el trabajo— pero se registra.
|
|
113
|
+
*/
|
|
114
|
+
export async function runBeforeToolCall(ctx: ToolCallContext): Promise<string | null> {
|
|
115
|
+
for (const fn of registry.beforeToolCall) {
|
|
116
|
+
try {
|
|
117
|
+
const r = await fn(ctx);
|
|
118
|
+
if (r && typeof r === "object" && r.block) return r.reason;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.warn(`beforeToolCall falló para ${ctx.toolName}: ${(err as Error).message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Corre los hooks de observación. Nunca lanza: son observadores. */
|
|
127
|
+
async function runObservers<K extends "afterToolCall" | "beforeCompaction" | "sessionStart" | "sessionEnd">(
|
|
128
|
+
name: K,
|
|
129
|
+
ctx: Parameters<HookMap[K]>[0],
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
for (const fn of registry[name]) {
|
|
132
|
+
try {
|
|
133
|
+
await (fn as (c: unknown) => unknown)(ctx);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
log.warn(`${name} falló: ${(err as Error).message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const runAfterToolCall = (ctx: ToolResultContext) => runObservers("afterToolCall", ctx);
|
|
141
|
+
export const runBeforeCompaction = (ctx: CompactionContext) => runObservers("beforeCompaction", ctx);
|
|
142
|
+
export const runSessionStart = (ctx: SessionContext) => runObservers("sessionStart", ctx);
|
|
143
|
+
export const runSessionEnd = (ctx: SessionContext) => runObservers("sessionEnd", ctx);
|
|
144
|
+
|
|
145
|
+
// ─── Scripts declarados en la configuración ──────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/** Nombres del esquema de config → hooks implementados. */
|
|
148
|
+
const SCRIPT_MAP: Record<string, HookName> = {
|
|
149
|
+
before_tool_call: "beforeToolCall",
|
|
150
|
+
after_tool_call: "afterToolCall",
|
|
151
|
+
before_compaction: "beforeCompaction",
|
|
152
|
+
session_start: "sessionStart",
|
|
153
|
+
session_end: "sessionEnd",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Ejecuta un script pasándole el contexto como JSON por stdin.
|
|
158
|
+
*
|
|
159
|
+
* Convención para `before_tool_call`: **salir con código distinto de 0 bloquea**
|
|
160
|
+
* la ejecución, y lo que el script escriba en stdout es el motivo que se le
|
|
161
|
+
* cuenta al modelo. Es el equivalente en procesos a devolver `{ block }`.
|
|
162
|
+
*/
|
|
163
|
+
async function runScript(path: string, ctx: unknown): Promise<{ blocked: boolean; reason: string }> {
|
|
164
|
+
const proc = Bun.spawn(["bun", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
165
|
+
proc.stdin.write(JSON.stringify(ctx));
|
|
166
|
+
proc.stdin.end();
|
|
167
|
+
|
|
168
|
+
const [salida, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
169
|
+
return { blocked: code !== 0, reason: salida.trim() || `el hook ${path} salió con código ${code}` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let scriptsCargados = false;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Registra los scripts declarados en `hooks.scripts`.
|
|
176
|
+
*
|
|
177
|
+
* Es opt-in y explícito: ejecutar procesos externos no es algo que deba pasar
|
|
178
|
+
* por el solo hecho de importar el SDK. Idempotente.
|
|
179
|
+
*/
|
|
180
|
+
export function loadConfiguredHookScripts(): number {
|
|
181
|
+
if (scriptsCargados) return 0;
|
|
182
|
+
const scripts = loadConfig().hooks?.scripts;
|
|
183
|
+
if (!scripts) return 0;
|
|
184
|
+
|
|
185
|
+
let n = 0;
|
|
186
|
+
for (const [clave, hook] of Object.entries(SCRIPT_MAP)) {
|
|
187
|
+
const path = (scripts as Record<string, string | undefined>)[clave];
|
|
188
|
+
if (!path) continue;
|
|
189
|
+
|
|
190
|
+
if (hook === "beforeToolCall") {
|
|
191
|
+
registerHook("beforeToolCall", async (ctx) => {
|
|
192
|
+
const r = await runScript(path, ctx);
|
|
193
|
+
return r.blocked ? { block: true as const, reason: r.reason } : undefined;
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
registerHook(hook as "afterToolCall", async (ctx) => { await runScript(path, ctx); });
|
|
197
|
+
}
|
|
198
|
+
n++;
|
|
199
|
+
log.info(`hook ${clave} → ${path}`);
|
|
200
|
+
}
|
|
201
|
+
scriptsCargados = true;
|
|
202
|
+
return n;
|
|
203
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imágenes — redimensionar, convertir y medir, sin dependencias nativas.
|
|
3
|
+
*
|
|
4
|
+
* Usa `Bun.Image` (Bun 1.4), que es sharp integrado en el runtime: no hay que
|
|
5
|
+
* instalar nada ni compilar bindings. El SDK ya exige Bun ≥ 1.4, así que esto
|
|
6
|
+
* no agrega ningún requisito.
|
|
7
|
+
*
|
|
8
|
+
* Sirve para dos cosas distintas y conviene no confundirlas:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Tools activables** para el agente (`tools/images/`) — recortar, convertir
|
|
11
|
+
* de formato, leer dimensiones. Es una capacidad de producto.
|
|
12
|
+
* 2. **Normalizar lo que entra** — una foto de 4 MB que un usuario manda por
|
|
13
|
+
* WhatsApp no tiene por qué viajar entera al modelo. Redimensionarla antes
|
|
14
|
+
* cuesta una fracción de los tokens y no cambia lo que el modelo puede ver.
|
|
15
|
+
*
|
|
16
|
+
* Nota sobre `Bun.Image`: las transformaciones son **diferidas**. `metadata()`
|
|
17
|
+
* sobre una cadena sin materializar devuelve las dimensiones del origen, no las
|
|
18
|
+
* del resultado — hay que pedir los bytes y releerlos. `measureImage()` lo hace
|
|
19
|
+
* por vos cuando hace falta.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { logger } from "../utils/logger.ts";
|
|
23
|
+
|
|
24
|
+
const log = logger.child("images");
|
|
25
|
+
|
|
26
|
+
/** Los que `Bun.Image` sabe escribir. */
|
|
27
|
+
export type ImageFormat = "jpeg" | "png" | "webp" | "avif" | "heic";
|
|
28
|
+
|
|
29
|
+
export interface ImageMetadata {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
format: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TransformOptions {
|
|
36
|
+
/** Ancho máximo; la altura se ajusta manteniendo la proporción si no se da. */
|
|
37
|
+
width?: number;
|
|
38
|
+
height?: number;
|
|
39
|
+
format?: ImageFormat;
|
|
40
|
+
/** 1–100. Sólo lo respetan los formatos con pérdida. */
|
|
41
|
+
quality?: number;
|
|
42
|
+
/** Grados: 90, 180, 270. */
|
|
43
|
+
rotate?: number;
|
|
44
|
+
flip?: boolean;
|
|
45
|
+
flop?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bunImage(): any {
|
|
49
|
+
const I = (Bun as any).Image;
|
|
50
|
+
if (!I) {
|
|
51
|
+
throw new Error("Bun.Image no está disponible: se necesita Bun >= 1.4");
|
|
52
|
+
}
|
|
53
|
+
return I;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toBytes(input: Uint8Array | ArrayBuffer | Buffer | string): Uint8Array {
|
|
57
|
+
if (typeof input === "string") return Uint8Array.from(Buffer.from(input, "base64"));
|
|
58
|
+
if (input instanceof Uint8Array) return input;
|
|
59
|
+
return new Uint8Array(input as ArrayBuffer);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Dimensiones y formato sin decodificar la imagen entera. */
|
|
63
|
+
export async function measureImage(input: Uint8Array | ArrayBuffer | Buffer | string): Promise<ImageMetadata> {
|
|
64
|
+
const meta = await new (bunImage())(toBytes(input)).metadata();
|
|
65
|
+
return { width: meta.width, height: meta.height, format: meta.format };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Aplica las transformaciones y devuelve los bytes resultantes.
|
|
70
|
+
*
|
|
71
|
+
* Sin `format` conserva el de origen. Sin nada que hacer devuelve la entrada tal
|
|
72
|
+
* cual, para no recomprimir de gusto.
|
|
73
|
+
*/
|
|
74
|
+
export async function transformImage(
|
|
75
|
+
input: Uint8Array | ArrayBuffer | Buffer | string,
|
|
76
|
+
opts: TransformOptions,
|
|
77
|
+
): Promise<{ bytes: Uint8Array; metadata: ImageMetadata }> {
|
|
78
|
+
const bytes = toBytes(input);
|
|
79
|
+
const hayQueHacer = opts.width || opts.height || opts.format || opts.rotate || opts.flip || opts.flop;
|
|
80
|
+
if (!hayQueHacer) return { bytes, metadata: await measureImage(bytes) };
|
|
81
|
+
|
|
82
|
+
let img = new (bunImage())(bytes);
|
|
83
|
+
|
|
84
|
+
if (opts.width || opts.height) img = img.resize(opts.width, opts.height);
|
|
85
|
+
if (opts.rotate) img = img.rotate(opts.rotate);
|
|
86
|
+
if (opts.flip) img = img.flip();
|
|
87
|
+
if (opts.flop) img = img.flop();
|
|
88
|
+
|
|
89
|
+
const formato = opts.format ?? ((await measureImage(bytes)).format as ImageFormat);
|
|
90
|
+
const args = opts.quality !== undefined ? [{ quality: opts.quality }] : [];
|
|
91
|
+
img = typeof img[formato] === "function" ? img[formato](...args) : img;
|
|
92
|
+
|
|
93
|
+
const salida: Uint8Array = await img.bytes();
|
|
94
|
+
// Se releen los bytes porque `metadata()` sobre la cadena diferida informa el
|
|
95
|
+
// origen, no el resultado.
|
|
96
|
+
return { bytes: salida, metadata: await measureImage(salida) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface NormalizeOptions {
|
|
100
|
+
/** Lado mayor permitido. Por encima se reduce manteniendo la proporción. */
|
|
101
|
+
maxDimension?: number;
|
|
102
|
+
/** Formato de salida; `webp` pesa mucho menos que un JPEG equivalente. */
|
|
103
|
+
format?: ImageFormat;
|
|
104
|
+
quality?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Por defecto: 1024 px de lado mayor y webp al 80 — el punto donde un modelo de visión deja de ganar detalle. */
|
|
108
|
+
const NORMALIZE_DEFAULTS: Required<NormalizeOptions> = {
|
|
109
|
+
maxDimension: 1024,
|
|
110
|
+
format: "webp",
|
|
111
|
+
quality: 80,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export interface NormalizeResult {
|
|
115
|
+
bytes: Uint8Array;
|
|
116
|
+
metadata: ImageMetadata;
|
|
117
|
+
/** Bytes originales, para saber cuánto se ahorró. */
|
|
118
|
+
originalBytes: number;
|
|
119
|
+
/** true si hubo que tocarla; false si ya era chica. */
|
|
120
|
+
changed: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Deja una imagen entrante en un tamaño razonable para mandársela a un modelo.
|
|
125
|
+
*
|
|
126
|
+
* Una foto de teléfono son varios megabytes y unos cuantos miles de tokens; a
|
|
127
|
+
* 1024 px el modelo ve lo mismo por una fracción del costo. Si ya está por
|
|
128
|
+
* debajo del límite no se toca — recomprimir una imagen chica sólo la empeora.
|
|
129
|
+
*/
|
|
130
|
+
export async function normalizeForModel(
|
|
131
|
+
input: Uint8Array | ArrayBuffer | Buffer | string,
|
|
132
|
+
opts: NormalizeOptions = {},
|
|
133
|
+
): Promise<NormalizeResult> {
|
|
134
|
+
const cfg = { ...NORMALIZE_DEFAULTS, ...opts };
|
|
135
|
+
const bytes = toBytes(input);
|
|
136
|
+
const meta = await measureImage(bytes);
|
|
137
|
+
const mayor = Math.max(meta.width, meta.height);
|
|
138
|
+
|
|
139
|
+
if (mayor <= cfg.maxDimension && meta.format === cfg.format) {
|
|
140
|
+
return { bytes, metadata: meta, originalBytes: bytes.length, changed: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const escala = mayor > cfg.maxDimension ? cfg.maxDimension / mayor : 1;
|
|
144
|
+
const { bytes: salida, metadata } = await transformImage(bytes, {
|
|
145
|
+
width: Math.round(meta.width * escala),
|
|
146
|
+
height: Math.round(meta.height * escala),
|
|
147
|
+
format: cfg.format,
|
|
148
|
+
quality: cfg.quality,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
log.info(
|
|
152
|
+
`imagen normalizada: ${meta.width}x${meta.height} ${meta.format} (${bytes.length}b) → ` +
|
|
153
|
+
`${metadata.width}x${metadata.height} ${metadata.format} (${salida.length}b)`,
|
|
154
|
+
);
|
|
155
|
+
return { bytes: salida, metadata, originalBytes: bytes.length, changed: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** true si este runtime puede procesar imágenes. */
|
|
159
|
+
export function imagesSupported(): boolean {
|
|
160
|
+
return typeof (Bun as any).Image === "function";
|
|
161
|
+
}
|
|
@@ -22,6 +22,7 @@ export type { Skill, SkillStep, OutputFormat, SkillsConfig } from "./skills/inde
|
|
|
22
22
|
export { runAgent, runAgentIsolated, AgentLoop, getAgentLoop, buildAgentLoop, rebuildAgentLoop } from "./agent/agent-loop.ts";
|
|
23
23
|
export type { AgentLoopOptions, StepEvent, StreamChunk } from "./agent/agent-loop.ts";
|
|
24
24
|
export type { Provider } from "./agent/providers/index.ts";
|
|
25
|
+
export { AgentRunner, createAgentRunner } from "./agent/providers/index.ts";
|
|
25
26
|
|
|
26
27
|
// El cliente LLM es parte de la superficie pública: hasta 0.1.5 sólo se exportaba
|
|
27
28
|
// el wrapper `AgentRunner`, así que no había forma de llamar a un provider ni de
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
-
import type { MCPConfig, MCPServerConfig } from "./config";
|
|
4
|
-
import { logger, type LogHandler } from "./logger";
|
|
3
|
+
import type { MCPConfig, MCPServerConfig } from "./config.ts";
|
|
4
|
+
import { logger, type LogHandler } from "./logger.ts";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import {
|
|
7
7
|
createTransport,
|
|
8
8
|
type TransportType,
|
|
9
|
-
} from "./transports/index";
|
|
9
|
+
} from "./transports/index.ts";
|
|
10
10
|
|
|
11
11
|
export interface MCPTool {
|
|
12
12
|
name: string;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* - MCP tools are loaded at runtime from connected servers (not stored in DB)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { col, updateDoc } from "../storage/hive";
|
|
12
|
-
import type { McpServerDoc } from "../storage/collections";
|
|
13
|
-
import { logger } from "../utils/logger";
|
|
14
|
-
import { loadMcpHeaders } from "../storage/crypto";
|
|
15
|
-
import { syncMCPToolsToDB, syncMCPToolsToIndex, clearMCPToolsFromDB } from "./tool-sync";
|
|
11
|
+
import { col, updateDoc } from "../storage/hive.ts";
|
|
12
|
+
import type { McpServerDoc } from "../storage/collections.ts";
|
|
13
|
+
import { logger } from "../utils/logger.ts";
|
|
14
|
+
import { loadMcpHeaders } from "../storage/crypto.ts";
|
|
15
|
+
import { syncMCPToolsToDB, syncMCPToolsToIndex, clearMCPToolsFromDB } from "./tool-sync.ts";
|
|
16
16
|
import type { MCPClientManager } from "./MCPClient.ts";
|
|
17
17
|
|
|
18
18
|
const log = logger.child("mcp:hot-reload");
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
* 3. Offline visibility of what MCP tools were available
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { col } from "../storage/hive"
|
|
17
|
-
import type { McpToolDoc } from "../storage/collections"
|
|
18
|
-
import { logger } from "../utils/logger"
|
|
19
|
-
import { mcpToolFullName } from "../agent/tool-selector"
|
|
16
|
+
import { col } from "../storage/hive.ts"
|
|
17
|
+
import type { McpToolDoc } from "../storage/collections.ts"
|
|
18
|
+
import { logger } from "../utils/logger.ts"
|
|
19
|
+
import { mcpToolFullName } from "../agent/tool-selector.ts"
|
|
20
20
|
import {
|
|
21
21
|
replaceCapabilityDocs,
|
|
22
22
|
deleteCapabilitiesByServer,
|
|
23
23
|
type CapabilityDoc,
|
|
24
|
-
} from "../agent/capability-search"
|
|
24
|
+
} from "../agent/capability-search.ts"
|
|
25
25
|
|
|
26
26
|
const log = logger.child("mcp:tool-sync")
|
|
27
27
|
|
|
@@ -4,8 +4,8 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
4
4
|
// CORRECCIÓN 1 — quitar extensión .ts de los imports
|
|
5
5
|
// Bun resuelve los módulos sin extensión correctamente
|
|
6
6
|
// Con .ts puede fallar en algunos contextos de build/bundle
|
|
7
|
-
import { SSETransport, type SSETransportConfig } from "./sse";
|
|
8
|
-
import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket";
|
|
7
|
+
import { SSETransport, type SSETransportConfig } from "./sse.ts";
|
|
8
|
+
import { WebSocketTransport, type WebSocketTransportConfig } from "./websocket.ts";
|
|
9
9
|
|
|
10
10
|
export { SSETransport, type SSETransportConfig };
|
|
11
11
|
export { WebSocketTransport, type WebSocketTransportConfig };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Models — el catálogo de proveedores y modelos con el que arranca una colmena.
|
|
3
|
+
*
|
|
4
|
+
* El "seed de modelos" ya existía, pero repartido en `storage/`: los datos en
|
|
5
|
+
* `seed.ts`, las claves en `model-id.ts`, el precio en `usage.ts`. Quien quería
|
|
6
|
+
* consumirlo tenía que saber que un catálogo de modelos vive bajo "storage".
|
|
7
|
+
* Esto le da nombre y un punto de entrada; no mueve la implementación ni
|
|
8
|
+
* duplica nada.
|
|
9
|
+
*
|
|
10
|
+
* `SEED_DATA` es la fuente de verdad del catálogo (18 proveedores, ~110 modelos)
|
|
11
|
+
* y `seedAllData()` la aplica de forma idempotente en cada arranque — la llama
|
|
12
|
+
* `ensureHiveDb()`, así que normalmente no hace falta invocarla a mano.
|
|
13
|
+
*
|
|
14
|
+
* Nota de precios: cada modelo LLM declara `inputPer1M`/`outputPer1M`. Un modelo
|
|
15
|
+
* sin ellos no es "gratis", es un modelo cuyo costo no se puede calcular.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Catálogo ────────────────────────────────────────────────────────────────
|
|
19
|
+
export { SEED_DATA, seedAllData, seedToolsAndSkills } from "../storage/seed.ts";
|
|
20
|
+
export type { SeedData } from "../storage/seed.ts";
|
|
21
|
+
export { activateElement, deactivateElement, getAllElements, getActiveElements } from "../storage/seed.ts";
|
|
22
|
+
|
|
23
|
+
// ─── Identidad de modelo ─────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Los revendedores (openrouter, nvidia, groq, opencode-go, modelscope) sirven
|
|
26
|
+
* modelos de terceros bajo su propio endpoint, así que el id del wire colisiona
|
|
27
|
+
* entre proveedores. `catalogModelKey` prefija; `wireModelId` deshace el prefijo
|
|
28
|
+
* antes de salir a la API.
|
|
29
|
+
*/
|
|
30
|
+
export { catalogModelKey, wireModelId, isResellerProvider } from "../storage/model-id.ts";
|
|
31
|
+
|
|
32
|
+
// ─── Costos ──────────────────────────────────────────────────────────────────
|
|
33
|
+
export { calculateCost, invalidateModelPricingCache } from "../storage/usage.ts";
|
|
34
|
+
|
|
35
|
+
// ─── Shapes ──────────────────────────────────────────────────────────────────
|
|
36
|
+
export type { ModelDoc, ProviderDoc } from "../storage/collections.ts";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { ContentPart, ImageInput, DocumentInput, VisionConfig, MultimodalMessageType } from "./types"
|
|
2
|
-
export { multimodalService } from "./vision-service"
|
|
1
|
+
export type { ContentPart, ImageInput, DocumentInput, VisionConfig, MultimodalMessageType } from "./types.ts"
|
|
2
|
+
export { multimodalService } from "./vision-service.ts"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { col } from "../storage/hive"
|
|
2
|
-
import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections"
|
|
3
|
-
import { loadProviderApiKey } from "../storage/crypto"
|
|
4
|
-
import { logger } from "../utils/logger"
|
|
5
|
-
import type { ImageInput, DocumentInput, VisionConfig } from "./types"
|
|
6
|
-
import type { ContentPart } from "./types"
|
|
1
|
+
import { col } from "../storage/hive.ts"
|
|
2
|
+
import type { ChannelDoc, ModelDoc, ProviderDoc } from "../storage/collections.ts"
|
|
3
|
+
import { loadProviderApiKey } from "../storage/crypto.ts"
|
|
4
|
+
import { logger } from "../utils/logger.ts"
|
|
5
|
+
import type { ImageInput, DocumentInput, VisionConfig } from "./types.ts"
|
|
6
|
+
import type { ContentPart } from "./types.ts"
|
|
7
7
|
|
|
8
8
|
const log = logger.child("multimodal")
|
|
9
9
|
|
|
@@ -35,6 +35,19 @@ class MultimodalService {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Convierte una imagen entrante en las partes que ve el modelo.
|
|
40
|
+
*
|
|
41
|
+
* Antes de mandarla la **normaliza**: una foto de teléfono son varios
|
|
42
|
+
* megabytes y unos cuantos miles de tokens, y a 1024 px el modelo ve lo mismo
|
|
43
|
+
* por una fracción del costo. Esa imagen no viaja una sola vez — queda en el
|
|
44
|
+
* historial y se reenvía en cada turno siguiente, así que el ahorro se
|
|
45
|
+
* multiplica por la longitud de la conversación.
|
|
46
|
+
*
|
|
47
|
+
* Es best-effort: si el runtime no puede procesarla (Bun < 1.4) o la imagen
|
|
48
|
+
* está corrupta, se manda tal cual. Perder la imagen sería peor que mandarla
|
|
49
|
+
* grande.
|
|
50
|
+
*/
|
|
38
51
|
async processImage(image: ImageInput, visionModelId?: string): Promise<ContentPart[]> {
|
|
39
52
|
const parts: ContentPart[] = []
|
|
40
53
|
|
|
@@ -43,25 +56,44 @@ class MultimodalService {
|
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
if (image.type === "url") {
|
|
59
|
+
// Una URL no ocupa contexto: la descarga la hace el proveedor.
|
|
46
60
|
parts.push({ type: "image_url", image_url: { url: image.data as string } })
|
|
47
|
-
|
|
48
|
-
parts.push({
|
|
49
|
-
type: "image_base64",
|
|
50
|
-
base64: image.data as string,
|
|
51
|
-
mimeType: image.mimeType || "image/jpeg",
|
|
52
|
-
})
|
|
53
|
-
} else if (image.type === "buffer") {
|
|
54
|
-
const base64 = Buffer.from(image.data as Buffer).toString("base64")
|
|
55
|
-
parts.push({
|
|
56
|
-
type: "image_base64",
|
|
57
|
-
base64,
|
|
58
|
-
mimeType: image.mimeType || "image/jpeg",
|
|
59
|
-
})
|
|
61
|
+
return parts
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
const crudo = image.type === "base64"
|
|
65
|
+
? (image.data as string)
|
|
66
|
+
: Buffer.from(image.data as Buffer).toString("base64")
|
|
67
|
+
|
|
68
|
+
const { base64, mimeType } = await this.normalizeIncoming(crudo, image.mimeType)
|
|
69
|
+
parts.push({ type: "image_base64", base64, mimeType })
|
|
70
|
+
|
|
62
71
|
return parts
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/** Achica la imagen para el modelo; ante cualquier problema devuelve la original. */
|
|
75
|
+
private async normalizeIncoming(
|
|
76
|
+
base64: string,
|
|
77
|
+
mimeType?: string,
|
|
78
|
+
): Promise<{ base64: string; mimeType: string }> {
|
|
79
|
+
const original = { base64, mimeType: mimeType || "image/jpeg" }
|
|
80
|
+
try {
|
|
81
|
+
const { imagesSupported, normalizeForModel } = await import("../images/index.ts")
|
|
82
|
+
if (!imagesSupported()) return original
|
|
83
|
+
|
|
84
|
+
const r = await normalizeForModel(base64)
|
|
85
|
+
if (!r.changed) return original
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
base64: Buffer.from(r.bytes).toString("base64"),
|
|
89
|
+
mimeType: `image/${r.metadata.format}`,
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.warn(`no pude normalizar la imagen entrante, va sin achicar: ${(err as Error).message}`)
|
|
93
|
+
return original
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
65
97
|
async ocrImage(image: ImageInput, providerId?: string): Promise<string> {
|
|
66
98
|
const resolved = providerId || "openai"
|
|
67
99
|
|
|
@@ -30,7 +30,10 @@ export class PluginLoader {
|
|
|
30
30
|
private commands: Map<string, CLICommand> = new Map();
|
|
31
31
|
private log = logger.child("plugins");
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
private options: PluginLoaderOptions;
|
|
34
|
+
|
|
35
|
+
constructor(options: PluginLoaderOptions) {
|
|
36
|
+
this.options = options;
|
|
34
37
|
if (!existsSync(options.pluginDir)) {
|
|
35
38
|
mkdirSync(options.pluginDir, { recursive: true });
|
|
36
39
|
}
|
|
@@ -39,10 +39,16 @@ export class CircuitBreaker {
|
|
|
39
39
|
private totalFailures = 0;
|
|
40
40
|
private totalSuccesses = 0;
|
|
41
41
|
|
|
42
|
+
private readonly name: string;
|
|
43
|
+
private readonly options: CircuitBreakerOptions;
|
|
44
|
+
|
|
42
45
|
constructor(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
) {
|
|
46
|
+
name: string,
|
|
47
|
+
options: CircuitBreakerOptions = defaultOptions
|
|
48
|
+
) {
|
|
49
|
+
this.name = name;
|
|
50
|
+
this.options = options;
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
48
54
|
if (this.state === "open") {
|
|
@@ -175,12 +181,17 @@ export class CircuitBreaker {
|
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
export class CircuitBreakerOpenError extends Error {
|
|
184
|
+
public readonly circuitName: string;
|
|
185
|
+
public readonly retryAfterMs: number;
|
|
186
|
+
|
|
178
187
|
constructor(
|
|
179
188
|
message: string,
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
circuitName: string,
|
|
190
|
+
retryAfterMs: number
|
|
182
191
|
) {
|
|
183
192
|
super(message);
|
|
193
|
+
this.circuitName = circuitName;
|
|
194
|
+
this.retryAfterMs = retryAfterMs;
|
|
184
195
|
this.name = "CircuitBreakerOpenError";
|
|
185
196
|
}
|
|
186
197
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilience — reintentos y circuit breakers.
|
|
3
|
+
*
|
|
4
|
+
* `withRetry` aplica backoff exponencial con jitter y respeta `Retry-After`
|
|
5
|
+
* cuando el proveedor lo manda; `isRetryableError` decide qué merece otro
|
|
6
|
+
* intento (429/5xx/timeout/red) y qué no (un 400 no mejora reintentando).
|
|
7
|
+
*
|
|
8
|
+
* El `CircuitBreaker` corta las llamadas a un servicio que ya viene fallando,
|
|
9
|
+
* en vez de seguir gastando intentos contra algo caído.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./retry.ts";
|
|
13
|
+
export * from "./circuit-breaker.ts";
|