elasticdash-test 0.1.17 → 0.1.18-alpha
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/capture/event.d.ts +5 -1
- package/dist/capture/event.d.ts.map +1 -1
- package/dist/cli.js +100 -0
- package/dist/cli.js.map +1 -1
- package/dist/evaluators/llm-judge.js +17 -14
- package/dist/evaluators/types.d.ts +1 -0
- package/dist/execution/tool-runner.d.ts +26 -0
- package/dist/execution/tool-runner.d.ts.map +1 -0
- package/dist/execution/tool-runner.js +270 -0
- package/dist/execution/tool-runner.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -0
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +4310 -2672
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
- package/dist/interceptors/ai-interceptor.js +97 -4
- package/dist/interceptors/ai-interceptor.js.map +1 -1
- package/dist/interceptors/db-auto.d.ts.map +1 -1
- package/dist/interceptors/db-auto.js +116 -24
- package/dist/interceptors/db-auto.js.map +1 -1
- package/dist/interceptors/db.d.ts +5 -0
- package/dist/interceptors/db.d.ts.map +1 -1
- package/dist/interceptors/db.js +93 -15
- package/dist/interceptors/db.js.map +1 -1
- package/dist/interceptors/http.d.ts.map +1 -1
- package/dist/interceptors/http.js +125 -93
- package/dist/interceptors/http.js.map +1 -1
- package/dist/interceptors/telemetry-push.d.ts +15 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -1
- package/dist/interceptors/telemetry-push.js +96 -13
- package/dist/interceptors/telemetry-push.js.map +1 -1
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +42 -5
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/interceptors/workflow-ai.d.ts.map +1 -1
- package/dist/interceptors/workflow-ai.js +46 -2
- package/dist/interceptors/workflow-ai.js.map +1 -1
- package/dist/observability.d.ts +69 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +242 -0
- package/dist/observability.js.map +1 -0
- package/dist/portal-executor.d.ts +30 -0
- package/dist/portal-executor.d.ts.map +1 -0
- package/dist/portal-executor.js +304 -0
- package/dist/portal-executor.js.map +1 -0
- package/dist/portal-server.d.ts +3 -0
- package/dist/portal-server.d.ts.map +1 -0
- package/dist/portal-server.js +265 -0
- package/dist/portal-server.js.map +1 -0
- package/dist/telemetry-batcher.d.ts +43 -0
- package/dist/telemetry-batcher.d.ts.map +1 -0
- package/dist/telemetry-batcher.js +111 -0
- package/dist/telemetry-batcher.js.map +1 -0
- package/dist/trigger-executor.d.ts +12 -0
- package/dist/trigger-executor.d.ts.map +1 -0
- package/dist/trigger-executor.js +83 -0
- package/dist/trigger-executor.js.map +1 -0
- package/dist/types/portal.d.ts +64 -0
- package/dist/types/portal.d.ts.map +1 -0
- package/dist/types/portal.js +2 -0
- package/dist/types/portal.js.map +1 -0
- package/dist/utils/debug.d.ts +3 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +8 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/redact.d.ts +7 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +26 -0
- package/dist/utils/redact.js.map +1 -0
- package/package.json +9 -1
- package/src/capture/event.ts +5 -1
- package/src/cli.ts +109 -0
- package/src/execution/tool-runner.ts +304 -0
- package/src/http.ts +2 -0
- package/src/index.ts +14 -0
- package/src/interceptors/ai-interceptor.ts +110 -4
- package/src/interceptors/db-auto.ts +121 -25
- package/src/interceptors/db.ts +92 -17
- package/src/interceptors/http.ts +145 -107
- package/src/interceptors/telemetry-push.ts +113 -13
- package/src/interceptors/tool.ts +42 -5
- package/src/interceptors/workflow-ai.ts +49 -2
- package/src/observability.ts +281 -0
- package/src/portal-executor.ts +335 -0
- package/src/portal-server.ts +290 -0
- package/src/telemetry-batcher.ts +143 -0
- package/src/trigger-executor.ts +121 -0
- package/src/types/portal.ts +67 -0
- package/src/utils/debug.ts +8 -0
- package/src/utils/redact.ts +25 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const DEBUG_KEY = 'ELASTICDASH_DEBUG';
|
|
2
|
+
/** Log only when ELASTICDASH_DEBUG=1 is set. Drop-in replacement for console.log in interceptors. */
|
|
3
|
+
export function debugLog(...args) {
|
|
4
|
+
if (typeof process !== 'undefined' && process.env?.[DEBUG_KEY] === '1') {
|
|
5
|
+
console.log(...args);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=debug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug.js","sourceRoot":"","sources":["../../src/utils/debug.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,mBAAmB,CAAA;AAErC,qGAAqG;AACrG,MAAM,UAAU,QAAQ,CAAC,GAAG,IAAe;IACzC,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-clones a value and replaces any object property whose key matches
|
|
3
|
+
* one of `keys` (case-insensitive) with "[REDACTED]".
|
|
4
|
+
* Returns the original value when `keys` is empty.
|
|
5
|
+
*/
|
|
6
|
+
export declare function redactPayload(value: unknown, keys: string[]): unknown;
|
|
7
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAIrE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-clones a value and replaces any object property whose key matches
|
|
3
|
+
* one of `keys` (case-insensitive) with "[REDACTED]".
|
|
4
|
+
* Returns the original value when `keys` is empty.
|
|
5
|
+
*/
|
|
6
|
+
export function redactPayload(value, keys) {
|
|
7
|
+
if (keys.length === 0)
|
|
8
|
+
return value;
|
|
9
|
+
const lowerKeys = new Set(keys.map((k) => k.toLowerCase()));
|
|
10
|
+
return redact(value, lowerKeys);
|
|
11
|
+
}
|
|
12
|
+
function redact(value, keys) {
|
|
13
|
+
if (value === null || value === undefined)
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value !== 'object')
|
|
16
|
+
return value;
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return value.map((item) => redact(item, keys));
|
|
19
|
+
}
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [k, v] of Object.entries(value)) {
|
|
22
|
+
out[k] = keys.has(k.toLowerCase()) ? '[REDACTED]' : redact(v, keys);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAc;IAC1D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AACjC,CAAC;AAED,SAAS,MAAM,CAAC,KAAc,EAAE,IAAiB;IAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE3C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;IAChD,CAAC;IAED,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elasticdash-test",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18-alpha",
|
|
4
4
|
"description": "AI-native test runner for ElasticDash workflow testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
"./http": {
|
|
18
18
|
"types": "./dist/http.d.ts",
|
|
19
19
|
"default": "./dist/http.js"
|
|
20
|
+
},
|
|
21
|
+
"./observability": {
|
|
22
|
+
"types": "./dist/observability.d.ts",
|
|
23
|
+
"default": "./dist/observability.js"
|
|
24
|
+
},
|
|
25
|
+
"./portal": {
|
|
26
|
+
"types": "./dist/portal-server.d.ts",
|
|
27
|
+
"default": "./dist/portal-server.js"
|
|
20
28
|
}
|
|
21
29
|
},
|
|
22
30
|
"files": [
|
package/src/capture/event.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect'
|
|
1
|
+
export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect' | 'workflow'
|
|
2
2
|
|
|
3
3
|
export interface WorkflowEvent {
|
|
4
4
|
id: number
|
|
@@ -18,6 +18,10 @@ export interface WorkflowEvent {
|
|
|
18
18
|
streamed?: boolean
|
|
19
19
|
/** Raw buffered text of a streamed response (used for replay) */
|
|
20
20
|
streamRaw?: string
|
|
21
|
+
/** Schema version for forward compatibility (default 1) */
|
|
22
|
+
schemaVersion?: number
|
|
23
|
+
/** Optional request-level trace ID for grouping events within a session */
|
|
24
|
+
traceId?: string
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface WorkflowTrace {
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { runFiles } from './runner.js'
|
|
|
12
12
|
import { reportResults } from './reporter.js'
|
|
13
13
|
import { startBrowserUiServer, type UiEvent } from './browser-ui.js'
|
|
14
14
|
import { startDashboardServer } from './dashboard-server.js'
|
|
15
|
+
import { initObservability, shutdownObservability } from './observability.js'
|
|
16
|
+
import { startPortalServer } from './portal-server.js'
|
|
15
17
|
|
|
16
18
|
function stripAnsi(input?: string): string | undefined {
|
|
17
19
|
if (!input) return input
|
|
@@ -306,6 +308,113 @@ async function bootstrap(): Promise<void> {
|
|
|
306
308
|
process.once('SIGTERM', cleanup)
|
|
307
309
|
})
|
|
308
310
|
|
|
311
|
+
// elasticdash observe
|
|
312
|
+
program
|
|
313
|
+
.command('observe')
|
|
314
|
+
.description('Start observability mode — stream trace events to ElasticDash backend')
|
|
315
|
+
.option('--server <url>', 'ElasticDash backend API URL', process.env.ELASTICDASH_API_URL)
|
|
316
|
+
.option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
|
|
317
|
+
.option('--service-id <id>', 'Service identifier', process.env.ELASTICDASH_SERVICE_ID)
|
|
318
|
+
.action(async (options: { server?: string; apiKey?: string; serviceId?: string }) => {
|
|
319
|
+
const serverUrl = options.server
|
|
320
|
+
if (!serverUrl) {
|
|
321
|
+
console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
|
|
322
|
+
process.exit(1)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const handle = initObservability({
|
|
326
|
+
serverUrl,
|
|
327
|
+
apiKey: options.apiKey,
|
|
328
|
+
serviceId: options.serviceId,
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
console.log(`[elasticdash] Observability active`)
|
|
332
|
+
console.log(` Session ID : ${handle.sessionId}`)
|
|
333
|
+
console.log(` Server : ${serverUrl}`)
|
|
334
|
+
console.log(` Service : ${options.serviceId ?? process.env.ELASTICDASH_SERVICE_ID ?? 'unknown-service'}`)
|
|
335
|
+
console.log(`[elasticdash] Press Ctrl+C to stop`)
|
|
336
|
+
|
|
337
|
+
let isShuttingDown = false
|
|
338
|
+
const cleanup = async () => {
|
|
339
|
+
if (isShuttingDown) {
|
|
340
|
+
process.exit(1)
|
|
341
|
+
}
|
|
342
|
+
isShuttingDown = true
|
|
343
|
+
console.log('\n[elasticdash] Shutting down observability...')
|
|
344
|
+
await shutdownObservability()
|
|
345
|
+
process.exit(0)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
process.once('SIGINT', cleanup)
|
|
349
|
+
process.once('SIGTERM', cleanup)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// elasticdash portal
|
|
353
|
+
program
|
|
354
|
+
.command('portal')
|
|
355
|
+
.description('Start a portal server to receive and execute rerun tasks from ElasticDash backend')
|
|
356
|
+
.option('--server <url>', 'ElasticDash backend API URL to POST results to', process.env.ELASTICDASH_API_URL)
|
|
357
|
+
.option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
|
|
358
|
+
.option('--port <port>', 'Portal server port', (v) => Number(v), process.env.ELASTICDASH_PORTAL_PORT ? Number(process.env.ELASTICDASH_PORTAL_PORT) : 4574)
|
|
359
|
+
.option('--allowed-origins <origins>', 'Comma-separated list of additional allowed origin domains', process.env.ELASTICDASH_ALLOWED_ORIGINS)
|
|
360
|
+
.action(async (options: { server?: string; apiKey?: string; port: number; allowedOrigins?: string }) => {
|
|
361
|
+
const backendUrl = options.server
|
|
362
|
+
if (!backendUrl) {
|
|
363
|
+
console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
|
|
364
|
+
process.exit(1)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const allowedOrigins = options.allowedOrigins
|
|
368
|
+
? options.allowedOrigins.split(',').map(s => s.trim()).filter(Boolean)
|
|
369
|
+
: undefined
|
|
370
|
+
|
|
371
|
+
const handle = await startPortalServer({
|
|
372
|
+
port: options.port,
|
|
373
|
+
backendUrl,
|
|
374
|
+
apiKey: options.apiKey,
|
|
375
|
+
cwd,
|
|
376
|
+
allowedOrigins,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
console.log(`[elasticdash] Portal server running`)
|
|
380
|
+
console.log(` URL : ${handle.url}`)
|
|
381
|
+
console.log(` Backend : ${backendUrl}`)
|
|
382
|
+
console.log(` Port : ${handle.port}`)
|
|
383
|
+
console.log(`[elasticdash] Waiting for tasks from backend... Press Ctrl+C to stop`)
|
|
384
|
+
|
|
385
|
+
// Register with backend
|
|
386
|
+
try {
|
|
387
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
388
|
+
if (options.apiKey) headers['Authorization'] = `Bearer ${options.apiKey}`
|
|
389
|
+
const res = await fetch(`${backendUrl}/api/portal/register`, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers,
|
|
392
|
+
body: JSON.stringify({ portalUrl: handle.url }),
|
|
393
|
+
})
|
|
394
|
+
if (res.ok) {
|
|
395
|
+
console.log(`[elasticdash] Registered with backend`)
|
|
396
|
+
} else {
|
|
397
|
+
console.warn(`[elasticdash] Backend registration returned ${res.status} — portal will still accept tasks directly`)
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
console.warn(`[elasticdash] Could not register with backend — portal will still accept tasks directly`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let isShuttingDown = false
|
|
404
|
+
const cleanup = async () => {
|
|
405
|
+
if (isShuttingDown) {
|
|
406
|
+
process.exit(1)
|
|
407
|
+
}
|
|
408
|
+
isShuttingDown = true
|
|
409
|
+
console.log('\n[elasticdash] Shutting down portal...')
|
|
410
|
+
await handle.close()
|
|
411
|
+
process.exit(0)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
process.once('SIGINT', cleanup)
|
|
415
|
+
process.once('SIGTERM', cleanup)
|
|
416
|
+
})
|
|
417
|
+
|
|
309
418
|
await program.parseAsync(process.argv)
|
|
310
419
|
}
|
|
311
420
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool/AI execution helpers for portal and dashboard.
|
|
3
|
+
* These are extracted versions of the helpers in dashboard-server.ts
|
|
4
|
+
* to avoid importing the full dashboard server module.
|
|
5
|
+
*/
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { spawn } from 'node:child_process'
|
|
9
|
+
import { pathToFileURL } from 'node:url'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface ToolInfo {
|
|
16
|
+
name: string
|
|
17
|
+
isAsync: boolean
|
|
18
|
+
signature: string
|
|
19
|
+
filePath: string
|
|
20
|
+
lineNumber?: number
|
|
21
|
+
sourceCode?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RerunResult {
|
|
25
|
+
ok: boolean
|
|
26
|
+
currentOutput?: unknown
|
|
27
|
+
currentDurationMs?: number
|
|
28
|
+
currentUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }
|
|
29
|
+
error?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ParsedExport {
|
|
33
|
+
name: string
|
|
34
|
+
isAsync: boolean
|
|
35
|
+
signature: string
|
|
36
|
+
filePath: string
|
|
37
|
+
lineNumber?: number
|
|
38
|
+
sourceCode?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Runtime helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export function isDenoProject(dir: string): boolean {
|
|
46
|
+
return existsSync(path.join(dir, 'deno.json')) || existsSync(path.join(dir, 'deno.jsonc'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveRuntimeModule(cwd: string, baseName: string): string | null {
|
|
50
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
51
|
+
const candidate = path.join(cwd, `${baseName}${ext}`)
|
|
52
|
+
if (existsSync(candidate)) return candidate
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseSignatureParams(signature?: string): string[] {
|
|
58
|
+
if (!signature) return []
|
|
59
|
+
const trimmed = signature.trim()
|
|
60
|
+
if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return []
|
|
61
|
+
const body = trimmed.slice(1, -1).trim()
|
|
62
|
+
if (!body) return []
|
|
63
|
+
return body
|
|
64
|
+
.split(',')
|
|
65
|
+
.map(part => part.trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.map(part => part.replace(/^\.\.\./, '').split('=')[0].split(':')[0].replace(/\?/g, '').trim())
|
|
68
|
+
.filter(part => /^[$A-Z_][0-9A-Z_$]*$/i.test(part))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Argument building
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export function buildToolArgs(input: unknown, tool?: ToolInfo): unknown[] {
|
|
76
|
+
if (input === undefined) return []
|
|
77
|
+
if (Array.isArray(input)) return input
|
|
78
|
+
if (input && typeof input === 'object') {
|
|
79
|
+
const argObject = input as Record<string, unknown>
|
|
80
|
+
const paramNames = parseSignatureParams(tool?.signature)
|
|
81
|
+
if (paramNames.length > 0 && paramNames.every(name => Object.prototype.hasOwnProperty.call(argObject, name))) {
|
|
82
|
+
return paramNames.map(name => argObject[name])
|
|
83
|
+
}
|
|
84
|
+
return [input]
|
|
85
|
+
}
|
|
86
|
+
return [input]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Subprocess execution
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export function runToolInSubprocess(
|
|
94
|
+
toolsModulePath: string,
|
|
95
|
+
toolName: string,
|
|
96
|
+
args: unknown[],
|
|
97
|
+
): Promise<RerunResult> {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const startMs = Date.now()
|
|
100
|
+
const workerScript = new URL('../tool-runner-worker.js', import.meta.url).pathname
|
|
101
|
+
const projectDir = path.dirname(toolsModulePath)
|
|
102
|
+
const denoProject = isDenoProject(projectDir)
|
|
103
|
+
|
|
104
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? ''
|
|
105
|
+
const tsxFlag = '--import tsx'
|
|
106
|
+
const childNodeOptions = nodeOptions.includes('tsx') ? nodeOptions : `${nodeOptions} ${tsxFlag}`.trim()
|
|
107
|
+
const childEnv = { ...process.env, NODE_OPTIONS: denoProject ? nodeOptions : childNodeOptions }
|
|
108
|
+
|
|
109
|
+
const runtime = denoProject ? 'deno' : process.execPath
|
|
110
|
+
const runtimeArgs = denoProject ? ['run', '--allow-all', workerScript] : [workerScript]
|
|
111
|
+
|
|
112
|
+
const child = spawn(runtime, runtimeArgs, {
|
|
113
|
+
env: childEnv,
|
|
114
|
+
cwd: projectDir,
|
|
115
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
|
|
119
|
+
let resultLine = ''
|
|
120
|
+
let stderr = ''
|
|
121
|
+
|
|
122
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
123
|
+
const text = chunk.toString()
|
|
124
|
+
for (const line of text.split('\n')) {
|
|
125
|
+
if (line.startsWith(RESULT_PREFIX)) {
|
|
126
|
+
resultLine = line.slice(RESULT_PREFIX.length)
|
|
127
|
+
} else if (line) {
|
|
128
|
+
process.stdout.write(line + '\n')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
133
|
+
stderr += chunk.toString()
|
|
134
|
+
process.stderr.write(chunk)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
child.on('close', () => {
|
|
138
|
+
const currentDurationMs = Date.now() - startMs
|
|
139
|
+
if (resultLine) {
|
|
140
|
+
try {
|
|
141
|
+
resolve({ ...JSON.parse(resultLine), currentDurationMs })
|
|
142
|
+
return
|
|
143
|
+
} catch { /* fall through */ }
|
|
144
|
+
}
|
|
145
|
+
resolve({ ok: false, error: stderr.trim() || 'Tool subprocess produced no output.', currentDurationMs })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
child.on('error', (err) => {
|
|
149
|
+
const hint = denoProject && (err as NodeJS.ErrnoException).code === 'ENOENT'
|
|
150
|
+
? ' (Deno project detected — ensure "deno" is installed and available in PATH)'
|
|
151
|
+
: ''
|
|
152
|
+
resolve({ ok: false, error: `Failed to spawn tool subprocess: ${err.message}${hint}`, currentDurationMs: Date.now() - startMs })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const payload = JSON.stringify({
|
|
156
|
+
toolsModulePath: pathToFileURL(toolsModulePath).pathname,
|
|
157
|
+
toolName,
|
|
158
|
+
args,
|
|
159
|
+
})
|
|
160
|
+
child.stdin.write(payload)
|
|
161
|
+
child.stdin.end()
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tool scanning (static analysis of ed_tools.ts/js)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function resolveModulePath(fromDir: string, specifier: string): string | null {
|
|
170
|
+
if (!specifier.startsWith('.')) return null
|
|
171
|
+
const exts = ['.ts', '.tsx', '.js', '.jsx', '']
|
|
172
|
+
for (const ext of exts) {
|
|
173
|
+
const candidate = path.resolve(fromDir, specifier + ext)
|
|
174
|
+
if (existsSync(candidate)) return candidate
|
|
175
|
+
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function lineAt(src: string, index: number): number {
|
|
180
|
+
return src.slice(0, index).split('\n').length
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractSource(src: string, index: number): string {
|
|
184
|
+
const snippet = src.slice(index, index + 2000)
|
|
185
|
+
return snippet.length < 2000 ? snippet : snippet + '\n// (truncated)'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function findFunctionInSource(src: string, name: string): { isAsync: boolean; signature: string; lineNumber?: number; sourceCode?: string } {
|
|
189
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
190
|
+
let m = src.match(new RegExp(`export\\s+(async\\s+)?function\\s+${escaped}\\s*(\\([^)]*\\))`))
|
|
191
|
+
if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!) }
|
|
192
|
+
m = src.match(new RegExp(`(?:^|\\n)\\s*(?:async\\s+)?function\\s+${escaped}\\s*(\\([^)]*\\))`, 'm'))
|
|
193
|
+
if (m) return {
|
|
194
|
+
isAsync: new RegExp(`async\\s+function\\s+${escaped}`).test(src),
|
|
195
|
+
signature: m[1],
|
|
196
|
+
lineNumber: lineAt(src, m.index!),
|
|
197
|
+
sourceCode: extractSource(src, m.index!),
|
|
198
|
+
}
|
|
199
|
+
m = src.match(new RegExp(`export\\s+const\\s+${escaped}\\s*=\\s*(async\\s*)?(\\([^)]*\\))\\s*=>`))
|
|
200
|
+
if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!) }
|
|
201
|
+
m = src.match(new RegExp(`(?:^|\\n)\\s*const\\s+${escaped}\\s*=\\s*(async\\s*)?(\\([^)]*\\))\\s*=>`, 'm'))
|
|
202
|
+
if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!) }
|
|
203
|
+
return { isAsync: false, signature: '()' }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractExportsFromSource(filePath: string): ParsedExport[] {
|
|
207
|
+
let src: string
|
|
208
|
+
try {
|
|
209
|
+
src = readFileSync(filePath, 'utf8')
|
|
210
|
+
} catch {
|
|
211
|
+
return []
|
|
212
|
+
}
|
|
213
|
+
const dir = path.dirname(filePath)
|
|
214
|
+
const results: ParsedExport[] = []
|
|
215
|
+
|
|
216
|
+
// 1. Direct: export [async] function name(params) { … }
|
|
217
|
+
for (const m of src.matchAll(/export\s+(async\s+)?function\s+(\w+)\s*(\([^)]*\))/g)) {
|
|
218
|
+
results.push({
|
|
219
|
+
name: m[2], isAsync: !!m[1], signature: m[3], filePath,
|
|
220
|
+
lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!),
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 2. Direct: export const name = [async] (params) => …
|
|
225
|
+
for (const m of src.matchAll(/export\s+const\s+(\w+)\s*=\s*(async\s*)?\(([^)]*)\)\s*=>/g)) {
|
|
226
|
+
results.push({
|
|
227
|
+
name: m[1], isAsync: !!m[2], signature: `(${m[3]})`, filePath,
|
|
228
|
+
lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!),
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 3. Named re-exports: export { X [as Y], … } from './module'
|
|
233
|
+
for (const m of src.matchAll(/export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
234
|
+
const modulePath = resolveModulePath(dir, m[2])
|
|
235
|
+
let moduleSrc = ''
|
|
236
|
+
try { if (modulePath) moduleSrc = readFileSync(modulePath, 'utf8') } catch { /* ignore */ }
|
|
237
|
+
for (const spec of m[1].split(',')) {
|
|
238
|
+
const parts = spec.trim().split(/\s+as\s+/)
|
|
239
|
+
const originalName = parts[0].trim()
|
|
240
|
+
const exportedName = (parts[1] ?? parts[0]).trim()
|
|
241
|
+
if (!exportedName || exportedName === 'default') continue
|
|
242
|
+
const info = moduleSrc ? findFunctionInSource(moduleSrc, originalName) : { isAsync: false, signature: '()' }
|
|
243
|
+
results.push({
|
|
244
|
+
name: exportedName, isAsync: info.isAsync, signature: info.signature,
|
|
245
|
+
filePath: modulePath ?? filePath, lineNumber: info.lineNumber, sourceCode: info.sourceCode,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 4. Import + destructure: import { obj } from './m' + export const { a, b } = obj
|
|
251
|
+
for (const imp of src.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
252
|
+
const importedNames = imp[1].split(',').map(s => {
|
|
253
|
+
const parts = s.trim().split(/\s+as\s+/)
|
|
254
|
+
return { original: parts[0].trim(), local: (parts[1] ?? parts[0]).trim() }
|
|
255
|
+
}).filter(n => n.local)
|
|
256
|
+
const modulePath = resolveModulePath(dir, imp[2])
|
|
257
|
+
for (const { local } of importedNames) {
|
|
258
|
+
const destructureRe = new RegExp(`export\\s+const\\s+\\{([^}]+)\\}\\s*=\\s*${local.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
|
|
259
|
+
const dm = src.match(destructureRe)
|
|
260
|
+
if (!dm) continue
|
|
261
|
+
let moduleSrc = ''
|
|
262
|
+
try { if (modulePath) moduleSrc = readFileSync(modulePath, 'utf8') } catch { /* ignore */ }
|
|
263
|
+
for (const member of dm[1].split(',')) {
|
|
264
|
+
const name = member.trim()
|
|
265
|
+
if (!name) continue
|
|
266
|
+
const info = moduleSrc ? findFunctionInSource(moduleSrc, name) : { isAsync: false, signature: '()' }
|
|
267
|
+
results.push({
|
|
268
|
+
name, isAsync: info.isAsync, signature: info.signature,
|
|
269
|
+
filePath: modulePath ?? filePath, lineNumber: info.lineNumber, sourceCode: info.sourceCode,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return results
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function scanTools(cwd: string): ToolInfo[] {
|
|
279
|
+
for (const candidate of [path.join(cwd, 'ed_tools.ts'), path.join(cwd, 'ed_tools.js')]) {
|
|
280
|
+
if (!existsSync(candidate)) continue
|
|
281
|
+
const exports = extractExportsFromSource(candidate)
|
|
282
|
+
if (exports.length > 0) {
|
|
283
|
+
return exports.map(e => ({
|
|
284
|
+
name: e.name, isAsync: e.isAsync, signature: e.signature,
|
|
285
|
+
filePath: e.filePath, lineNumber: e.lineNumber, sourceCode: e.sourceCode,
|
|
286
|
+
}))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return []
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function scanWorkflows(cwd: string): ToolInfo[] {
|
|
293
|
+
for (const candidate of [path.join(cwd, 'ed_workflows.ts'), path.join(cwd, 'ed_workflows.js')]) {
|
|
294
|
+
if (!existsSync(candidate)) continue
|
|
295
|
+
const exports = extractExportsFromSource(candidate)
|
|
296
|
+
if (exports.length > 0) {
|
|
297
|
+
return exports.map(e => ({
|
|
298
|
+
name: e.name, isAsync: e.isAsync, signature: e.signature,
|
|
299
|
+
filePath: e.filePath, lineNumber: e.lineNumber, sourceCode: e.sourceCode,
|
|
300
|
+
}))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return []
|
|
304
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
export { setHttpRunContext, initHttpRunContext, getHttpRunContext } from './interceptors/telemetry-push.js'
|
|
7
7
|
export { wrapTool } from './interceptors/tool.js'
|
|
8
8
|
export { wrapAI } from './interceptors/workflow-ai.js'
|
|
9
|
+
export { wrapDB, wrapPgClient, wrapKnex, wrapMongoCollection, wrapRedisClient } from './interceptors/db.js'
|
|
10
|
+
export { initObservability, shutdownObservability, startTrace, wrapWorkflow } from './observability.js'
|
package/src/index.ts
CHANGED
|
@@ -71,6 +71,15 @@ export { interceptRandom, restoreRandom, interceptDateNow, restoreDateNow } from
|
|
|
71
71
|
// AI interceptor (monkey-patch based)
|
|
72
72
|
export { installAIInterceptor, uninstallAIInterceptor } from './interceptors/ai-interceptor.js'
|
|
73
73
|
|
|
74
|
+
// Observability
|
|
75
|
+
export { initObservability, shutdownObservability, startTrace, wrapWorkflow } from './observability.js'
|
|
76
|
+
export type { ObservabilityOptions, ObservabilityHandle } from './observability.js'
|
|
77
|
+
export { TelemetryBatcher } from './telemetry-batcher.js'
|
|
78
|
+
export type { TelemetryBatcherOptions, TriggerSignal, TriggerStep } from './telemetry-batcher.js'
|
|
79
|
+
export type { ObservabilityContext } from './interceptors/telemetry-push.js'
|
|
80
|
+
export { checkToolAvailability, checkAIAvailability } from './portal-executor.js'
|
|
81
|
+
export type { AvailabilityResult } from './portal-executor.js'
|
|
82
|
+
|
|
74
83
|
// LLM proxy
|
|
75
84
|
export { startLLMProxy, fetchCapturedTrace } from './proxy/llm-capture.js'
|
|
76
85
|
|
|
@@ -85,3 +94,8 @@ export type { RunWorkflowOptions, WorkflowRunResult } from './workflow-runner.js
|
|
|
85
94
|
// Agent mid-trace replay
|
|
86
95
|
export { serializeAgentState, deserializeAgentState, extractTaskOutputs, resolveTaskInput } from './core/agent-state.js'
|
|
87
96
|
export type { AgentTask, AgentPlan, AgentState, AgentTaskStatus, AgentPlanStatus } from './types/agent.js'
|
|
97
|
+
|
|
98
|
+
// Portal (remote rerun queue)
|
|
99
|
+
export { startPortalServer } from './portal-server.js'
|
|
100
|
+
export { executePortalTask } from './portal-executor.js'
|
|
101
|
+
export type { PortalTask, PortalTaskResult, PortalServerOptions, PortalServerHandle, PortalStatus } from './types/portal.js'
|