elasticdash-sdk 0.2.5 → 0.2.7-beta

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.
@@ -13,6 +13,22 @@ interface MethodPatch {
13
13
 
14
14
  const appliedPatches: MethodPatch[] = []
15
15
 
16
+ /**
17
+ * Bundler-opaque dynamic import for optional peer dependencies. Webpack /
18
+ * turbopack / rollup statically analyze `await import('<literal>')` and try
19
+ * to resolve the target at build time, which fails for consumers that don't
20
+ * have the optional peer installed (e.g. a Next.js app that doesn't use
21
+ * Postgres still gets "Module not found: Can't resolve 'pg'" because this
22
+ * file is reachable from its bundle). Routing the import through
23
+ * `new Function` hides the call from static analysis so resolution happens
24
+ * at runtime in Node, where missing modules throw and the caller's outer
25
+ * Promise.allSettled swallows the rejection — the SDK's intended behavior.
26
+ */
27
+ const loadOptionalPeer = new Function(
28
+ 'specifier',
29
+ 'return import(specifier)',
30
+ ) as (specifier: string) => Promise<unknown>
31
+
16
32
  function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
17
33
  if (input && typeof input === 'object' && !Array.isArray(input)) {
18
34
  return input as Record<string, unknown>
@@ -177,8 +193,7 @@ function wrapProtoMethod(proto: object, method: string, eventName: string): void
177
193
  }
178
194
 
179
195
  async function tryPatchPg(): Promise<void> {
180
- // @ts-ignore optional peer dependency
181
- const pgMod = await import('pg') as Record<string, unknown>
196
+ const pgMod = (await loadOptionalPeer('pg')) as Record<string, unknown>
182
197
  const pg = (pgMod.default as Record<string, unknown> | undefined) ?? pgMod
183
198
  const Client = pg.Client as { prototype: object } | undefined
184
199
  // Patch Client.prototype only — Pool.query delegates to Client internally
@@ -188,8 +203,7 @@ async function tryPatchPg(): Promise<void> {
188
203
  }
189
204
 
190
205
  async function tryPatchMysql2(): Promise<void> {
191
- // @ts-ignore optional peer dependency
192
- const mod = await import('mysql2/promise') as Record<string, unknown>
206
+ const mod = (await loadOptionalPeer('mysql2/promise')) as Record<string, unknown>
193
207
  const mysql2 = (mod.default as Record<string, unknown> | undefined) ?? mod
194
208
  const Connection = mysql2.Connection as { prototype: object } | undefined
195
209
  if (Connection?.prototype) {
@@ -199,8 +213,7 @@ async function tryPatchMysql2(): Promise<void> {
199
213
  }
200
214
 
201
215
  async function tryPatchMongodb(): Promise<void> {
202
- // @ts-ignore optional peer dependency
203
- const mongMod = await import('mongodb') as Record<string, unknown>
216
+ const mongMod = (await loadOptionalPeer('mongodb')) as Record<string, unknown>
204
217
  const Collection = (
205
218
  mongMod.Collection ??
206
219
  (mongMod.default as Record<string, unknown> | undefined)?.Collection
@@ -213,8 +226,7 @@ async function tryPatchMongodb(): Promise<void> {
213
226
  }
214
227
 
215
228
  async function tryPatchIoredis(): Promise<void> {
216
- // @ts-ignore optional peer dependency
217
- const mod = await import('ioredis') as Record<string, unknown>
229
+ const mod = (await loadOptionalPeer('ioredis')) as Record<string, unknown>
218
230
  const Redis = (mod.default ?? mod) as { prototype: object } | undefined
219
231
  if (Redis?.prototype) {
220
232
  wrapProtoMethod(Redis.prototype, 'call', 'redis.call')
@@ -216,7 +216,7 @@ export function applyInboundMockConfig(req: unknown): void {
216
216
  if (!raw) return
217
217
 
218
218
  // Header format:
219
- // "gz1:<base64-gzip>" — current SDK (>=0.2.5) — gzipped JSON, used to
219
+ // "gz1:<base64-gzip>" — current SDK (>=0.2.6) — gzipped JSON, used to
220
220
  // stay under Node's default 8KB header limit when configs include
221
221
  // large trace-derived outputs.
222
222
  // "<base64-json>" — legacy plain base64 (back-compat with earlier SDKs).
@@ -23,6 +23,13 @@ import { pathToFileURL } from 'node:url'
23
23
 
24
24
  const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
25
25
 
26
+ const WORKER_START_MS = Date.now()
27
+ function stage(name: string, extra?: Record<string, unknown>): void {
28
+ if (process.env.ELASTICDASH_DEBUG !== '1') return
29
+ const tail = extra ? ' ' + Object.entries(extra).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' ') : ''
30
+ process.stderr.write(`[elasticdash-worker tool] stage=${name} pid=${process.pid} elapsedMs=${Date.now() - WORKER_START_MS}${tail}\n`)
31
+ }
32
+
26
33
  function writeResult(result: unknown): Promise<void> {
27
34
  return new Promise((resolve, reject) => {
28
35
  process.stdout.write(RESULT_PREFIX + JSON.stringify(result) + '\n', (err) =>
@@ -158,6 +165,7 @@ function installFrozenFetchFallback(frozenEvents: FrozenEvent[]): void {
158
165
  }
159
166
 
160
167
  async function main() {
168
+ stage('boot')
161
169
  const originalExit = process.exit.bind(process)
162
170
 
163
171
  // Prevent the SDK's tryAutoInitHttpContext from triggering full observability
@@ -175,6 +183,7 @@ async function main() {
175
183
  for await (const chunk of process.stdin) {
176
184
  raw += chunk
177
185
  }
186
+ stage('stdin-eof', { bytes: raw.length })
178
187
 
179
188
  let payload: { toolsModulePath: string; toolName: string; args: unknown[]; frozenEvents?: FrozenEvent[] }
180
189
  try {
@@ -184,6 +193,7 @@ async function main() {
184
193
  originalExit(1)
185
194
  return
186
195
  }
196
+ stage('payload-parsed')
187
197
 
188
198
  const { toolsModulePath, toolName, args, frozenEvents } = payload
189
199
 
@@ -193,12 +203,16 @@ async function main() {
193
203
  const hasFrozen = frozenEvents && frozenEvents.length > 0
194
204
  if (hasFrozen) {
195
205
  await setupFrozenContext(frozenEvents)
206
+ stage('frozen-context-ready', { count: frozenEvents.length })
207
+ } else {
208
+ stage('frozen-context-skipped')
196
209
  }
197
210
 
198
211
  try {
199
212
  let mod: any
200
213
  try {
201
214
  mod = await import(pathToFileURL(toolsModulePath).href)
215
+ stage('tool-module-imported')
202
216
  } catch (importErr) {
203
217
  const ie = importErr as Error
204
218
  await writeResult({ ok: false, error: `Failed to import tool module: ${ie.stack || ie.message}` })
@@ -210,31 +224,37 @@ async function main() {
210
224
  // as long as their containing module is reachable from toolsModulePath's
211
225
  // import graph. Falls back to ed_tools-style module export lookup.
212
226
  let fn: ((...a: unknown[]) => unknown) | undefined
227
+ let resolvedVia = 'none'
213
228
  try {
214
229
  const reg = await import('./tool-registry.js')
215
230
  const registered = reg.getRegisteredTool(toolName)
216
- if (registered) fn = registered.wrapped
231
+ if (registered) { fn = registered.wrapped; resolvedVia = 'registry' }
217
232
  } catch {
218
233
  // Registry module not available (older SDK build); fall through to export lookup.
219
234
  }
220
235
  if (!fn) {
221
236
  const exported = mod[toolName]
222
- if (typeof exported === 'function') fn = exported
237
+ if (typeof exported === 'function') { fn = exported; resolvedVia = 'module-export' }
223
238
  }
224
239
  if (typeof fn !== 'function') {
225
240
  await writeResult({ ok: false, error: `"${toolName}" not found via edTool() registry or as an exported function in the module.` })
226
241
  originalExit(1)
227
242
  return
228
243
  }
244
+ stage('tool-resolved', { tool: toolName, via: resolvedVia })
229
245
 
246
+ stage('tool-call-start', { tool: toolName })
230
247
  const currentOutput = await fn(...args)
248
+ stage('tool-call-end', { tool: toolName })
231
249
  await writeResult({ ok: true, currentOutput })
250
+ stage('result-written')
232
251
  originalExit(0)
233
252
  } catch (e) {
234
253
  const err = e as Error
235
254
  const errorMsg = err.stack || err.message || String(e)
236
255
  process.stderr.write(`[elasticdash-worker] Tool execution failed:\n${errorMsg}\n`)
237
256
  await writeResult({ ok: false, error: errorMsg })
257
+ stage('result-written', { ok: false })
238
258
  originalExit(1)
239
259
  } finally {
240
260
  if (hasFrozen) restoreFrozenFetch()
@@ -101,6 +101,8 @@ export async function executeTrigger(
101
101
  const runs: StepRunResult[] = []
102
102
 
103
103
  for (let i = 0; i < trigger.runCount; i++) {
104
+ const runStart = Date.now()
105
+ debugLog(`[elasticdash] Trigger ${trigger.triggerId} step=${stepIndex + 1}/${totalSteps} name=${step.eventName} run=${i + 1}/${trigger.runCount} phase=start`)
104
106
  const result = await executePortalTask(
105
107
  {
106
108
  taskId: `trigger-${trigger.triggerId}-${step.eventName}-${i}`,
@@ -130,7 +132,7 @@ export async function executeTrigger(
130
132
  usageTotalTokens: result.usage?.totalTokens,
131
133
  })
132
134
 
133
- debugLog(`[elasticdash] Trigger ${trigger.triggerId} step=${step.eventName} run=${i} ok=${result.ok}`)
135
+ debugLog(`[elasticdash] Trigger ${trigger.triggerId} step=${stepIndex + 1}/${totalSteps} name=${step.eventName} run=${i + 1}/${trigger.runCount} phase=done ok=${result.ok} elapsedMs=${Date.now() - runStart}`)
134
136
  }
135
137
 
136
138
  stepResult = {
@@ -43,6 +43,13 @@ async function readStdin(): Promise<string> {
43
43
 
44
44
  const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
45
45
 
46
+ const WORKER_START_MS = Date.now()
47
+ function stage(name: string, extra?: Record<string, unknown>): void {
48
+ if (process.env.ELASTICDASH_DEBUG !== '1') return
49
+ const tail = extra ? ' ' + Object.entries(extra).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' ') : ''
50
+ process.stderr.write(`[elasticdash-worker workflow] stage=${name} pid=${process.pid} elapsedMs=${Date.now() - WORKER_START_MS}${tail}\n`)
51
+ }
52
+
46
53
  const IS_DENO = typeof (globalThis as any).Deno !== 'undefined'
47
54
 
48
55
  /** Write the result JSON to fd3 pipe (Node.js) or a prefixed stdout line (Deno).
@@ -215,10 +222,12 @@ async function loadAndWrapTools(
215
222
  }
216
223
 
217
224
  async function main() {
225
+ stage('boot')
218
226
  // Keep a reference to the real process.exit so we can call it after flushing stdout.
219
227
  const originalExit = process.exit.bind(process)
220
228
 
221
229
  const raw = await readStdin()
230
+ stage('stdin-eof', { bytes: raw.length })
222
231
 
223
232
  let payload: {
224
233
  workflowsModulePath: string
@@ -249,6 +258,7 @@ async function main() {
249
258
  originalExit(1)
250
259
  return
251
260
  }
261
+ stage('payload-parsed')
252
262
 
253
263
  const { workflowsModulePath, toolsModulePath, workflowName, args, input, replayMode = false, checkpoint = 0, history = [], agentState, toolMockConfig, aiMockConfig, promptMockConfig, userPromptMockConfig, strict } = payload
254
264
 
@@ -273,6 +283,9 @@ async function main() {
273
283
  originalValues[name] = globals[name]
274
284
  globals[name] = fn
275
285
  }
286
+ stage('tools-wrapped', { count: Object.keys(wrappedTools).length })
287
+ } else {
288
+ stage('tools-wrapped-skipped')
276
289
  }
277
290
 
278
291
  // Intercept process.exit() so that workflows that call it internally (e.g. agent
@@ -305,20 +318,25 @@ async function main() {
305
318
  interceptFetch()
306
319
  interceptRandom()
307
320
  interceptDateNow()
321
+ stage('interceptors-installed')
308
322
 
309
323
  try {
310
324
  if (agentState) {
311
325
  // Agent mid-trace resumption path: load ed_agents and resume from saved state
312
326
  const agentsModulePath = workflowsModulePath.replace(/ed_workflows(\.[^.]+)?$/, 'ed_agents$1')
313
327
  const agentsMod = await import(pathToFileURL(agentsModulePath).href)
328
+ stage('agents-module-imported')
314
329
  if (typeof agentsMod.resumeAgentFromTrace !== 'function') {
315
330
  throw new Error(`"resumeAgentFromTrace" is not an exported function in ${agentsModulePath}`)
316
331
  }
332
+ stage('workflow-call-start', { mode: 'agent-resume' })
317
333
  currentOutput = await (agentsMod.resumeAgentFromTrace as (s: AgentState) => Promise<unknown>)(agentState)
334
+ stage('workflow-call-end', { mode: 'agent-resume' })
318
335
  console.error('[worker] resumeAgentFromTrace resolved, currentOutput:', currentOutput)
319
336
  } else {
320
337
  // Standard workflow path
321
338
  const workflowsMod = await import(pathToFileURL(workflowsModulePath).href)
339
+ stage('workflow-module-imported')
322
340
  const workflowFn = workflowsMod[workflowName]
323
341
  if (typeof workflowFn !== 'function') {
324
342
  ;(process as NodeJS.Process).exit = originalExit
@@ -328,7 +346,9 @@ async function main() {
328
346
  }
329
347
  // Standardize workflow argument resolution: always pass [input] if args is empty
330
348
  const callArgs = args.length ? args : [input]
349
+ stage('workflow-call-start', { workflow: workflowName })
331
350
  currentOutput = await (workflowFn as (...a: unknown[]) => unknown)(...callArgs)
351
+ stage('workflow-call-end', { workflow: workflowName })
332
352
  console.error('[worker] workflowFn resolved, currentOutput:', currentOutput) // stderr so it's visible
333
353
  }
334
354
  } finally {
@@ -369,6 +389,7 @@ async function main() {
369
389
  }
370
390
 
371
391
  await recorder.flush()
392
+ stage('recorder-flushed')
372
393
 
373
394
  const traceData = {
374
395
  steps: context.trace.getSteps(),
@@ -380,9 +401,11 @@ async function main() {
380
401
 
381
402
  if (workflowError) {
382
403
  await writeResult({ ok: false, error: workflowError.message ?? String(workflowError), ...traceData })
404
+ stage('result-written', { ok: false })
383
405
  originalExit(pendingExitCode ?? 1)
384
406
  } else {
385
407
  await writeResult({ ok: true, currentOutput, ...traceData })
408
+ stage('result-written', { ok: true })
386
409
  originalExit(pendingExitCode ?? 0)
387
410
  }
388
411
  }