@agnt5/sdk 0.2.1

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.
Files changed (126) hide show
  1. package/README.md +183 -0
  2. package/dist/__tests__/integration/helpers.d.ts +41 -0
  3. package/dist/__tests__/integration/helpers.d.ts.map +1 -0
  4. package/dist/__tests__/integration/helpers.js +78 -0
  5. package/dist/__tests__/integration/helpers.js.map +1 -0
  6. package/dist/agent.d.ts +260 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +493 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/async-context.d.ts +57 -0
  11. package/dist/async-context.d.ts.map +1 -0
  12. package/dist/async-context.js +52 -0
  13. package/dist/async-context.js.map +1 -0
  14. package/dist/batch.d.ts +116 -0
  15. package/dist/batch.d.ts.map +1 -0
  16. package/dist/batch.js +98 -0
  17. package/dist/batch.js.map +1 -0
  18. package/dist/chat.d.ts +137 -0
  19. package/dist/chat.d.ts.map +1 -0
  20. package/dist/chat.js +278 -0
  21. package/dist/chat.js.map +1 -0
  22. package/dist/client.d.ts +394 -0
  23. package/dist/client.d.ts.map +1 -0
  24. package/dist/client.js +757 -0
  25. package/dist/client.js.map +1 -0
  26. package/dist/context.d.ts +47 -0
  27. package/dist/context.d.ts.map +1 -0
  28. package/dist/context.js +244 -0
  29. package/dist/context.js.map +1 -0
  30. package/dist/errors.d.ts +148 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +201 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/eval.d.ts +242 -0
  35. package/dist/eval.d.ts.map +1 -0
  36. package/dist/eval.js +452 -0
  37. package/dist/eval.js.map +1 -0
  38. package/dist/event-emitter.d.ts +28 -0
  39. package/dist/event-emitter.d.ts.map +1 -0
  40. package/dist/event-emitter.js +79 -0
  41. package/dist/event-emitter.js.map +1 -0
  42. package/dist/events.d.ts +285 -0
  43. package/dist/events.d.ts.map +1 -0
  44. package/dist/events.js +256 -0
  45. package/dist/events.js.map +1 -0
  46. package/dist/function.d.ts +61 -0
  47. package/dist/function.d.ts.map +1 -0
  48. package/dist/function.js +78 -0
  49. package/dist/function.js.map +1 -0
  50. package/dist/index.d.ts +67 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +80 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/lm.d.ts +301 -0
  55. package/dist/lm.d.ts.map +1 -0
  56. package/dist/lm.js +283 -0
  57. package/dist/lm.js.map +1 -0
  58. package/dist/logging.d.ts +68 -0
  59. package/dist/logging.d.ts.map +1 -0
  60. package/dist/logging.js +165 -0
  61. package/dist/logging.js.map +1 -0
  62. package/dist/mcp-server.d.ts +98 -0
  63. package/dist/mcp-server.d.ts.map +1 -0
  64. package/dist/mcp-server.js +307 -0
  65. package/dist/mcp-server.js.map +1 -0
  66. package/dist/mcp.d.ts +73 -0
  67. package/dist/mcp.d.ts.map +1 -0
  68. package/dist/mcp.js +224 -0
  69. package/dist/mcp.js.map +1 -0
  70. package/dist/memory.d.ts +234 -0
  71. package/dist/memory.d.ts.map +1 -0
  72. package/dist/memory.js +609 -0
  73. package/dist/memory.js.map +1 -0
  74. package/dist/platform-adapters.d.ts +121 -0
  75. package/dist/platform-adapters.d.ts.map +1 -0
  76. package/dist/platform-adapters.js +174 -0
  77. package/dist/platform-adapters.js.map +1 -0
  78. package/dist/platform-context.d.ts +55 -0
  79. package/dist/platform-context.d.ts.map +1 -0
  80. package/dist/platform-context.js +196 -0
  81. package/dist/platform-context.js.map +1 -0
  82. package/dist/retry-utils.d.ts +169 -0
  83. package/dist/retry-utils.d.ts.map +1 -0
  84. package/dist/retry-utils.js +304 -0
  85. package/dist/retry-utils.js.map +1 -0
  86. package/dist/sandbox.d.ts +103 -0
  87. package/dist/sandbox.d.ts.map +1 -0
  88. package/dist/sandbox.js +168 -0
  89. package/dist/sandbox.js.map +1 -0
  90. package/dist/schema-utils.d.ts +250 -0
  91. package/dist/schema-utils.d.ts.map +1 -0
  92. package/dist/schema-utils.js +444 -0
  93. package/dist/schema-utils.js.map +1 -0
  94. package/dist/scorer.d.ts +130 -0
  95. package/dist/scorer.d.ts.map +1 -0
  96. package/dist/scorer.js +211 -0
  97. package/dist/scorer.js.map +1 -0
  98. package/dist/state.d.ts +92 -0
  99. package/dist/state.d.ts.map +1 -0
  100. package/dist/state.js +151 -0
  101. package/dist/state.js.map +1 -0
  102. package/dist/tool.d.ts +120 -0
  103. package/dist/tool.d.ts.map +1 -0
  104. package/dist/tool.js +215 -0
  105. package/dist/tool.js.map +1 -0
  106. package/dist/tracing.d.ts +82 -0
  107. package/dist/tracing.d.ts.map +1 -0
  108. package/dist/tracing.js +206 -0
  109. package/dist/tracing.js.map +1 -0
  110. package/dist/types.d.ts +139 -0
  111. package/dist/types.d.ts.map +1 -0
  112. package/dist/types.js +2 -0
  113. package/dist/types.js.map +1 -0
  114. package/dist/worker.d.ts +111 -0
  115. package/dist/worker.d.ts.map +1 -0
  116. package/dist/worker.js +944 -0
  117. package/dist/worker.js.map +1 -0
  118. package/dist/workflow-utils.d.ts +257 -0
  119. package/dist/workflow-utils.d.ts.map +1 -0
  120. package/dist/workflow-utils.js +370 -0
  121. package/dist/workflow-utils.js.map +1 -0
  122. package/dist/workflow.d.ts +78 -0
  123. package/dist/workflow.d.ts.map +1 -0
  124. package/dist/workflow.js +138 -0
  125. package/dist/workflow.js.map +1 -0
  126. package/package.json +86 -0
