@johpaz/hive-sdk 0.0.12 → 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,122 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../tools/ToolRegistry.ts";
|
|
2
|
+
import type { SkillDefinition } from "../skills/defineSkill.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
const log = logger.child("api");
|
|
6
|
+
|
|
7
|
+
export interface AgentConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
provider?: "openai" | "anthropic" | "gemini" | "ollama";
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
tools?: ToolDefinition[];
|
|
13
|
+
skills?: SkillDefinition[];
|
|
14
|
+
mcpServers?: Record<string, { command?: string; url?: string; args?: string[]; env?: Record<string, string> }>;
|
|
15
|
+
maxIterations?: number;
|
|
16
|
+
workspace?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Agent {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly config: AgentConfig;
|
|
22
|
+
chat(message: string, opts?: { threadId?: string; channel?: string }): AsyncGenerator<AgentEvent>;
|
|
23
|
+
run(task: string, opts?: { threadId?: string; channel?: string }): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type AgentEvent =
|
|
27
|
+
| { type: "text"; content: string }
|
|
28
|
+
| { type: "tool_call"; name: string; args: Record<string, unknown> }
|
|
29
|
+
| { type: "tool_result"; name: string; result: unknown }
|
|
30
|
+
| { type: "done"; response: string };
|
|
31
|
+
|
|
32
|
+
export async function createAgent(config: AgentConfig): Promise<Agent> {
|
|
33
|
+
const { initializeDatabase } = await import("../storage/SQLiteStorage.ts");
|
|
34
|
+
const { createAllTools } = await import("../tools/index.ts");
|
|
35
|
+
const { loadConfig } = await import("../config/loader.ts");
|
|
36
|
+
|
|
37
|
+
await initializeDatabase();
|
|
38
|
+
|
|
39
|
+
const coreConfig = await loadConfig();
|
|
40
|
+
const allBuiltInTools = createAllTools(coreConfig);
|
|
41
|
+
|
|
42
|
+
const customTools = (config.tools ?? []).map(t => ({
|
|
43
|
+
name: t.name,
|
|
44
|
+
description: t.description,
|
|
45
|
+
execute: t.execute,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const mergedTools = [...allBuiltInTools, ...customTools];
|
|
49
|
+
|
|
50
|
+
let mcpManager = null;
|
|
51
|
+
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
52
|
+
const { MCPClientManager } = await import("../mcp/index.ts");
|
|
53
|
+
const mcpConfig = {
|
|
54
|
+
servers: Object.fromEntries(
|
|
55
|
+
Object.entries(config.mcpServers).map(([name, serverConfig]) => [
|
|
56
|
+
name,
|
|
57
|
+
{
|
|
58
|
+
transport: (serverConfig.command ? "stdio" : "sse") as "stdio" | "sse",
|
|
59
|
+
command: serverConfig.command,
|
|
60
|
+
url: serverConfig.url,
|
|
61
|
+
args: serverConfig.args ?? [],
|
|
62
|
+
env: serverConfig.env ?? {},
|
|
63
|
+
enabled: true,
|
|
64
|
+
},
|
|
65
|
+
])
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
mcpManager = new MCPClientManager(mcpConfig);
|
|
69
|
+
await mcpManager.initialize();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const loop = await (async () => {
|
|
73
|
+
const { buildAgentLoop } = await import("../agent/AgentRunner.ts");
|
|
74
|
+
const agentLoop = buildAgentLoop({ mcpManager });
|
|
75
|
+
return agentLoop;
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
name: config.name,
|
|
80
|
+
config,
|
|
81
|
+
async *chat(message, opts) {
|
|
82
|
+
const threadId = opts?.threadId ?? crypto.randomUUID();
|
|
83
|
+
const iter = loop.stream(
|
|
84
|
+
{ messages: [{ role: "user", content: message }] },
|
|
85
|
+
{
|
|
86
|
+
configurable: {
|
|
87
|
+
thread_id: threadId,
|
|
88
|
+
channel: opts?.channel ?? "cli",
|
|
89
|
+
system_prompt: config.systemPrompt,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
for await (const chunk of iter) {
|
|
94
|
+
if (chunk.agent?.messages) {
|
|
95
|
+
for (const msg of chunk.agent.messages) {
|
|
96
|
+
if (msg.content && typeof msg.content === "string") {
|
|
97
|
+
yield { type: "text" as const, content: msg.content };
|
|
98
|
+
}
|
|
99
|
+
if (msg.tool_calls) {
|
|
100
|
+
for (const tc of msg.tool_calls) {
|
|
101
|
+
yield { type: "tool_call" as const, name: tc.function?.name ?? tc.name ?? "unknown", args: typeof tc.function?.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function?.arguments ?? {} };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (chunk.tools?.messages) {
|
|
107
|
+
for (const msg of chunk.tools.messages) {
|
|
108
|
+
yield { type: "tool_result" as const, name: msg.name ?? msg.tool_name ?? "unknown", result: msg.content };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
yield { type: "done" as const, response: "" };
|
|
113
|
+
},
|
|
114
|
+
async run(task, opts) {
|
|
115
|
+
let response = "";
|
|
116
|
+
for await (const event of this.chat(task, opts)) {
|
|
117
|
+
if (event.type === "text") response += event.content;
|
|
118
|
+
}
|
|
119
|
+
return response;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
import { eventBus } from "../swarm/EventBus.ts";
|
|
4
|
+
|
|
5
|
+
export interface WebSocketLike {
|
|
6
|
+
readyState: number;
|
|
7
|
+
send(data: string | ArrayBuffer | Uint8Array): number | boolean;
|
|
8
|
+
close(code?: number, reason?: string): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const WebSocketState = {
|
|
12
|
+
CONNECTING: 0,
|
|
13
|
+
OPEN: 1,
|
|
14
|
+
CLOSING: 2,
|
|
15
|
+
CLOSED: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface CanvasComponent {
|
|
19
|
+
id: string;
|
|
20
|
+
type: "button" | "form" | "chart" | "table" | "markdown" | "text" | "image" | "card" | "progress" | "list" | "confirm";
|
|
21
|
+
props: Record<string, unknown>;
|
|
22
|
+
span?: "full" | "half";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CanvasMessage {
|
|
26
|
+
type: "canvas:render" | "canvas:update" | "canvas:clear" | "canvas:interact" | "canvas:connected" | "canvas:snapshot";
|
|
27
|
+
sessionId: string;
|
|
28
|
+
componentId?: string;
|
|
29
|
+
component?: CanvasComponent;
|
|
30
|
+
action?: string;
|
|
31
|
+
data?: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InteractionEvent {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
componentId: string;
|
|
37
|
+
action: string;
|
|
38
|
+
data: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PendingInteraction {
|
|
42
|
+
resolve: (data: unknown) => void;
|
|
43
|
+
reject: (error: Error) => void;
|
|
44
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface A2UISurfaceCache {
|
|
48
|
+
createData: Record<string, unknown>;
|
|
49
|
+
components?: unknown[];
|
|
50
|
+
dataModel?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class CanvasManager extends EventEmitter {
|
|
54
|
+
private sessions: Map<string, WebSocketLike> = new Map();
|
|
55
|
+
private pendingInteractions: Map<string, PendingInteraction> = new Map();
|
|
56
|
+
private componentCache: Map<string, CanvasComponent[]> = new Map();
|
|
57
|
+
private a2uiCache: Map<string, A2UISurfaceCache> = new Map();
|
|
58
|
+
private log = logger.child("canvas");
|
|
59
|
+
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super();
|
|
63
|
+
this.startHeartbeat();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private startHeartbeat(): void {
|
|
67
|
+
if (this.heartbeatInterval) return;
|
|
68
|
+
|
|
69
|
+
// Enviar ping a todas las sesiones cada 30 segundos
|
|
70
|
+
this.heartbeatInterval = setInterval(() => {
|
|
71
|
+
for (const [sessionId, ws] of this.sessions) {
|
|
72
|
+
if (ws.readyState === WebSocketState.OPEN) {
|
|
73
|
+
try {
|
|
74
|
+
ws.send(JSON.stringify({ type: "canvas:ping", sessionId }));
|
|
75
|
+
this.log.debug(`Heartbeat sent to ${sessionId}`);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
this.log.error(`Failed to send heartbeat to ${sessionId}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, 30000);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private stopHeartbeat(): void {
|
|
85
|
+
if (this.heartbeatInterval) {
|
|
86
|
+
clearInterval(this.heartbeatInterval);
|
|
87
|
+
this.heartbeatInterval = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
registerSession(sessionId: string, ws: WebSocketLike): void {
|
|
92
|
+
this.sessions.set(sessionId, ws);
|
|
93
|
+
this.log.info(`Canvas session registered: ${sessionId}`);
|
|
94
|
+
|
|
95
|
+
eventBus.emit("tool:completed" as any, {
|
|
96
|
+
toolName: "canvas:session:register",
|
|
97
|
+
result: { sessionId },
|
|
98
|
+
duration: 0,
|
|
99
|
+
success: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Notify the client that the session is registered
|
|
103
|
+
ws.send(JSON.stringify({ type: "canvas:connected", sessionId }));
|
|
104
|
+
|
|
105
|
+
// Replay any cached A2UI surfaces so late-connecting clients get current state
|
|
106
|
+
for (const [surfaceId, cache] of this.a2uiCache) {
|
|
107
|
+
try {
|
|
108
|
+
ws.send(JSON.stringify({ type: "a2ui:createSurface", data: cache.createData }));
|
|
109
|
+
if (cache.components && cache.components.length > 0) {
|
|
110
|
+
ws.send(JSON.stringify({ type: "a2ui:updateComponents", data: { surfaceId, components: cache.components } }));
|
|
111
|
+
}
|
|
112
|
+
if (cache.dataModel && Object.keys(cache.dataModel).length > 0) {
|
|
113
|
+
ws.send(JSON.stringify({ type: "a2ui:updateDataModel", data: { surfaceId, path: undefined, value: cache.dataModel } }));
|
|
114
|
+
}
|
|
115
|
+
this.log.debug(`Replayed A2UI surface '${surfaceId}' to session ${sessionId}`);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
this.log.warn(`Failed to replay A2UI surface '${surfaceId}' to ${sessionId}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
unregisterSession(sessionId: string): void {
|
|
123
|
+
this.sessions.delete(sessionId);
|
|
124
|
+
this.cleanupPendingInteractions(sessionId);
|
|
125
|
+
this.log.info(`Canvas session disconnected: ${sessionId}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
handleMessage(sessionId: string, data: unknown): void {
|
|
129
|
+
try {
|
|
130
|
+
const msg = JSON.parse(data as string);
|
|
131
|
+
|
|
132
|
+
this.log.debug(`Received message from session ${sessionId}: ${msg.type}`);
|
|
133
|
+
|
|
134
|
+
// === HANDSHAKE INICIAL ===
|
|
135
|
+
if (msg.type === "canvas:handshake") {
|
|
136
|
+
this.log.debug(`Handshake received from ${sessionId}`, msg);
|
|
137
|
+
const ws = this.sessions.get(sessionId);
|
|
138
|
+
|
|
139
|
+
// Confirmar handshake
|
|
140
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
141
|
+
ws.send(JSON.stringify({
|
|
142
|
+
type: "canvas:handshake:ack",
|
|
143
|
+
sessionId,
|
|
144
|
+
serverSessionId: sessionId,
|
|
145
|
+
timestamp: Date.now()
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// === HEARTBEAT: PING DEL CLIENTE ===
|
|
152
|
+
if (msg.type === "canvas:ping") {
|
|
153
|
+
const ws = this.sessions.get(sessionId);
|
|
154
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
155
|
+
ws.send(JSON.stringify({ type: "canvas:pong", sessionId }));
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// === PONG DEL CLIENTE (opcional) ===
|
|
161
|
+
if (msg.type === "canvas:pong") {
|
|
162
|
+
// El cliente respondió al ping del servidor - conexión viva
|
|
163
|
+
this.log.debug(`Pong received from ${sessionId}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (msg.type === "canvas:interact") {
|
|
168
|
+
const { componentId, action, data: interactionData } = msg.payload || msg;
|
|
169
|
+
|
|
170
|
+
if (componentId) {
|
|
171
|
+
this.resolveInteraction(sessionId, componentId, interactionData);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.emit("interaction", {
|
|
175
|
+
sessionId,
|
|
176
|
+
componentId,
|
|
177
|
+
action,
|
|
178
|
+
data: interactionData,
|
|
179
|
+
} as InteractionEvent);
|
|
180
|
+
} else if (msg.type === "canvas:get_cached") {
|
|
181
|
+
const components = this.getSessionComponents(sessionId);
|
|
182
|
+
const ws = this.sessions.get(sessionId);
|
|
183
|
+
|
|
184
|
+
this.log.info(`Sending ${components.length} cached components to session ${sessionId}`);
|
|
185
|
+
|
|
186
|
+
if (ws && ws.readyState === WebSocketState.OPEN) {
|
|
187
|
+
for (const component of components) {
|
|
188
|
+
ws.send(JSON.stringify({
|
|
189
|
+
type: "canvas:render",
|
|
190
|
+
sessionId,
|
|
191
|
+
component
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.log.error(`Invalid canvas message: ${(error as Error).message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private resolveInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
202
|
+
const key = `${sessionId}:${componentId}`;
|
|
203
|
+
const pending = this.pendingInteractions.get(key);
|
|
204
|
+
|
|
205
|
+
if (pending) {
|
|
206
|
+
clearTimeout(pending.timeoutId);
|
|
207
|
+
this.pendingInteractions.delete(key);
|
|
208
|
+
|
|
209
|
+
// Remove from cache so it doesn't reappear in the next snapshot
|
|
210
|
+
const cached = this.componentCache.get(sessionId);
|
|
211
|
+
if (cached) {
|
|
212
|
+
this.componentCache.set(sessionId, cached.filter(c => c.id !== componentId));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pending.resolve(data);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async render(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
220
|
+
// Always cache the component for later retrieval
|
|
221
|
+
const cached = this.componentCache.get(sessionId) || [];
|
|
222
|
+
const existingIdx = cached.findIndex(c => c.id === component.id);
|
|
223
|
+
if (existingIdx >= 0) {
|
|
224
|
+
cached[existingIdx] = component;
|
|
225
|
+
} else {
|
|
226
|
+
cached.push(component);
|
|
227
|
+
}
|
|
228
|
+
this.componentCache.set(sessionId, cached);
|
|
229
|
+
|
|
230
|
+
const ws = this.sessions.get(sessionId);
|
|
231
|
+
|
|
232
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
233
|
+
// Session not connected, but we cached the component
|
|
234
|
+
this.log.debug(`Session ${sessionId} NOT connected. Available: ${this.getConnectedSessions().join(", ")}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const message: CanvasMessage = {
|
|
239
|
+
type: "canvas:render",
|
|
240
|
+
sessionId,
|
|
241
|
+
component,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
this.log.debug(`Sending render to session ${sessionId}: ${component.id}`);
|
|
245
|
+
ws.send(JSON.stringify(message));
|
|
246
|
+
this.log.debug(`Rendered component ${component.id} to session ${sessionId}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async update(sessionId: string, component: CanvasComponent): Promise<void> {
|
|
250
|
+
const ws = this.sessions.get(sessionId);
|
|
251
|
+
|
|
252
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
253
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const message: CanvasMessage = {
|
|
257
|
+
type: "canvas:update",
|
|
258
|
+
sessionId,
|
|
259
|
+
component,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
ws.send(JSON.stringify(message));
|
|
263
|
+
this.log.debug(`Updated component ${component.id} in session ${sessionId}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async clear(sessionId: string): Promise<void> {
|
|
267
|
+
const ws = this.sessions.get(sessionId);
|
|
268
|
+
|
|
269
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
270
|
+
throw new Error(`Session not connected: ${sessionId}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const message: CanvasMessage = {
|
|
274
|
+
type: "canvas:clear",
|
|
275
|
+
sessionId,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
ws.send(JSON.stringify(message));
|
|
279
|
+
this.log.debug(`Cleared canvas for session ${sessionId}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async sendA2UIMessage(sessionId: string, messageType: string, data: Record<string, unknown>): Promise<void> {
|
|
283
|
+
// Update A2UI cache so late-connecting clients can receive current state
|
|
284
|
+
const surfaceId = data.surfaceId as string | undefined;
|
|
285
|
+
if (surfaceId) {
|
|
286
|
+
if (messageType === "a2ui:createSurface") {
|
|
287
|
+
this.a2uiCache.set(surfaceId, { createData: data });
|
|
288
|
+
} else if (messageType === "a2ui:updateComponents") {
|
|
289
|
+
const cached = this.a2uiCache.get(surfaceId);
|
|
290
|
+
if (cached) cached.components = data.components as unknown[];
|
|
291
|
+
} else if (messageType === "a2ui:updateDataModel") {
|
|
292
|
+
const cached = this.a2uiCache.get(surfaceId);
|
|
293
|
+
if (cached) {
|
|
294
|
+
const path = data.path as string | undefined;
|
|
295
|
+
const value = data.value as Record<string, unknown>;
|
|
296
|
+
if (!path || path === "/") {
|
|
297
|
+
cached.dataModel = value;
|
|
298
|
+
} else {
|
|
299
|
+
cached.dataModel = cached.dataModel ?? {};
|
|
300
|
+
// Store the full model snapshot when possible; partial paths accumulate
|
|
301
|
+
const key = path.replace(/^\//, "").split("/")[0];
|
|
302
|
+
if (key) cached.dataModel[key] = value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else if (messageType === "a2ui:deleteSurface") {
|
|
306
|
+
this.a2uiCache.delete(surfaceId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const ws = this.sessions.get(sessionId);
|
|
311
|
+
|
|
312
|
+
if (!ws || ws.readyState !== WebSocketState.OPEN) {
|
|
313
|
+
const connected = this.getConnectedSessions();
|
|
314
|
+
this.log.warn(`Session ${sessionId} NOT connected for A2UI message. Cached for replay. Available: ${connected.join(", ")}`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
ws.send(JSON.stringify({ type: messageType, data }));
|
|
319
|
+
this.log.debug(`Sent A2UI message '${messageType}' to session ${sessionId}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async waitForInteraction(
|
|
323
|
+
sessionId: string,
|
|
324
|
+
componentId: string,
|
|
325
|
+
timeout = 300000
|
|
326
|
+
): Promise<unknown> {
|
|
327
|
+
const key = `${sessionId}:${componentId}`;
|
|
328
|
+
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
const timeoutId = setTimeout(() => {
|
|
331
|
+
this.pendingInteractions.delete(key);
|
|
332
|
+
reject(new Error(`Interaction timeout for ${componentId}`));
|
|
333
|
+
}, timeout);
|
|
334
|
+
|
|
335
|
+
this.pendingInteractions.set(key, { resolve, reject, timeoutId });
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
handleInteraction(sessionId: string, componentId: string, data: unknown): void {
|
|
340
|
+
this.resolveInteraction(sessionId, componentId, data);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getSessionComponents(sessionId: string): CanvasComponent[] {
|
|
344
|
+
return this.componentCache.get(sessionId) || [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
isSessionConnected(sessionId: string): boolean {
|
|
348
|
+
const ws = this.sessions.get(sessionId);
|
|
349
|
+
return ws !== undefined && ws.readyState === WebSocketState.OPEN;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getConnectedSessions(): string[] {
|
|
353
|
+
return Array.from(this.sessions.entries())
|
|
354
|
+
.filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
|
|
355
|
+
.map(([id]) => id);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getStats(): { totalSessions: number; activeSessions: number; pendingInteractions: number } {
|
|
359
|
+
return {
|
|
360
|
+
totalSessions: this.sessions.size,
|
|
361
|
+
activeSessions: this.getConnectedSessions().length,
|
|
362
|
+
pendingInteractions: this.pendingInteractions.size,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private cleanupPendingInteractions(sessionId: string): void {
|
|
367
|
+
for (const [key, pending] of this.pendingInteractions) {
|
|
368
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
369
|
+
clearTimeout(pending.timeoutId);
|
|
370
|
+
pending.reject(new Error(`Session disconnected: ${sessionId}`));
|
|
371
|
+
this.pendingInteractions.delete(key);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
clearAll(): void {
|
|
377
|
+
this.stopHeartbeat();
|
|
378
|
+
|
|
379
|
+
for (const pending of this.pendingInteractions.values()) {
|
|
380
|
+
clearTimeout(pending.timeoutId);
|
|
381
|
+
pending.reject(new Error("Canvas manager cleared"));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.pendingInteractions.clear();
|
|
385
|
+
this.sessions.clear();
|
|
386
|
+
this.log.info("Canvas manager cleared");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export const canvasManager = new CanvasManager();
|