@kadi.build/core 0.3.4 → 0.5.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.
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Native transport for kadi-core v0.1.0
3
+ *
4
+ * Loads abilities that run in the SAME Node.js process.
5
+ * This is the fastest transport - direct function calls with zero serialization.
6
+ *
7
+ * How it works:
8
+ * 1. Dynamic import the module at the given path
9
+ * 2. Verify it exports a KadiClient as default
10
+ * 3. Return a LoadedAbility that delegates to the client
11
+ *
12
+ * Use case: Abilities written in TypeScript/JavaScript that you want
13
+ * to load without spawning a child process.
14
+ */
15
+
16
+ import type { LoadedAbility, InvokeOptions, ToolDefinition, EventHandler, ToolExecutionBridge } from '../types.js';
17
+ import { KadiError } from '../errors.js';
18
+
19
+ // ═══════════════════════════════════════════════════════════════════════════════
20
+ // TYPE GUARD
21
+ // ═══════════════════════════════════════════════════════════════════════════════
22
+
23
+ /**
24
+ * Interface for what we expect from a KadiClient.
25
+ * We don't import KadiClient directly to avoid circular dependencies.
26
+ */
27
+ interface KadiClientLike {
28
+ /** Get agent information (name, tools, etc.) */
29
+ readAgentJson(): AgentJsonLike;
30
+
31
+ /** Create a bridge for tool execution (internal API) */
32
+ createToolBridge(): ToolExecutionBridge;
33
+
34
+ /** Set event handler callback (for events) */
35
+ setEventHandler(handler: (event: string, data: unknown) => void): void;
36
+ }
37
+
38
+ /**
39
+ * Minimal agent.json structure we need.
40
+ */
41
+ interface AgentJsonLike {
42
+ name: string;
43
+ tools?: ToolDefinition[];
44
+ }
45
+
46
+ /**
47
+ * Check if an object looks like a KadiClient.
48
+ * We check for the methods we need rather than using instanceof,
49
+ * because the loaded module might have a different version of KadiClient.
50
+ */
51
+ function isKadiClient(obj: unknown): obj is KadiClientLike {
52
+ if (obj === null || typeof obj !== 'object') {
53
+ return false;
54
+ }
55
+
56
+ // obj is confirmed to be a non-null object, safe to access properties
57
+ const candidate = obj as Record<string, unknown>;
58
+
59
+ // Must have readAgentJson method
60
+ if (typeof candidate.readAgentJson !== 'function') {
61
+ return false;
62
+ }
63
+
64
+ // Must have createToolBridge method
65
+ if (typeof candidate.createToolBridge !== 'function') {
66
+ return false;
67
+ }
68
+
69
+ // Must have setEventHandler method (for events)
70
+ if (typeof candidate.setEventHandler !== 'function') {
71
+ return false;
72
+ }
73
+
74
+ return true;
75
+ }
76
+
77
+ // ═══════════════════════════════════════════════════════════════════════════════
78
+ // NATIVE TRANSPORT
79
+ // ═══════════════════════════════════════════════════════════════════════════════
80
+
81
+ /**
82
+ * Load an in-process ability via dynamic import.
83
+ *
84
+ * The module at the given path must export a KadiClient instance as default.
85
+ * This is the standard pattern for KADI abilities.
86
+ *
87
+ * @param path - Absolute path to the ability module directory
88
+ * @param entrypoint - Optional entry point file (e.g., "index.js"). If not provided,
89
+ * uses Node.js module resolution (package.json main/exports or index.js).
90
+ * @returns LoadedAbility that can invoke tools
91
+ * @throws KadiError if the module can't be loaded or doesn't export a KadiClient
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // The ability module (calculator/index.ts):
96
+ * const client = new KadiClient({ name: 'calculator' });
97
+ * client.registerTool(...);
98
+ * export default client;
99
+ *
100
+ * // Loading it:
101
+ * const calc = await loadNativeTransport('/path/to/calculator');
102
+ * const result = await calc.invoke('add', { a: 5, b: 3 });
103
+ *
104
+ * // With explicit entrypoint:
105
+ * const calc = await loadNativeTransport('/path/to/calc', 'main.js');
106
+ *
107
+ * // With default timeout:
108
+ * const calc = await loadNativeTransport('/path/to/calc', undefined, { timeout: 300000 });
109
+ * ```
110
+ */
111
+ export async function loadNativeTransport(
112
+ path: string,
113
+ entrypoint?: string,
114
+ options: { timeout?: number } = {}
115
+ ): Promise<LoadedAbility> {
116
+ const defaultTimeout = options.timeout;
117
+ // Build the import path - append entrypoint if provided
118
+ const importPath = entrypoint ? `${path}/${entrypoint}` : path;
119
+
120
+ // Step 1: Dynamic import the module
121
+ let module: Record<string, unknown>;
122
+ try {
123
+ module = await import(importPath);
124
+ } catch (error) {
125
+ throw new KadiError(
126
+ `Failed to import ability at "${importPath}"`,
127
+ 'NATIVE_IMPORT_ERROR',
128
+ {
129
+ path: importPath,
130
+ reason: error instanceof Error ? error.message : String(error),
131
+ hint: 'Check that the path is correct and the module has no syntax errors',
132
+ }
133
+ );
134
+ }
135
+
136
+ // Step 2: Get the default export
137
+ const client = module.default;
138
+
139
+ if (client === undefined) {
140
+ throw new KadiError(
141
+ `Module at "${importPath}" has no default export`,
142
+ 'ABILITY_LOAD_FAILED',
143
+ {
144
+ path: importPath,
145
+ hint: 'The ability must export a KadiClient instance as default export',
146
+ alternative: 'Example: export default client;',
147
+ }
148
+ );
149
+ }
150
+
151
+ // Step 3: Verify it's a KadiClient
152
+ if (!isKadiClient(client)) {
153
+ throw new KadiError(
154
+ `Module at "${importPath}" does not export a KadiClient`,
155
+ 'ABILITY_LOAD_FAILED',
156
+ {
157
+ path: importPath,
158
+ hint: 'The default export must be a KadiClient instance',
159
+ alternative: 'Make sure you\'re exporting the client, not the class',
160
+ }
161
+ );
162
+ }
163
+
164
+ // Step 4: Get agent information
165
+ let agentJson: AgentJsonLike;
166
+ try {
167
+ agentJson = client.readAgentJson();
168
+ } catch (error) {
169
+ throw new KadiError(
170
+ `Failed to read agent information from "${importPath}"`,
171
+ 'ABILITY_LOAD_FAILED',
172
+ {
173
+ path: importPath,
174
+ reason: error instanceof Error ? error.message : String(error),
175
+ hint: 'The ability\'s readAgentJson() method threw an error',
176
+ }
177
+ );
178
+ }
179
+
180
+ // Step 5: Get the tool execution bridge
181
+ const bridge = client.createToolBridge();
182
+
183
+ // Step 6: Set up event handling
184
+ // Map of event name → array of handlers
185
+ const eventHandlers = new Map<string, Set<EventHandler>>();
186
+
187
+ // When ability calls emit(), this callback fires
188
+ client.setEventHandler((event: string, data: unknown) => {
189
+ const handlers = eventHandlers.get(event);
190
+ if (handlers) {
191
+ for (const handler of handlers) {
192
+ // Fire handlers asynchronously, don't block
193
+ Promise.resolve(handler(data)).catch((err) => {
194
+ // Using console.error directly: event handlers run async and we don't
195
+ // want to require logger injection into the transport layer
196
+ console.error(`[KADI] Event handler error for "${event}":`, err);
197
+ });
198
+ }
199
+ }
200
+ });
201
+
202
+ // Step 7: Return LoadedAbility interface
203
+ return {
204
+ name: agentJson.name,
205
+ transport: 'native',
206
+
207
+ /**
208
+ * Invoke a tool on this ability.
209
+ * For native transport, this is a direct function call - no serialization.
210
+ *
211
+ * @param toolName - Name of the tool to invoke
212
+ * @param params - Input parameters for the tool
213
+ * @param invokeOptions - Optional settings (timeout override)
214
+ */
215
+ async invoke<T>(toolName: string, params: unknown, invokeOptions?: InvokeOptions): Promise<T> {
216
+ try {
217
+ const invocation = bridge.executeToolHandler(toolName, params);
218
+
219
+ // Per-call timeout overrides default timeout
220
+ const effectiveTimeout = invokeOptions?.timeout ?? defaultTimeout;
221
+
222
+ // If timeout is specified, race against it
223
+ if (effectiveTimeout !== undefined) {
224
+ let timeoutId: ReturnType<typeof setTimeout>;
225
+ const timeoutPromise = new Promise<never>((_, reject) => {
226
+ timeoutId = setTimeout(() => {
227
+ reject(new KadiError(
228
+ `Timeout after ${effectiveTimeout}ms waiting for "${toolName}"`,
229
+ 'TIMEOUT',
230
+ { toolName, timeoutMs: effectiveTimeout }
231
+ ));
232
+ }, effectiveTimeout);
233
+ });
234
+
235
+ try {
236
+ const result = await Promise.race([invocation, timeoutPromise]);
237
+ return result as T;
238
+ } finally {
239
+ clearTimeout(timeoutId!);
240
+ }
241
+ }
242
+
243
+ // No timeout - just await directly
244
+ const result = await invocation;
245
+ return result as T;
246
+ } catch (error) {
247
+ // Re-throw KadiErrors as-is
248
+ if (error instanceof KadiError) {
249
+ throw error;
250
+ }
251
+ // Wrap other errors
252
+ throw new KadiError(
253
+ `Tool "${toolName}" invocation failed`,
254
+ 'TOOL_INVOCATION_FAILED',
255
+ {
256
+ toolName,
257
+ ability: agentJson.name,
258
+ reason: error instanceof Error ? error.message : String(error),
259
+ }
260
+ );
261
+ }
262
+ },
263
+
264
+ /**
265
+ * Get the list of tools this ability provides.
266
+ */
267
+ getTools(): ToolDefinition[] {
268
+ return agentJson.tools ?? [];
269
+ },
270
+
271
+ /**
272
+ * Subscribe to events from this ability.
273
+ */
274
+ on(event: string, handler: EventHandler): void {
275
+ let handlers = eventHandlers.get(event);
276
+ if (!handlers) {
277
+ handlers = new Set();
278
+ eventHandlers.set(event, handlers);
279
+ }
280
+ handlers.add(handler);
281
+ },
282
+
283
+ /**
284
+ * Unsubscribe from events.
285
+ */
286
+ off(event: string, handler: EventHandler): void {
287
+ const handlers = eventHandlers.get(event);
288
+ if (handlers) {
289
+ handlers.delete(handler);
290
+ if (handlers.size === 0) {
291
+ eventHandlers.delete(event);
292
+ }
293
+ }
294
+ },
295
+
296
+ /**
297
+ * Disconnect and cleanup.
298
+ * For native transport, there's nothing to cleanup - we share the process.
299
+ */
300
+ async disconnect(): Promise<void> {
301
+ // Clear all event handlers
302
+ eventHandlers.clear();
303
+ // Native abilities share the process with us.
304
+ // No other cleanup needed.
305
+ },
306
+ };
307
+ }