@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.
- package/dist/client.d.ts +138 -11
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +319 -21
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +88 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +120 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +30 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/client.ts +2380 -0
- package/src/crypto.ts +153 -0
- package/src/errors.ts +292 -0
- package/src/index.ts +114 -0
- package/src/lockfile.ts +493 -0
- package/src/transports/broker.ts +682 -0
- package/src/transports/native.ts +307 -0
- package/src/transports/stdio.ts +580 -0
- package/src/types.ts +1011 -0
- package/src/zod.ts +69 -0
- package/tsconfig.json +52 -0
|
@@ -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
|
+
}
|