elasticdash-test 0.1.17 → 0.1.18-alpha-2
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 +4400 -2665
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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 +246 -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/socket-connector.d.ts +23 -0
- package/dist/socket-connector.d.ts.map +1 -0
- package/dist/socket-connector.js +102 -0
- package/dist/socket-connector.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 +10 -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 +18 -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 +286 -0
- package/src/portal-executor.ts +335 -0
- package/src/portal-server.ts +290 -0
- package/src/socket-connector.ts +115 -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,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,19 @@ 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
|
+
|
|
83
|
+
// Socket connector (real-time backend communication)
|
|
84
|
+
export { connectToBackend, disconnectFromBackend } from './socket-connector.js'
|
|
85
|
+
export type { SocketConnectorOptions } from './socket-connector.js'
|
|
86
|
+
|
|
74
87
|
// LLM proxy
|
|
75
88
|
export { startLLMProxy, fetchCapturedTrace } from './proxy/llm-capture.js'
|
|
76
89
|
|
|
@@ -85,3 +98,8 @@ export type { RunWorkflowOptions, WorkflowRunResult } from './workflow-runner.js
|
|
|
85
98
|
// Agent mid-trace replay
|
|
86
99
|
export { serializeAgentState, deserializeAgentState, extractTaskOutputs, resolveTaskInput } from './core/agent-state.js'
|
|
87
100
|
export type { AgentTask, AgentPlan, AgentState, AgentTaskStatus, AgentPlanStatus } from './types/agent.js'
|
|
101
|
+
|
|
102
|
+
// Portal (remote rerun queue)
|
|
103
|
+
export { startPortalServer } from './portal-server.js'
|
|
104
|
+
export { executePortalTask } from './portal-executor.js'
|
|
105
|
+
export type { PortalTask, PortalTaskResult, PortalServerOptions, PortalServerHandle, PortalStatus } from './types/portal.js'
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getCurrentTrace } from '../trace-adapter/context.js'
|
|
2
2
|
import { getCaptureContext } from '../capture/recorder.js'
|
|
3
3
|
import { rawDateNow } from './side-effects.js'
|
|
4
|
+
import { getObservabilityContext, getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext } from './telemetry-push.js'
|
|
5
|
+
import type { WorkflowEvent } from '../capture/event.js'
|
|
4
6
|
|
|
5
7
|
type UsageInfo = { inputTokens?: number; outputTokens?: number; totalTokens?: number }
|
|
6
8
|
|
|
@@ -385,9 +387,12 @@ export function installAIInterceptor(): void {
|
|
|
385
387
|
|
|
386
388
|
const provider = detectProvider(url)
|
|
387
389
|
const traceAtCall = getCurrentTrace()
|
|
390
|
+
const obsCtx = getObservabilityContext()
|
|
388
391
|
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
const httpCtx = getHttpRunContext()
|
|
393
|
+
|
|
394
|
+
// No match or no active context: pass through unchanged
|
|
395
|
+
if (!provider || (!traceAtCall && !obsCtx && !httpCtx)) {
|
|
391
396
|
return originalFetch!(input, init)
|
|
392
397
|
}
|
|
393
398
|
|
|
@@ -414,7 +419,106 @@ export function installAIInterceptor(): void {
|
|
|
414
419
|
|
|
415
420
|
const ctx = getCaptureContext()
|
|
416
421
|
|
|
417
|
-
|
|
422
|
+
// Observability-only mode: no trace handle, no capture context — record via pushTelemetryEvent
|
|
423
|
+
if (!traceAtCall && !ctx && obsCtx) {
|
|
424
|
+
const id = obsCtx.nextId()
|
|
425
|
+
const start = rawDateNow()
|
|
426
|
+
const eventInput = { url, provider, model, prompt, messages }
|
|
427
|
+
|
|
428
|
+
const response = await originalFetch!(input, init)
|
|
429
|
+
|
|
430
|
+
if (isStreaming && response.body) {
|
|
431
|
+
const [streamForCaller, streamForRecorder] = response.body.tee()
|
|
432
|
+
bufferSSEStream(provider, streamForRecorder).then((completion) => {
|
|
433
|
+
const durationMs = rawDateNow() - start
|
|
434
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
|
|
435
|
+
}).catch(() => {
|
|
436
|
+
const durationMs = rawDateNow() - start
|
|
437
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
438
|
+
})
|
|
439
|
+
return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const cloned = response.clone()
|
|
444
|
+
const responseBody = await cloned.json() as Record<string, unknown>
|
|
445
|
+
const completion = extractCompletion(provider, responseBody)
|
|
446
|
+
const usage = extractUsage(provider, responseBody)
|
|
447
|
+
const durationMs = rawDateNow() - start
|
|
448
|
+
const event: WorkflowEvent = {
|
|
449
|
+
id, type: 'ai', name: model, input: eventInput, output: { completion },
|
|
450
|
+
timestamp: start, durationMs,
|
|
451
|
+
...(usage ? { usage } : {}),
|
|
452
|
+
}
|
|
453
|
+
pushTelemetryEvent(event)
|
|
454
|
+
} catch {
|
|
455
|
+
const durationMs = rawDateNow() - start
|
|
456
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return response
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// HTTP mode (no capture context): replay frozen AI events or execute live + push telemetry
|
|
463
|
+
if (!ctx && httpCtx) {
|
|
464
|
+
const id = httpCtx.nextId()
|
|
465
|
+
const eventInput = { url, provider, model, prompt, messages }
|
|
466
|
+
|
|
467
|
+
// Replay frozen step
|
|
468
|
+
const frozen = getHttpFrozenEvent(id)
|
|
469
|
+
if (frozen && frozen.type === 'ai') {
|
|
470
|
+
pushTelemetryEvent(frozen)
|
|
471
|
+
const frozenOutput = frozen.output as Record<string, unknown> | null
|
|
472
|
+
const completion = frozenOutput ? extractCompletion(provider, frozenOutput) : '(replayed)'
|
|
473
|
+
|
|
474
|
+
if (isStreaming) {
|
|
475
|
+
return new Response(synthesizeSSEStream(provider, completion), {
|
|
476
|
+
status: 200,
|
|
477
|
+
headers: { 'Content-Type': provider === 'gemini' ? 'application/json' : 'text/event-stream' },
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const body = frozenOutput?.streamed === true
|
|
482
|
+
? synthesizeCompletionJSON(provider, completion)
|
|
483
|
+
: (frozenOutput ?? synthesizeCompletionJSON(provider, completion))
|
|
484
|
+
return new Response(JSON.stringify(body), {
|
|
485
|
+
status: 200,
|
|
486
|
+
headers: { 'Content-Type': 'application/json' },
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Not frozen → execute live, push telemetry
|
|
491
|
+
const start = rawDateNow()
|
|
492
|
+
const response = await originalFetch!(input, init)
|
|
493
|
+
|
|
494
|
+
if (isStreaming && response.body) {
|
|
495
|
+
const [streamForCaller, streamForRecorder] = response.body.tee()
|
|
496
|
+
bufferSSEStream(provider, streamForRecorder).then((completion) => {
|
|
497
|
+
const durationMs = rawDateNow() - start
|
|
498
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
|
|
499
|
+
}).catch(() => {
|
|
500
|
+
const durationMs = rawDateNow() - start
|
|
501
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
|
|
502
|
+
})
|
|
503
|
+
return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const cloned = response.clone()
|
|
508
|
+
const responseBody = await cloned.json() as Record<string, unknown>
|
|
509
|
+
const completion = extractCompletion(provider, responseBody)
|
|
510
|
+
const usage = extractUsage(provider, responseBody)
|
|
511
|
+
const durationMs = rawDateNow() - start
|
|
512
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { completion }, timestamp: start, durationMs, ...(usage ? { usage } : {}) })
|
|
513
|
+
} catch {
|
|
514
|
+
const durationMs = rawDateNow() - start
|
|
515
|
+
pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return response
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (ctx && traceAtCall) {
|
|
418
522
|
const { recorder, replay } = ctx
|
|
419
523
|
const id = recorder.nextId()
|
|
420
524
|
const start = rawDateNow()
|
|
@@ -505,7 +609,9 @@ export function installAIInterceptor(): void {
|
|
|
505
609
|
return response
|
|
506
610
|
}
|
|
507
611
|
|
|
508
|
-
// No capture context — original behaviour (outside of a workflow run)
|
|
612
|
+
// No capture context — original behaviour (trace handle only, outside of a workflow run)
|
|
613
|
+
if (!traceAtCall) return originalFetch!(input, init)
|
|
614
|
+
|
|
509
615
|
const response = await originalFetch!(input, init)
|
|
510
616
|
|
|
511
617
|
if (isStreaming && response.body) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getCaptureContext } from '../capture/recorder.js'
|
|
2
|
+
import { getCurrentTrace } from '../trace-adapter/context.js'
|
|
3
|
+
import { rawDateNow } from './side-effects.js'
|
|
4
|
+
import { getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
|
|
2
5
|
|
|
3
6
|
type AnyFn = (...args: unknown[]) => unknown
|
|
4
7
|
|
|
@@ -10,6 +13,14 @@ interface MethodPatch {
|
|
|
10
13
|
|
|
11
14
|
const appliedPatches: MethodPatch[] = []
|
|
12
15
|
|
|
16
|
+
function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
|
|
17
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
18
|
+
return input as Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
if (input === undefined) return undefined
|
|
21
|
+
return { value: input }
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function wrapProtoMethod(proto: object, method: string, eventName: string): void {
|
|
14
25
|
const p = proto as Record<string, unknown>
|
|
15
26
|
if (typeof p[method] !== 'function') return
|
|
@@ -24,58 +35,143 @@ function wrapProtoMethod(proto: object, method: string, eventName: string): void
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
const ctx = getCaptureContext()
|
|
27
|
-
|
|
38
|
+
const httpCtx = getHttpRunContext()
|
|
39
|
+
const obsCtx = getObservabilityContext()
|
|
40
|
+
const input = args.length === 1 ? args[0] : args
|
|
41
|
+
|
|
42
|
+
if (!ctx && !httpCtx && !obsCtx) return original.apply(this, args)
|
|
43
|
+
|
|
44
|
+
// Observability-only mode: record and push, no mocks/replay
|
|
45
|
+
if (!ctx && !httpCtx && obsCtx) {
|
|
46
|
+
const id = obsCtx.nextId()
|
|
47
|
+
const start = rawDateNow()
|
|
48
|
+
|
|
49
|
+
let result: unknown
|
|
50
|
+
try {
|
|
51
|
+
result = original.apply(this, args)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
54
|
+
throw err
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
58
|
+
return (result as Promise<unknown>)
|
|
59
|
+
.then((output: unknown) => {
|
|
60
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
61
|
+
return output
|
|
62
|
+
})
|
|
63
|
+
.catch((err: unknown) => {
|
|
64
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
65
|
+
throw err
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
28
72
|
|
|
29
|
-
|
|
73
|
+
// HTTP mode (no capture context): replay frozen events or execute live
|
|
74
|
+
if (!ctx && httpCtx) {
|
|
75
|
+
const id = httpCtx.nextId()
|
|
76
|
+
|
|
77
|
+
// Replay frozen step
|
|
78
|
+
const frozen = getHttpFrozenEvent(id)
|
|
79
|
+
if (frozen && frozen.type === 'db') {
|
|
80
|
+
pushTelemetryEvent(frozen)
|
|
81
|
+
return Promise.resolve(frozen.output)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Not frozen → execute live, push telemetry
|
|
85
|
+
const start = rawDateNow()
|
|
86
|
+
|
|
87
|
+
let result: unknown
|
|
88
|
+
try {
|
|
89
|
+
result = original.apply(this, args)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
92
|
+
throw err
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
96
|
+
return (result as Promise<unknown>)
|
|
97
|
+
.then((output: unknown) => {
|
|
98
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
|
|
99
|
+
return output
|
|
100
|
+
})
|
|
101
|
+
.catch((err: unknown) => {
|
|
102
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
|
|
103
|
+
throw err
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Capture mode (enhanced with telemetry + TraceHandle)
|
|
112
|
+
const trace = getCurrentTrace()
|
|
113
|
+
const { recorder, replay } = ctx!
|
|
30
114
|
const id = recorder.nextId()
|
|
31
115
|
|
|
32
116
|
if (replay.shouldReplay(id)) {
|
|
33
117
|
const historicalEvent = replay.getRecordedEvent(id)
|
|
34
118
|
if (historicalEvent) recorder.record(historicalEvent)
|
|
35
|
-
|
|
119
|
+
if (httpCtx) pushTelemetryEvent(historicalEvent ?? { id, type: 'db', name: eventName, input, output: null, timestamp: rawDateNow(), durationMs: 0 })
|
|
120
|
+
const replayed = replay.getRecordedResult(id)
|
|
121
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
122
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: replayed, workflowEventId: id })
|
|
123
|
+
}
|
|
124
|
+
return Promise.resolve(replayed)
|
|
36
125
|
}
|
|
37
126
|
|
|
38
|
-
const start =
|
|
39
|
-
const input = args.length === 1 ? args[0] : args
|
|
127
|
+
const start = rawDateNow()
|
|
40
128
|
|
|
41
129
|
let result: unknown
|
|
42
130
|
try {
|
|
43
131
|
result = original.apply(this, args)
|
|
44
132
|
} catch (err) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
133
|
+
const durationMs = rawDateNow() - start
|
|
134
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
|
|
135
|
+
recorder.record(event)
|
|
136
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
137
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
138
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
|
|
139
|
+
}
|
|
50
140
|
throw err
|
|
51
141
|
}
|
|
52
142
|
|
|
53
143
|
if (result != null && typeof (result as Promise<unknown>).then === 'function') {
|
|
54
144
|
return (result as Promise<unknown>)
|
|
55
145
|
.then((output: unknown) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
146
|
+
const durationMs = rawDateNow() - start
|
|
147
|
+
const event = { id, type: 'db' as const, name: eventName, input, output, timestamp: start, durationMs }
|
|
148
|
+
recorder.record(event)
|
|
149
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
150
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
151
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: output, workflowEventId: id, durationMs })
|
|
152
|
+
}
|
|
61
153
|
return output
|
|
62
154
|
})
|
|
63
155
|
.catch((err: unknown) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
156
|
+
const durationMs = rawDateNow() - start
|
|
157
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
|
|
158
|
+
recorder.record(event)
|
|
159
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
160
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
161
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
|
|
162
|
+
}
|
|
69
163
|
throw err
|
|
70
164
|
})
|
|
71
165
|
}
|
|
72
166
|
|
|
73
167
|
// Sync return (rare for DB calls)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
168
|
+
const durationMs = rawDateNow() - start
|
|
169
|
+
const event = { id, type: 'db' as const, name: eventName, input, output: result, timestamp: start, durationMs }
|
|
170
|
+
recorder.record(event)
|
|
171
|
+
if (httpCtx) pushTelemetryEvent(event)
|
|
172
|
+
if (trace && typeof trace.recordToolCall === 'function') {
|
|
173
|
+
trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result, workflowEventId: id, durationMs })
|
|
174
|
+
}
|
|
79
175
|
return result
|
|
80
176
|
}
|
|
81
177
|
}
|