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.
- package/README.md +119 -18
- package/dist/cli.js +13 -2
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +76 -3
- package/dist/dashboard-server.js.map +1 -1
- package/dist/execution/tool-runner.d.ts.map +1 -1
- package/dist/execution/tool-runner.js +66 -5
- package/dist/execution/tool-runner.js.map +1 -1
- package/dist/index.cjs +66 -11
- package/dist/interceptors/db-auto.d.ts.map +1 -1
- package/dist/interceptors/db-auto.js +16 -8
- package/dist/interceptors/db-auto.js.map +1 -1
- package/dist/interceptors/telemetry-push.js +1 -1
- package/dist/tool-runner-worker.js +27 -2
- package/dist/tool-runner-worker.js.map +1 -1
- package/dist/trigger-executor.d.ts.map +1 -1
- package/dist/trigger-executor.js +3 -1
- package/dist/trigger-executor.js.map +1 -1
- package/dist/workflow-runner-worker.js +24 -0
- package/dist/workflow-runner-worker.js.map +1 -1
- package/docs/agent-coding-instructions.md +8 -5
- package/docs/agent-integration-guide.md +130 -10
- package/package.json +1 -1
- package/src/cli.ts +13 -2
- package/src/dashboard-server.ts +71 -3
- package/src/execution/tool-runner.ts +62 -5
- package/src/interceptors/db-auto.ts +20 -8
- package/src/interceptors/telemetry-push.ts +1 -1
- package/src/tool-runner-worker.ts +22 -2
- package/src/trigger-executor.ts +3 -1
- package/src/workflow-runner-worker.ts +23 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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()
|
package/src/trigger-executor.ts
CHANGED
|
@@ -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
|
}
|