package/dist/worker.js ADDED
@@ -0,0 +1,944 @@
1
+ import { FunctionRegistry } from './function.js';
2
+ import { WorkflowRegistry } from './workflow.js';
3
+ import { ToolRegistry } from './tool.js';
4
+ import { ScorerRegistry, isScorer } from './scorer.js';
5
+ import { Message } from './agent.js';
6
+ import { ChatBot } from './chat.js';
7
+ import { runWithContext } from './async-context.js';
8
+ import { WaitingForUserInputError } from './errors.js';
9
+ import { EventEmitter } from './event-emitter.js';
10
+ import { generateCid, runStarted, runCompleted, runFailed, functionStarted, functionCompleted, functionFailed, workflowStarted, workflowCompleted, workflowFailed, workflowPaused, toolStarted, toolCompleted, toolFailed, } from './events.js';
11
+ import { createRequire } from 'module';
12
+ import { fileURLToPath } from 'url';
13
+ import { dirname, join } from 'path';
14
+ // Dynamic import for native bindings
15
+ let nativeBindings = null;
16
+ /**
17
+ * Load native bindings based on platform
18
+ */
19
+ function loadNativeBindings() {
20
+ if (nativeBindings)
21
+ return nativeBindings;
22
+ try {
23
+ const runtime = getRuntime();
24
+ if (runtime === 'edge') {
25
+ // TODO: Load WASM bindings for edge runtimes
26
+ throw new Error('WASM bindings not yet implemented');
27
+ }
28
+ // Load NAPI bindings for Node.js/Bun/Deno
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ const require = createRequire(import.meta.url);
32
+ // Try multiple paths to find the native module
33
+ const possiblePaths = [
34
+ join(__dirname, '../../native/agnt5-sdk-native.darwin-arm64.node'), // From dist/src (macOS)
35
+ join(__dirname, '../native/agnt5-sdk-native.darwin-arm64.node'), // From src (macOS)
36
+ join(__dirname, '../../native/agnt5-sdk-native.linux-x64-gnu.node'), // From dist/src (Linux)
37
+ join(__dirname, '../native/agnt5-sdk-native.linux-x64-gnu.node'), // From src (Linux)
38
+ join(__dirname, '../../native/agnt5-sdk-native.linux-x64.node'), // From dist/src (Linux fallback)
39
+ join(__dirname, '../native/agnt5-sdk-native.linux-x64.node'), // From src (Linux fallback)
40
+ ];
41
+ for (const nativePath of possiblePaths) {
42
+ try {
43
+ nativeBindings = require(nativePath);
44
+ return nativeBindings;
45
+ }
46
+ catch (e) {
47
+ // Try next path
48
+ continue;
49
+ }
50
+ }
51
+ throw new Error('Could not find native bindings in any expected location');
52
+ }
53
+ catch (error) {
54
+ throw new Error(`Failed to load native bindings: ${error.message}`);
55
+ }
56
+ }
57
+ /**
58
+ * Simple context implementation
59
+ */
60
+ class SimpleContext {
61
+ constructor(invocationId, runId, attempt, serviceName, state = new Map()) {
62
+ this.invocationId = invocationId;
63
+ this.runId = runId;
64
+ this.attempt = attempt;
65
+ this.serviceName = serviceName;
66
+ this.state = state;
67
+ this._stepCounter = 0;
68
+ this._stepCache = new Map();
69
+ // HITL state — populated by Worker.processMessage on resume from message metadata.
70
+ // _pauseIndex is the running counter incremented by each waitForUser call.
71
+ // _userResponses is a sparse cache of {pauseIndex -> user response} populated
72
+ // from the resume metadata so the workflow handler can replay past pauses.
73
+ //
74
+ // Multi-step replay works by persisting the full history of past responses
75
+ // into the `step_events` field of the workflow.paused event metadata. On
76
+ // every resume, loadReplayState hydrates _userResponses from step_events
77
+ // (historical) + user_response/pause_index (current), so the workflow
78
+ // re-execution can advance through all prior pauses before reaching a new
79
+ // one. Mirrors sdk-python's WorkflowEntity._completed_steps persistence.
80
+ this._pauseIndex = 0;
81
+ this._userResponses = new Map();
82
+ }
83
+ /**
84
+ * Seed HITL replay state from incoming message metadata.
85
+ * Called by Worker.processMessage before invoking the workflow handler.
86
+ *
87
+ * Reads `user_response` and `pause_index` from metadata (set by the gateway's
88
+ * resume endpoint at runtime/crates/gateway/src/handlers/signals.rs) and
89
+ * caches the response so the next waitForUser call at that index returns it
90
+ * instead of throwing.
91
+ */
92
+ loadReplayState(metadata) {
93
+ if (!metadata)
94
+ return;
95
+ // First, rehydrate any historical responses from step_events. These are
96
+ // accumulated by the emit path below and propagated through the gateway's
97
+ // resume handler (which copies the latest workflow.paused metadata into
98
+ // the next dispatch). Each entry is {pauseIndex -> decoded response}.
99
+ const stepEventsStr = metadata.step_events;
100
+ if (stepEventsStr) {
101
+ try {
102
+ const parsed = JSON.parse(stepEventsStr);
103
+ for (const [idxStr, value] of Object.entries(parsed)) {
104
+ const idx = Number.parseInt(idxStr, 10);
105
+ if (!Number.isNaN(idx)) {
106
+ this._userResponses.set(idx, value);
107
+ }
108
+ }
109
+ }
110
+ catch {
111
+ // Ignore corrupt step_events — fall back to the current pause only.
112
+ }
113
+ }
114
+ // Rehydrate persistent ctx.step() cache from completed_steps metadata.
115
+ // Any ctx.step() invocation persisted prior to the last pause is replayed
116
+ // on resume without re-execution; step() emits workflow.step.completed
117
+ // with cache_hit=true on each hit so the durability projection can prove
118
+ // replay happened.
119
+ const completedStepsStr = metadata.completed_steps;
120
+ if (completedStepsStr) {
121
+ try {
122
+ const parsed = JSON.parse(completedStepsStr);
123
+ for (const [key, value] of Object.entries(parsed)) {
124
+ this._stepCache.set(key, value);
125
+ }
126
+ }
127
+ catch {
128
+ // Best effort: ignore corrupt completed_steps
129
+ }
130
+ }
131
+ // Then layer the current resume's user_response on top at its pause_index.
132
+ const userResponse = metadata.user_response;
133
+ if (userResponse === undefined)
134
+ return;
135
+ const pauseIndexStr = metadata.pause_index ?? '0';
136
+ const pauseIndex = Number.parseInt(pauseIndexStr, 10);
137
+ if (Number.isNaN(pauseIndex))
138
+ return;
139
+ // Wire-format decoding mirrors sdk-python's wait_for_user (workflow.py:1520):
140
+ // "__skipped__" → null, "__custom__:..." → strip prefix.
141
+ let decoded = userResponse;
142
+ if (userResponse === '__skipped__' || userResponse === '__skip__') {
143
+ decoded = null;
144
+ }
145
+ else if (userResponse.startsWith('__custom__:')) {
146
+ decoded = userResponse.slice('__custom__:'.length);
147
+ }
148
+ this._userResponses.set(pauseIndex, decoded);
149
+ }
150
+ setEmitter(emitter) {
151
+ this._emitter = emitter;
152
+ }
153
+ setNativeWorker(worker) {
154
+ this._nativeWorker = worker;
155
+ }
156
+ /**
157
+ * Store the workflow + run correlation IDs so waitForUser can emit
158
+ * workflow.paused events with the correct parent/child linkage.
159
+ * Called by Worker.processMessage before invoking a workflow handler.
160
+ */
161
+ setWorkflowCorrelation(workflowCid, runCid) {
162
+ this._workflowCid = workflowCid;
163
+ this._runCid = runCid;
164
+ }
165
+ async emit(event) {
166
+ if (this._emitter) {
167
+ await this._emitter.emit(event);
168
+ }
169
+ }
170
+ get logger() {
171
+ const runId = this.runId;
172
+ return {
173
+ info: (message, meta) => {
174
+ console.log(`[INFO] ${message}`, meta || {});
175
+ nativeBindings?.logFromTypescript('INFO', message, runId, null, null, meta ?? null);
176
+ },
177
+ error: (message, meta) => {
178
+ console.error(`[ERROR] ${message}`, meta || {});
179
+ nativeBindings?.logFromTypescript('ERROR', message, runId, null, null, meta ?? null);
180
+ },
181
+ warn: (message, meta) => {
182
+ console.warn(`[WARN] ${message}`, meta || {});
183
+ nativeBindings?.logFromTypescript('WARN', message, runId, null, null, meta ?? null);
184
+ },
185
+ debug: (message, meta) => {
186
+ console.debug(`[DEBUG] ${message}`, meta || {});
187
+ nativeBindings?.logFromTypescript('DEBUG', message, runId, null, null, meta ?? null);
188
+ },
189
+ };
190
+ }
191
+ async get(key, defaultValue) {
192
+ return this.state.has(key) ? this.state.get(key) : defaultValue;
193
+ }
194
+ async set(key, value) {
195
+ this.state.set(key, value);
196
+ }
197
+ async delete(key) {
198
+ return this.state.delete(key);
199
+ }
200
+ /**
201
+ * Pause the workflow and request user input (HITL).
202
+ *
203
+ * On first call: throws WaitingForUserInputError, which Worker.processMessage
204
+ * catches and propagates as a `waiting_for_user_input` response to the
205
+ * platform. On resume, the platform re-dispatches the workflow with the
206
+ * user's response in metadata; loadReplayState seeds _userResponses, and
207
+ * the next call at the matching pauseIndex returns the cached value.
208
+ *
209
+ * Mirrors sdk-python/src/agnt5/workflow.py wait_for_user. The TS resume
210
+ * cache currently only restores the LATEST pause — see the FIXME at
211
+ * _userResponses for the multi-step replay limitation.
212
+ */
213
+ async waitForUser(question, options) {
214
+ const pauseIndex = this._pauseIndex++;
215
+ // Resume path: response was injected from metadata before handler ran.
216
+ if (this._userResponses.has(pauseIndex)) {
217
+ return this._userResponses.get(pauseIndex);
218
+ }
219
+ // First-time path: emit workflow.paused, then throw to unwind the handler.
220
+ //
221
+ // The runtime's gateway tail treats `workflow.paused` as a terminal event
222
+ // for /v1/workflows/:name/run and transitions the run to the `paused`
223
+ // status via apply_paused. Without this emission, the /run endpoint hangs
224
+ // until the sync deadline. Mirrors sdk-python's wait_for_input which
225
+ // emits Paused via ctx.emit() before raising WaitingForUserInputException.
226
+ const stepName = `wait_for_user_${pauseIndex}`;
227
+ if (this._emitter && this._workflowCid && this._runCid) {
228
+ const metadata = {
229
+ pause_reason: 'user_input_required',
230
+ pause_index: String(pauseIndex),
231
+ step_name: stepName,
232
+ question,
233
+ };
234
+ if (options?.inputType)
235
+ metadata.input_type = options.inputType;
236
+ if (options?.allowCustom !== undefined)
237
+ metadata.allow_custom = String(options.allowCustom);
238
+ if (options?.skippable !== undefined)
239
+ metadata.skippable = String(options.skippable);
240
+ if (options?.options)
241
+ metadata.options = JSON.stringify(options.options);
242
+ // Persist history of completed pauses so subsequent resumes can replay
243
+ // the full chain. The gateway's resume handler copies the latest
244
+ // workflow.paused metadata verbatim onto the next dispatch, so every
245
+ // entry here will flow back into loadReplayState on the next run.
246
+ if (this._userResponses.size > 0) {
247
+ const stepEvents = {};
248
+ for (const [idx, resp] of this._userResponses.entries()) {
249
+ stepEvents[String(idx)] = resp;
250
+ }
251
+ metadata.step_events = JSON.stringify(stepEvents);
252
+ }
253
+ // Also persist ctx.step() results so durable steps replay across the
254
+ // resume boundary. Without this, _stepCache is instance-scoped to this
255
+ // dispatch and all prior step results would re-execute on every resume.
256
+ if (this._stepCache.size > 0) {
257
+ const completedSteps = {};
258
+ for (const [key, value] of this._stepCache.entries()) {
259
+ completedSteps[key] = value;
260
+ }
261
+ metadata.completed_steps = JSON.stringify(completedSteps);
262
+ }
263
+ try {
264
+ await this._emitter.emit(workflowPaused(this._workflowCid, this._runCid, {
265
+ reason: 'user_input_required',
266
+ pauseData: {
267
+ question,
268
+ input_type: options?.inputType,
269
+ options: options?.options,
270
+ pause_index: pauseIndex,
271
+ step_name: stepName,
272
+ allow_custom: options?.allowCustom,
273
+ skippable: options?.skippable,
274
+ },
275
+ metadata,
276
+ }));
277
+ }
278
+ catch (emitErr) {
279
+ // Best-effort: if the checkpoint write fails, we still want to throw
280
+ // the pause exception so the handler unwinds. The /run endpoint will
281
+ // time out rather than hang, and the caller gets a clear error.
282
+ console.error('Failed to emit workflow.paused event:', emitErr);
283
+ }
284
+ }
285
+ throw new WaitingForUserInputError({
286
+ runId: this.runId,
287
+ question,
288
+ inputType: options?.inputType,
289
+ options: options?.options,
290
+ pauseIndex,
291
+ allowCustom: options?.allowCustom,
292
+ skippable: options?.skippable,
293
+ stepName,
294
+ });
295
+ }
296
+ /**
297
+ * Execute a durable step with checkpointing.
298
+ *
299
+ * On first execution: runs fn(), caches result, emits checkpoint.
300
+ * On replay: returns cached result without re-executing.
301
+ */
302
+ async step(stepName, fn) {
303
+ const stepKey = `step:${stepName}:${this._stepCounter++}`;
304
+ // Cache hit: either same-run (local) or cross-dispatch (rehydrated from
305
+ // workflow.paused metadata by loadReplayState). On a hit, emit a
306
+ // workflow.step.completed event with cache_hit=true so downstream
307
+ // projections and tests can prove the step was replayed rather than
308
+ // re-executed. Mirrors sdk-python's durable task replay semantics.
309
+ if (this._stepCache.has(stepKey)) {
310
+ const cached = this._stepCache.get(stepKey);
311
+ if (this._nativeWorker?.emitCheckpoint) {
312
+ try {
313
+ await this._nativeWorker.emitCheckpoint(this.runId, 'workflow.step.completed', JSON.stringify({
314
+ step_key: stepKey,
315
+ step_name: stepName,
316
+ output: cached,
317
+ duration_ms: 0,
318
+ }), this._stepCounter, { cache_hit: 'true' }, Date.now() * 1000000);
319
+ }
320
+ catch { /* checkpoint emission is best-effort */ }
321
+ }
322
+ return cached;
323
+ }
324
+ // Emit step.started checkpoint via NAPI if available
325
+ const seqNum = this._stepCounter;
326
+ const tsNs = Date.now() * 1000000;
327
+ if (this._nativeWorker?.emitCheckpoint) {
328
+ try {
329
+ await this._nativeWorker.emitCheckpoint(this.runId, 'workflow.step.started', JSON.stringify({ step_key: stepKey, step_name: stepName }), seqNum, {}, tsNs);
330
+ }
331
+ catch { /* checkpoint emission is best-effort */ }
332
+ }
333
+ // Execute the step
334
+ const startMs = Date.now();
335
+ const result = await fn();
336
+ const durationMs = Date.now() - startMs;
337
+ // Cache locally
338
+ this._stepCache.set(stepKey, result);
339
+ // Emit step.completed checkpoint
340
+ if (this._nativeWorker?.emitCheckpoint) {
341
+ try {
342
+ await this._nativeWorker.emitCheckpoint(this.runId, 'workflow.step.completed', JSON.stringify({
343
+ step_key: stepKey,
344
+ step_name: stepName,
345
+ output: result,
346
+ duration_ms: durationMs,
347
+ }), seqNum + 1, {}, Date.now() * 1000000);
348
+ }
349
+ catch { /* checkpoint emission is best-effort */ }
350
+ }
351
+ return result;
352
+ }
353
+ }
354
+ /**
355
+ * Worker class for running AGNT5 functions with platform integration
356
+ */
357
+ export class Worker {
358
+ constructor(serviceName, options = {}) {
359
+ this.isInitialized = false;
360
+ /** Registered agents (keyed by name) for dispatch */
361
+ this.agents = new Map();
362
+ /** Registered ChatBots (keyed by agent name) for webhook dispatch */
363
+ this.chatbots = new Map();
364
+ this.serviceName = serviceName;
365
+ this.options = {
366
+ ...options,
367
+ serviceName,
368
+ runtime: 'standalone',
369
+ };
370
+ }
371
+ /**
372
+ * Initialize the worker and load native bindings
373
+ */
374
+ async initialize() {
375
+ if (this.isInitialized)
376
+ return;
377
+ const native = loadNativeBindings();
378
+ // Initialize SDK
379
+ try {
380
+ native.initialize(this.serviceName, this.options.serviceVersion || '0.1.0');
381
+ console.log('✓ SDK initialized with telemetry');
382
+ }
383
+ catch (error) {
384
+ // Telemetry might already be initialized, that's okay
385
+ console.log('SDK initialization:', error.message);
386
+ }
387
+ // Create native worker
388
+ this.nativeWorker = new native.Worker({
389
+ serviceName: this.serviceName,
390
+ serviceVersion: this.options.serviceVersion,
391
+ serviceType: this.options.serviceType || 'function',
392
+ coordinatorEndpoint: this.options.coordinatorEndpoint ||
393
+ process.env.AGNT5_COORDINATOR_ENDPOINT ||
394
+ 'http://localhost:34186',
395
+ tenantId: this.options.tenantId ||
396
+ process.env.AGNT5_PROJECT_ID ||
397
+ process.env.AGNT5_TENANT_ID,
398
+ deploymentId: this.options.deploymentId ||
399
+ process.env.AGNT5_DEPLOYMENT_ID,
400
+ });
401
+ this.isInitialized = true;
402
+ }
403
+ /**
404
+ * Get worker ID
405
+ */
406
+ get workerId() {
407
+ if (!this.nativeWorker) {
408
+ throw new Error('Worker not initialized. Call run() first.');
409
+ }
410
+ return this.nativeWorker.workerId;
411
+ }
412
+ /**
413
+ * Get coordinator endpoint
414
+ */
415
+ get coordinatorEndpoint() {
416
+ if (!this.nativeWorker) {
417
+ throw new Error('Worker not initialized. Call run() first.');
418
+ }
419
+ return this.nativeWorker.coordinatorEndpoint;
420
+ }
421
+ /**
422
+ * Get tenant ID
423
+ */
424
+ get tenantId() {
425
+ if (!this.nativeWorker) {
426
+ throw new Error('Worker not initialized. Call run() first.');
427
+ }
428
+ return this.nativeWorker.tenantId;
429
+ }
430
+ /**
431
+ * Get deployment ID
432
+ */
433
+ get deploymentId() {
434
+ if (!this.nativeWorker) {
435
+ throw new Error('Worker not initialized. Call run() first.');
436
+ }
437
+ return this.nativeWorker.deploymentId;
438
+ }
439
+ /**
440
+ * Register agents that can be dispatched by the worker.
441
+ * Accepts both Agent and ChatBot instances. ChatBot instances
442
+ * will have their wrapped agent registered for normal dispatch,
443
+ * plus the ChatBot tracked for webhook routing.
444
+ */
445
+ registerAgents(agents) {
446
+ for (const item of agents) {
447
+ if (item instanceof ChatBot) {
448
+ this.chatbots.set(item.name, item);
449
+ // ChatBot wraps an agent — register the agent for the platform
450
+ // (The ChatBot.agent getter would need to be public for this)
451
+ // For now, just track by name — webhook dispatch uses ChatBot directly
452
+ console.log(`Registered ChatBot for agent '${item.name}'`);
453
+ }
454
+ else {
455
+ this.agents.set(item.name, item);
456
+ }
457
+ }
458
+ }
459
+ /**
460
+ * Handle incoming execution requests from the platform.
461
+ * This is a SYNC callback (returns void) — napi-rs ThreadsafeFunction cannot
462
+ * properly handle async (Promise) return values. Instead, async processing
463
+ * starts here and calls nativeWorker.resolveResponse() when done, which sends
464
+ * the result back through a Rust oneshot channel.
465
+ */
466
+ handleMessage(message) {
467
+ this.processMessage(message).then((responseJson) => {
468
+ this.nativeWorker.resolveResponse(message.invocationId, responseJson);
469
+ }, (error) => {
470
+ this.nativeWorker.resolveResponse(message.invocationId, JSON.stringify({
471
+ invocationId: message.invocationId,
472
+ error: error.message || 'Unknown error',
473
+ }));
474
+ });
475
+ }
476
+ /**
477
+ * Async message processing — dispatches to the appropriate component handler.
478
+ * Wraps each dispatch with lifecycle events matching the Python SDK executor pattern:
479
+ * run.started → component.started → handler → component.completed/failed → run.completed/failed
480
+ */
481
+ async processMessage(message) {
482
+ const runId = message.metadata?.run_id || message.invocationId;
483
+ return runWithContext({
484
+ runId,
485
+ sessionId: message.metadata?.session_id,
486
+ userId: message.metadata?.user_id,
487
+ correlationId: message.metadata?.correlation_id || message.invocationId,
488
+ tenantId: message.metadata?.tenant_id,
489
+ }, async () => {
490
+ // Create EventEmitter wired to NAPI worker for event emission
491
+ const emitter = new EventEmitter(runId, {
492
+ traceparent: message.metadata?.traceparent || '',
493
+ tracestate: message.metadata?.tracestate || '',
494
+ });
495
+ emitter.setWorker(this.nativeWorker);
496
+ // Correlation IDs: run CID from run_id[:8], component CID random
497
+ const runCid = runId.slice(0, 8);
498
+ const parentCid = message.metadata?.parent_correlation_id || null;
499
+ const attempt = parseInt(message.metadata?.attempt || '0', 10);
500
+ const maxAttempts = parseInt(message.metadata?.max_attempts || '1', 10);
501
+ try {
502
+ console.log(`📨 Received ${message.componentType} execution: ${message.componentName}`);
503
+ // Parse input data
504
+ const inputData = JSON.parse(message.inputJson);
505
+ // Create context with emitter
506
+ const ctx = new SimpleContext(message.invocationId, runId, attempt, this.serviceName);
507
+ ctx.setEmitter(emitter);
508
+ if (this.nativeWorker) {
509
+ ctx.setNativeWorker(this.nativeWorker);
510
+ }
511
+ // Seed HITL replay state from incoming resume metadata (no-op on
512
+ // fresh dispatches). Mirrors sdk-python's executors which read
513
+ // user_response/pause_index from request.metadata on resume.
514
+ ctx.loadReplayState(message.metadata);
515
+ // ── run.started ──
516
+ await emitter.emit(runStarted(runCid, parentCid, {
517
+ inputData,
518
+ attempt,
519
+ }));
520
+ let result;
521
+ const startTimeNs = BigInt(Date.now()) * 1000000n;
522
+ switch (message.componentType) {
523
+ case 'function': {
524
+ const fn = FunctionRegistry.get(message.componentName);
525
+ if (!fn) {
526
+ throw new Error(`Function not found: ${message.componentName}`);
527
+ }
528
+ const fnCid = generateCid();
529
+ // ── function.started ──
530
+ await emitter.emit(functionStarted(fnCid, runCid, {
531
+ inputData,
532
+ attempt,
533
+ }));
534
+ try {
535
+ result = await fn.handler(ctx, inputData);
536
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
537
+ // ── function.completed ──
538
+ await emitter.emit(functionCompleted(fnCid, runCid, {
539
+ outputData: result,
540
+ durationMs,
541
+ }));
542
+ }
543
+ catch (fnError) {
544
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
545
+ // ── function.failed ──
546
+ await emitter.emit(functionFailed(fnCid, runCid, {
547
+ errorCode: 'FUNCTION_ERROR',
548
+ errorMessage: fnError.message,
549
+ durationMs,
550
+ }));
551
+ throw fnError;
552
+ }
553
+ break;
554
+ }
555
+ case 'workflow': {
556
+ const wf = WorkflowRegistry.get(message.componentName);
557
+ if (!wf) {
558
+ throw new Error(`Workflow not found: ${message.componentName}`);
559
+ }
560
+ const wfCid = generateCid();
561
+ // Expose workflow + run correlation IDs so ctx.waitForUser can
562
+ // emit a properly-parented workflow.paused event.
563
+ ctx.setWorkflowCorrelation(wfCid, runCid);
564
+ // ── workflow.started ──
565
+ await emitter.emit(workflowStarted(wfCid, runCid, {
566
+ inputData,
567
+ attempt,
568
+ }));
569
+ try {
570
+ result = await wf.handler(ctx, inputData);
571
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
572
+ // ── workflow.completed ──
573
+ await emitter.emit(workflowCompleted(wfCid, runCid, {
574
+ outputData: result,
575
+ durationMs,
576
+ }));
577
+ }
578
+ catch (wfError) {
579
+ // HITL: waitForUser already emitted workflow.paused — don't
580
+ // emit workflow.failed. Let the outer catch short-circuit
581
+ // the run.failed emission and return a success response to
582
+ // the coordinator (the run is in the paused status already).
583
+ if (wfError instanceof WaitingForUserInputError) {
584
+ throw wfError;
585
+ }
586
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
587
+ // ── workflow.failed ──
588
+ await emitter.emit(workflowFailed(wfCid, runCid, {
589
+ errorCode: 'WORKFLOW_ERROR',
590
+ errorMessage: wfError.message,
591
+ durationMs,
592
+ }));
593
+ throw wfError;
594
+ }
595
+ break;
596
+ }
597
+ case 'agent': {
598
+ // Check if this is a chat webhook dispatch
599
+ if (inputData._chat_webhook && this.chatbots.has(message.componentName)) {
600
+ const chatbot = this.chatbots.get(message.componentName);
601
+ const platform = inputData.platform;
602
+ const headers = (inputData.headers || {});
603
+ const body = Buffer.from(inputData.body || '', 'utf-8');
604
+ console.log(`💬 Chat webhook received: platform=${platform}, bot=${message.componentName}`);
605
+ const challengeResult = await chatbot.handleWebhook(platform, headers, body);
606
+ result = challengeResult ?? {};
607
+ break;
608
+ }
609
+ const agent = this.agents.get(message.componentName);
610
+ if (!agent) {
611
+ throw new Error(`Agent not found: ${message.componentName}`);
612
+ }
613
+ // Session history: load prior conversation from entity storage
614
+ const sessionId = inputData.session_id || message.metadata?.session_id || runId;
615
+ const history = await this._loadSessionHistory(sessionId, message.componentName, message.metadata);
616
+ // Consume the agent stream so internal events (agent.started,
617
+ // iteration.started, tool_call.started, etc.) are forwarded to the platform.
618
+ const userMessage = inputData.prompt || inputData.message || JSON.stringify(inputData);
619
+ let agentResult;
620
+ for await (const event of agent.stream(userMessage, ctx, history)) {
621
+ if ('output' in event && 'toolCalls' in event && 'context' in event) {
622
+ agentResult = event;
623
+ }
624
+ else {
625
+ // Forward AgentEvent to platform via emitter
626
+ await emitter.emit(event);
627
+ }
628
+ }
629
+ if (!agentResult) {
630
+ throw new Error(`Agent '${message.componentName}' completed without producing a result`);
631
+ }
632
+ result = agentResult.output;
633
+ // Session history: save updated conversation to entity storage
634
+ const updatedMessages = [
635
+ ...history,
636
+ Message.user(userMessage),
637
+ Message.assistant(typeof result === 'string' ? result : JSON.stringify(result)),
638
+ ];
639
+ await this._saveSessionHistory(sessionId, message.componentName, updatedMessages, message.metadata);
640
+ // Emit session.created so GET /v1/sessions/{id} works
641
+ await emitter.emit({
642
+ name: 'session.created',
643
+ eventType: 'session.created',
644
+ eventId: generateCid(),
645
+ correlationId: generateCid(),
646
+ parentCorrelationId: null,
647
+ timestampNs: BigInt(Date.now()) * 1000000n,
648
+ metadata: {
649
+ session_id: sessionId,
650
+ component_name: message.componentName,
651
+ session_type: 'agent',
652
+ },
653
+ });
654
+ break;
655
+ }
656
+ case 'tool': {
657
+ const tool = ToolRegistry.get(message.componentName);
658
+ if (!tool) {
659
+ throw new Error(`Tool not found: ${message.componentName}`);
660
+ }
661
+ const toolCid = generateCid();
662
+ // ── tool.started ──
663
+ await emitter.emit(toolStarted(toolCid, runCid, {
664
+ inputData,
665
+ attempt,
666
+ }));
667
+ try {
668
+ result = await tool.invoke(ctx, inputData);
669
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
670
+ // ── tool.completed ──
671
+ await emitter.emit(toolCompleted(toolCid, runCid, {
672
+ outputData: result,
673
+ durationMs,
674
+ }));
675
+ }
676
+ catch (toolError) {
677
+ const durationMs = Number((BigInt(Date.now()) * 1000000n - startTimeNs) / 1000000n);
678
+ // ── tool.failed ──
679
+ await emitter.emit(toolFailed(toolCid, runCid, {
680
+ errorCode: 'TOOL_ERROR',
681
+ errorMessage: toolError.message,
682
+ durationMs,
683
+ }));
684
+ throw toolError;
685
+ }
686
+ break;
687
+ }
688
+ case 'scorer': {
689
+ const scorerConfig = ScorerRegistry.get(message.componentName);
690
+ if (!scorerConfig) {
691
+ throw new Error(`Scorer not found: ${message.componentName}`);
692
+ }
693
+ const scorerRequest = {
694
+ output: inputData.output,
695
+ expected: inputData.expected,
696
+ input: inputData.input,
697
+ config: inputData.config,
698
+ };
699
+ const scorerCtx = {
700
+ runId,
701
+ correlationId: runCid,
702
+ attempt,
703
+ log: (msg, extra) => {
704
+ console.log(`[SCORER] ${msg}`, extra || {});
705
+ },
706
+ };
707
+ const scorerResult = await scorerConfig.handler(scorerCtx, scorerRequest);
708
+ result = {
709
+ score: scorerResult.score,
710
+ passed: scorerResult.passed,
711
+ explanation: scorerResult.explanation,
712
+ label: scorerResult.label,
713
+ metadata: scorerResult.metadata,
714
+ };
715
+ break;
716
+ }
717
+ default:
718
+ throw new Error(`Unknown component type: ${message.componentType}`);
719
+ }
720
+ // ── run.completed ──
721
+ await emitter.emit(runCompleted(runCid, parentCid, {
722
+ outputData: result,
723
+ }));
724
+ return JSON.stringify({
725
+ invocationId: message.invocationId,
726
+ outputJson: JSON.stringify(result),
727
+ });
728
+ }
729
+ catch (error) {
730
+ // HITL: workflow.paused has already been journaled by waitForUser
731
+ // and the run is in the `paused` status. Return a success response
732
+ // (no `error` field) so the native layer emits a dispatch response
733
+ // with success=true — this lets the coordinator drain the lease
734
+ // and decrement worker load without writing any additional run
735
+ // lifecycle events. Do NOT emit run.completed: the run is paused,
736
+ // not completed.
737
+ if (error instanceof WaitingForUserInputError) {
738
+ return JSON.stringify({
739
+ invocationId: message.invocationId,
740
+ outputJson: JSON.stringify({
741
+ _paused: true,
742
+ question: error.question,
743
+ pauseIndex: error.pauseIndex,
744
+ }),
745
+ });
746
+ }
747
+ // ── run.failed ──
748
+ try {
749
+ await emitter.emit(runFailed(runCid, parentCid, {
750
+ errorCode: 'EXECUTION_ERROR',
751
+ errorMessage: error.message,
752
+ attempt,
753
+ maxAttempts,
754
+ }));
755
+ }
756
+ catch (emitError) {
757
+ console.error('Failed to emit run.failed event:', emitError);
758
+ }
759
+ console.error(`❌ Execution failed:`, error);
760
+ return JSON.stringify({
761
+ invocationId: message.invocationId,
762
+ error: error.message,
763
+ });
764
+ }
765
+ });
766
+ }
767
+ /**
768
+ * Load conversation history from entity storage (mirrors Python AgentContext).
769
+ */
770
+ async _loadSessionHistory(sessionId, agentName, metadata) {
771
+ const gatewayUrl = process.env.AGNT5_GATEWAY_URL || 'http://localhost:34181';
772
+ const entityKey = `agent_session_${sessionId}`;
773
+ const headers = { 'Content-Type': 'application/json' };
774
+ if (metadata?.tenant_id)
775
+ headers['X-Tenant-ID'] = metadata.tenant_id;
776
+ if (metadata?.deployment_id)
777
+ headers['X-Deployment-ID'] = metadata.deployment_id;
778
+ try {
779
+ const resp = await fetch(`${gatewayUrl}/v1/entity/AgentSession/${entityKey}/get`, {
780
+ method: 'POST',
781
+ headers,
782
+ body: JSON.stringify({ scope: 'session', scope_id: sessionId }),
783
+ });
784
+ if (resp.status === 404)
785
+ return [];
786
+ if (!resp.ok)
787
+ return [];
788
+ const data = (await resp.json());
789
+ const messages = data?.state?.messages || [];
790
+ return messages.map((m) => ({ role: m.role || 'user', content: m.content || '' }));
791
+ }
792
+ catch {
793
+ return [];
794
+ }
795
+ }
796
+ /**
797
+ * Save conversation history to entity storage (mirrors Python AgentContext).
798
+ */
799
+ async _saveSessionHistory(sessionId, agentName, messages, metadata) {
800
+ const gatewayUrl = process.env.AGNT5_GATEWAY_URL || 'http://localhost:34181';
801
+ const entityKey = `agent_session_${sessionId}`;
802
+ const headers = { 'Content-Type': 'application/json' };
803
+ if (metadata?.tenant_id)
804
+ headers['X-Tenant-ID'] = metadata.tenant_id;
805
+ if (metadata?.deployment_id)
806
+ headers['X-Deployment-ID'] = metadata.deployment_id;
807
+ const messagesData = messages.map((m) => ({
808
+ role: m.role,
809
+ content: m.content,
810
+ timestamp: Date.now() / 1000,
811
+ }));
812
+ try {
813
+ await fetch(`${gatewayUrl}/v1/entity/AgentSession/${entityKey}/set`, {
814
+ method: 'POST',
815
+ headers,
816
+ body: JSON.stringify({
817
+ input: {
818
+ agent_name: agentName,
819
+ session_id: sessionId,
820
+ messages: messagesData,
821
+ updated_at: Date.now() / 1000,
822
+ turn_count: messagesData.length,
823
+ },
824
+ scope: 'session',
825
+ scope_id: sessionId,
826
+ }),
827
+ });
828
+ }
829
+ catch {
830
+ // Best-effort — don't fail the dispatch on save errors
831
+ }
832
+ }
833
+ /**
834
+ * Start the worker and connect to platform
835
+ */
836
+ async run() {
837
+ await this.initialize();
838
+ console.log(`
839
+ 🚀 AGNT5 Worker Starting
840
+ Service: ${this.serviceName}
841
+ Worker ID: ${this.workerId}
842
+ Coordinator: ${this.coordinatorEndpoint}
843
+ Tenant: ${this.tenantId}
844
+ Deployment: ${this.deploymentId}
845
+ Runtime: ${getRuntime()}
846
+ `);
847
+ // Collect all registered components
848
+ const components = [];
849
+ // Functions
850
+ for (const [name, fnConfig] of FunctionRegistry.getAll()) {
851
+ const config = {};
852
+ if (fnConfig.options.retries?.maxAttempts !== undefined) {
853
+ config.max_attempts = String(fnConfig.options.retries.maxAttempts);
854
+ }
855
+ if (fnConfig.options.retries?.initialIntervalMs !== undefined) {
856
+ config.initial_interval_ms = String(fnConfig.options.retries.initialIntervalMs);
857
+ }
858
+ if (fnConfig.options.retries?.maxIntervalMs !== undefined) {
859
+ config.max_interval_ms = String(fnConfig.options.retries.maxIntervalMs);
860
+ }
861
+ if (fnConfig.options.backoff?.type) {
862
+ config.backoff_type = fnConfig.options.backoff.type;
863
+ }
864
+ if (fnConfig.options.backoff?.multiplier !== undefined) {
865
+ config.backoff_multiplier = String(fnConfig.options.backoff.multiplier);
866
+ }
867
+ if (fnConfig.options.timeout_ms !== undefined) {
868
+ config.timeout_ms = String(fnConfig.options.timeout_ms);
869
+ }
870
+ components.push({ name, componentType: 'function', config, metadata: {} });
871
+ }
872
+ // Workflows (auto-discover from registry)
873
+ for (const [name, cfg] of WorkflowRegistry.all()) {
874
+ const metadata = {};
875
+ if (cfg.cron)
876
+ metadata.cron = cfg.cron;
877
+ components.push({ name, componentType: 'workflow', config: {}, metadata });
878
+ }
879
+ // Tools (auto-discover from registry)
880
+ for (const [name] of ToolRegistry.all()) {
881
+ components.push({ name, componentType: 'tool', config: {}, metadata: {} });
882
+ }
883
+ // Agents (explicitly registered via registerAgents)
884
+ for (const [name] of this.agents) {
885
+ components.push({ name, componentType: 'agent', config: {}, metadata: {} });
886
+ }
887
+ // Scorers (custom only — built-ins are handled by Rust fast path)
888
+ for (const [name, cfg] of ScorerRegistry.all()) {
889
+ if (isScorer(cfg.handler)) {
890
+ components.push({ name, componentType: 'scorer', config: {}, metadata: {} });
891
+ }
892
+ }
893
+ const counts = {
894
+ function: components.filter(c => c.componentType === 'function').length,
895
+ workflow: components.filter(c => c.componentType === 'workflow').length,
896
+ tool: components.filter(c => c.componentType === 'tool').length,
897
+ agent: components.filter(c => c.componentType === 'agent').length,
898
+ scorer: components.filter(c => c.componentType === 'scorer').length,
899
+ };
900
+ const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([t, n]) => `${n} ${t}(s)`).join(', ');
901
+ console.log(`📦 Registered components: ${summary || 'none'}`);
902
+ await this.nativeWorker.setComponents(components);
903
+ // Set message handler
904
+ this.nativeWorker.setMessageHandler(this.handleMessage.bind(this));
905
+ console.log('✓ Message handler configured');
906
+ console.log('🔗 Connecting to platform...\n');
907
+ // Run the worker (this will block until shutdown)
908
+ await this.nativeWorker.run();
909
+ }
910
+ }
911
+ /**
912
+ * Detect current JavaScript runtime
913
+ */
914
+ export function getRuntime() {
915
+ // @ts-ignore
916
+ if (typeof process !== 'undefined' && process.versions?.node) {
917
+ // @ts-ignore
918
+ if (typeof Bun !== 'undefined')
919
+ return 'bun';
920
+ // @ts-ignore
921
+ if (typeof Deno !== 'undefined')
922
+ return 'deno';
923
+ return 'node';
924
+ }
925
+ // @ts-ignore
926
+ if (typeof EdgeRuntime !== 'undefined')
927
+ return 'edge';
928
+ return 'unknown';
929
+ }
930
+ /**
931
+ * Check platform connectivity
932
+ */
933
+ export async function checkPlatformConnectivity(coordinatorUrl) {
934
+ try {
935
+ const native = loadNativeBindings();
936
+ const url = coordinatorUrl || process.env.AGNT5_COORDINATOR_ENDPOINT || 'http://localhost:34186';
937
+ return await native.checkPlatformConnectivity(url);
938
+ }
939
+ catch (error) {
940
+ console.error('Platform connectivity check failed:', error);
941
+ return false;
942
+ }
943
+ }
944
+ //# sourceMappingURL=worker.js.map