elasticdash-test 0.1.24 → 0.1.25-alpha-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,554 @@
1
+ # ElasticDash SDK — Agent Integration Guide
2
+
3
+ > **Audience**: Coding agents (Claude Code, Cursor, Copilot, etc.) integrating `elasticdash-test` into a user's project.
4
+ > **When to use**: Follow this guide when a user asks you to set up ElasticDash testing, add AI workflow observability, or integrate the `elasticdash-test` SDK.
5
+
6
+ ---
7
+
8
+ ## Prerequisites
9
+
10
+ - Node.js >= 20
11
+ - npm, yarn, or pnpm
12
+ - At least one AI workflow function (a callable async function that makes LLM/tool calls)
13
+ - LLM provider API keys for providers used in workflows (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`)
14
+
15
+ ### Detect project type before starting
16
+
17
+ Check these before choosing patterns:
18
+
19
+ | Check | How | Result |
20
+ |-------|-----|--------|
21
+ | Module system | `package.json` → `"type"` field | `"module"` = ESM, `"commonjs"` or missing = CJS |
22
+ | Framework | Look for `next.config.*`, `remix.config.*`, `svelte.config.*` | Present = framework project, use HTTP mode |
23
+ | Path aliases | `tsconfig.json` → `"paths"` field | Present = need advanced dashboard script |
24
+ | Tool architecture | Does the project use a central `dispatchTool(name, args)` function? | Yes = Pattern A, No = Pattern B |
25
+
26
+ ---
27
+
28
+ ## Step 1: Install
29
+
30
+ ```bash
31
+ npm install elasticdash-test
32
+ ```
33
+
34
+ Add to `.gitignore`:
35
+
36
+ ```gitignore
37
+ .temp/
38
+ .ed_traces/
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Step 2: Create `ed_tools.ts`
44
+
45
+ Create `ed_tools.ts` in the project root. This file wraps each tool function with tracing so ElasticDash can record and replay tool calls.
46
+
47
+ ### Choose your pattern
48
+
49
+ - **Pattern B (recommended for most projects)**: inline async with mock support. Use when tools are imported individually, no central dispatcher exists, or you want dashboard mock support.
50
+ - **Pattern A**: `withTrace` HOF with central dispatcher. Use when tools are pure functions called through a single `dispatchTool(name, args)` dispatcher.
51
+
52
+ ### Pattern B template (recommended)
53
+
54
+ ```ts
55
+ // ed_tools.ts
56
+ // Replace imports with your actual tool functions and source paths
57
+ import { YOUR_TOOL_1 as YOUR_TOOL_1_impl } from './YOUR_SOURCE_PATH_1'
58
+ import { YOUR_TOOL_2 as YOUR_TOOL_2_impl } from './YOUR_SOURCE_PATH_2'
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers — copy as-is, no customization needed
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function resolveMock(toolName: string): { mocked: true; result: unknown } | { mocked: false } {
65
+ const g = globalThis as any
66
+ const mocks = g.__ELASTICDASH_TOOL_MOCKS__
67
+ if (!mocks) return { mocked: false }
68
+
69
+ const entry = mocks[toolName]
70
+ if (!entry || entry.mode === 'live') return { mocked: false }
71
+
72
+ if (!g.__ELASTICDASH_TOOL_CALL_COUNTERS__) g.__ELASTICDASH_TOOL_CALL_COUNTERS__ = {}
73
+ const counters = g.__ELASTICDASH_TOOL_CALL_COUNTERS__
74
+ counters[toolName] = (counters[toolName] ?? 0) + 1
75
+ const callNumber = counters[toolName]
76
+
77
+ if (entry.mode === 'mock-all') {
78
+ const data = entry.mockData ?? {}
79
+ const result = data[callNumber] !== undefined ? data[callNumber] : data[0]
80
+ return { mocked: true, result }
81
+ }
82
+
83
+ if (entry.mode === 'mock-specific') {
84
+ const indices = entry.callIndices ?? []
85
+ if (indices.includes(callNumber)) {
86
+ return { mocked: true, result: (entry.mockData ?? {})[callNumber] }
87
+ }
88
+ return { mocked: false }
89
+ }
90
+
91
+ return { mocked: false }
92
+ }
93
+
94
+ async function safeRecordToolCall(tool: string, input: any, result: any) {
95
+ if (!(globalThis as any).__ELASTICDASH_WORKER__) return
96
+ try {
97
+ const { recordToolCall } = await import('elasticdash-test')
98
+ recordToolCall(tool, input, result)
99
+ } catch { /* tracing must never block business logic */ }
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Tools — one export per tool, following this pattern
104
+ // ---------------------------------------------------------------------------
105
+
106
+ // TEMPLATE: Copy this block for each tool, replacing YOUR_TOOL_NAME and YOUR_TOOL_IMPL
107
+ export const YOUR_TOOL_1 = async (input: any) => {
108
+ const mock = resolveMock('YOUR_TOOL_1')
109
+ if (mock.mocked) {
110
+ await safeRecordToolCall('YOUR_TOOL_1', input, mock.result)
111
+ return mock.result
112
+ }
113
+
114
+ return await YOUR_TOOL_1_impl(input)
115
+ .then(async (res: any) => {
116
+ await safeRecordToolCall('YOUR_TOOL_1', input, res)
117
+ return res
118
+ })
119
+ .catch(async (err: any) => {
120
+ await safeRecordToolCall('YOUR_TOOL_1', input, err)
121
+ throw err
122
+ })
123
+ }
124
+
125
+ export const YOUR_TOOL_2 = async (input: any) => {
126
+ const mock = resolveMock('YOUR_TOOL_2')
127
+ if (mock.mocked) {
128
+ await safeRecordToolCall('YOUR_TOOL_2', input, mock.result)
129
+ return mock.result
130
+ }
131
+
132
+ return await YOUR_TOOL_2_impl(input)
133
+ .then(async (res: any) => {
134
+ await safeRecordToolCall('YOUR_TOOL_2', input, res)
135
+ return res
136
+ })
137
+ .catch(async (err: any) => {
138
+ await safeRecordToolCall('YOUR_TOOL_2', input, err)
139
+ throw err
140
+ })
141
+ }
142
+ ```
143
+
144
+ ### Pattern A template (dispatcher-based)
145
+
146
+ ```ts
147
+ // ed_tools.ts
148
+ // Replace imports with your actual tool functions and source paths
149
+ import {
150
+ YOUR_TOOL_1 as _YOUR_TOOL_1,
151
+ YOUR_TOOL_2 as _YOUR_TOOL_2,
152
+ dispatchTool as _dispatchTool,
153
+ } from './YOUR_TOOLS_SOURCE'
154
+
155
+ async function withTrace<I, O>(
156
+ toolName: string,
157
+ input: I,
158
+ fn: (input: I) => Promise<O>,
159
+ ): Promise<O> {
160
+ const result = await fn(input)
161
+ try {
162
+ const { recordToolCall } = await import('elasticdash-test')
163
+ recordToolCall(toolName, input, result)
164
+ } catch { /* tracing must never block business logic */ }
165
+ return result
166
+ }
167
+
168
+ export function YOUR_TOOL_1(input: Parameters<typeof _YOUR_TOOL_1>[0]) {
169
+ return withTrace('YOUR_TOOL_1', input, _YOUR_TOOL_1)
170
+ }
171
+
172
+ export function YOUR_TOOL_2(input: Parameters<typeof _YOUR_TOOL_2>[0]) {
173
+ return withTrace('YOUR_TOOL_2', input, _YOUR_TOOL_2)
174
+ }
175
+
176
+ export async function dispatchTool(name: string, args: Record<string, unknown>) {
177
+ switch (name) {
178
+ case 'YOUR_TOOL_1': return YOUR_TOOL_1(args as Parameters<typeof _YOUR_TOOL_1>[0])
179
+ case 'YOUR_TOOL_2': return YOUR_TOOL_2(args as Parameters<typeof _YOUR_TOOL_2>[0])
180
+ default: return _dispatchTool(name, args)
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Alternative: `wrapTool` shorthand
186
+
187
+ For simpler setups where you don't need mock support or dashboard replays:
188
+
189
+ ```ts
190
+ // ed_tools.ts
191
+ import { wrapTool } from 'elasticdash-test'
192
+ import { YOUR_TOOL_1 as YOUR_TOOL_1_impl } from './YOUR_SOURCE_PATH'
193
+
194
+ export const YOUR_TOOL_1 = wrapTool('YOUR_TOOL_1', YOUR_TOOL_1_impl)
195
+ ```
196
+
197
+ ### Important rules
198
+
199
+ - The string name passed to `resolveMock()`, `safeRecordToolCall()`, or `wrapTool()` **must match** the exported function name exactly.
200
+ - Each tool function must accept a single input object and return a plain value (JSON-serializable).
201
+ - Tool functions must not close over HTTP context, framework state, or database clients — extract pure logic first.
202
+
203
+ ### Next.js only
204
+
205
+ Add `elasticdash-test` to `serverExternalPackages` in `next.config.ts`:
206
+
207
+ ```ts
208
+ // next.config.ts
209
+ const nextConfig = {
210
+ serverExternalPackages: ['elasticdash-test'],
211
+ }
212
+ export default nextConfig
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Step 3: Create `ed_workflows.ts`
218
+
219
+ Create `ed_workflows.ts` in the project root. This file exports workflow functions for the ElasticDash runner.
220
+
221
+ ### Simple case — direct re-export
222
+
223
+ ```ts
224
+ // ed_workflows.ts
225
+ // Replace with your actual workflow function and source path
226
+ export { YOUR_WORKFLOW } from './YOUR_SOURCE_PATH'
227
+ ```
228
+
229
+ ### Framework adapter case (Next.js / Remix)
230
+
231
+ When the workflow lives inside a route handler, create a plain-value wrapper:
232
+
233
+ ```ts
234
+ // ed_workflows.ts
235
+ import { YOUR_HANDLER as _YOUR_HANDLER } from './app/api/YOUR_ROUTE/route'
236
+
237
+ export async function YOUR_WORKFLOW(input: { message: string; sessionId: string }) {
238
+ return _YOUR_HANDLER(input)
239
+ }
240
+ ```
241
+
242
+ ### Streaming workflow case (Vercel AI SDK)
243
+
244
+ Create a separate handler file that is only imported by `ed_workflows.ts`:
245
+
246
+ ```ts
247
+ // app/api/chat-stream/chatStreamHandler.ts
248
+ import { NextRequest } from 'next/server'
249
+ import { readVercelAIStream, recordToolCall } from 'elasticdash-test'
250
+ import type { VercelAIStreamResult } from 'elasticdash-test'
251
+ import { POST } from './route'
252
+
253
+ export async function chatStreamHandler(args: {
254
+ messages: Array<{ role: string; content: string }>
255
+ sessionId?: string
256
+ }): Promise<VercelAIStreamResult> {
257
+ const req = new NextRequest('http://localhost/api/chat-stream', {
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/json' },
260
+ body: JSON.stringify(args),
261
+ })
262
+
263
+ const response = await POST(req)
264
+
265
+ if (response.headers.get('x-vercel-ai-data-stream') !== 'v1') {
266
+ const errorMessage = await response.text().catch(() => `HTTP ${response.status}`)
267
+ return { message: errorMessage, type: 'error', error: errorMessage }
268
+ }
269
+
270
+ const result = await readVercelAIStream(response)
271
+ recordToolCall('chatStream', args, result)
272
+ return result
273
+ }
274
+ ```
275
+
276
+ Then re-export from `ed_workflows.ts`:
277
+
278
+ ```ts
279
+ // ed_workflows.ts
280
+ export { chatStreamHandler } from './app/api/chat-stream/chatStreamHandler'
281
+ ```
282
+
283
+ ### Requirements for all workflow exports
284
+
285
+ - Accept only JSON-serializable inputs (strings, numbers, arrays, plain objects)
286
+ - Return only JSON-serializable outputs
287
+ - Must not depend on framework runtime APIs, HTTP request context, or live service clients
288
+ - If a dependency is non-serializable (e.g., database client), instantiate it inside `ed_workflows.ts`, not passed as a parameter
289
+
290
+ ---
291
+
292
+ ## Step 4: Update workflow imports
293
+
294
+ Change your workflow code to import tools from `ed_tools.ts` instead of the original source files:
295
+
296
+ ```ts
297
+ // BEFORE
298
+ import { YOUR_TOOL_1 } from './services/YOUR_SOURCE'
299
+
300
+ // AFTER
301
+ import { YOUR_TOOL_1 } from './ed_tools'
302
+ ```
303
+
304
+ This single import change makes all tool calls observable by ElasticDash.
305
+
306
+ ---
307
+
308
+ ## Step 5: Add config and scripts
309
+
310
+ ### `elasticdash.config.ts`
311
+
312
+ Create in project root:
313
+
314
+ ```ts
315
+ // elasticdash.config.ts
316
+ export default {
317
+ testMatch: ['**/*.ai.test.ts'],
318
+ traceMode: 'local' as const,
319
+ }
320
+ ```
321
+
322
+ ### `package.json` scripts
323
+
324
+ Add these scripts:
325
+
326
+ ```json
327
+ {
328
+ "scripts": {
329
+ "dashboard:ai": "elasticdash dashboard",
330
+ "test:ai": "elasticdash test"
331
+ }
332
+ }
333
+ ```
334
+
335
+ **If the project uses TypeScript path aliases** (e.g., `@/lib/...` in `tsconfig.json` `paths`):
336
+
337
+ ```json
338
+ {
339
+ "scripts": {
340
+ "dashboard:ai": "NODE_OPTIONS='--import tsx/esm --require tsx/cjs --require tsconfig-paths/register' elasticdash dashboard"
341
+ }
342
+ }
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Step 6: Environment variables
348
+
349
+ Set in `.env` or CI secrets:
350
+
351
+ | Variable | Description | Required |
352
+ |----------|-------------|----------|
353
+ | `ELASTICDASH_API_URL` | Backend server URL (`https://server.elasticdash.com` for cloud) | For upload/CI |
354
+ | `ELASTICDASH_API_KEY` | Project API key from dashboard | For upload/CI |
355
+ | `ELASTICDASH_CAPTURE_TRACE` | Set to `1` to record a trace fixture | For trace recording |
356
+ | `OPENAI_API_KEY` | OpenAI API key | If using OpenAI |
357
+ | `ANTHROPIC_API_KEY` | Anthropic API key | If using Claude |
358
+ | `GEMINI_API_KEY` | Google Gemini API key | If using Gemini |
359
+ | `GROK_API_KEY` | xAI Grok API key | If using Grok |
360
+
361
+ ---
362
+
363
+ ## Step 7: Write a test
364
+
365
+ ### Option A: `aiTest` — live workflow testing
366
+
367
+ Create a file ending in `.ai.test.ts`:
368
+
369
+ ```ts
370
+ // tests/YOUR_WORKFLOW.ai.test.ts
371
+ import 'elasticdash-test/dist/test-setup.js'
372
+ import { expect } from 'expect'
373
+
374
+ // Import your workflow from ed_workflows.ts
375
+ import { YOUR_WORKFLOW } from '../ed_workflows'
376
+
377
+ aiTest('YOUR_TEST_NAME', async (ctx) => {
378
+ await YOUR_WORKFLOW({ /* your input */ })
379
+
380
+ // Assert an LLM step occurred
381
+ expect(ctx.trace).toHaveLLMStep({ model: 'gpt-4' })
382
+
383
+ // Assert a tool was called
384
+ expect(ctx.trace).toCallTool('YOUR_TOOL_NAME')
385
+
386
+ // Semantic output matching (LLM-judged)
387
+ expect(ctx.trace).toMatchSemanticOutput('expected output description')
388
+ })
389
+ ```
390
+
391
+ ### Option B: `defineTest` — CI/CD fixture-based testing
392
+
393
+ First, record a trace:
394
+
395
+ ```bash
396
+ ELASTICDASH_CAPTURE_TRACE=1 tsx YOUR_WORKFLOW_SCRIPT.ts
397
+ ```
398
+
399
+ Then create `ed_tests.ts`:
400
+
401
+ ```ts
402
+ // ed_tests.ts
403
+ import { defineTest } from 'elasticdash-test'
404
+ import { YOUR_WORKFLOW } from './ed_workflows'
405
+
406
+ defineTest({
407
+ name: 'YOUR_TEST_NAME',
408
+ trace: './.ed_traces/YOUR_TRACE_FILE.json',
409
+ target: { type: 'tool_call', step_id: 'tool_call_0' },
410
+ benchmarks: { max_duration_ms: 2000 },
411
+ run: async () => {
412
+ await YOUR_WORKFLOW({ /* your input */ })
413
+ },
414
+ })
415
+ ```
416
+
417
+ Run:
418
+
419
+ ```bash
420
+ npx ed ed-test --no-upload
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Step 8: Run and verify
426
+
427
+ ```bash
428
+ # Run aiTest tests
429
+ npx elasticdash test
430
+
431
+ # Run defineTest benchmarks
432
+ npx ed ed-test --no-upload
433
+
434
+ # Open the dashboard
435
+ npx elasticdash dashboard
436
+
437
+ # Record a trace fixture
438
+ ELASTICDASH_CAPTURE_TRACE=1 tsx your-workflow.ts
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Decision Trees
444
+
445
+ ### Subprocess mode vs HTTP mode
446
+
447
+ ```
448
+ Does your workflow live inside a framework route handler (Next.js, Remix, SvelteKit)?
449
+ YES → Use HTTP mode:
450
+ 1. Configure workflow in elasticdash.config.ts with mode: 'http'
451
+ 2. Add initHttpRunContext() to your request handler
452
+ 3. Use wrapTool/wrapAI for observability
453
+ NO → Use subprocess mode (default):
454
+ 1. Export workflow from ed_workflows.ts
455
+ 2. Tools auto-intercepted via ed_tools.ts
456
+ ```
457
+
458
+ ### HTTP mode config template
459
+
460
+ ```ts
461
+ // elasticdash.config.ts
462
+ export default {
463
+ testMatch: ['**/*.ai.test.ts'],
464
+ workflows: {
465
+ YOUR_WORKFLOW: {
466
+ mode: 'http',
467
+ url: 'http://localhost:3001/api/YOUR_ENDPOINT',
468
+ method: 'POST',
469
+ headers: {
470
+ 'Content-Type': 'application/json',
471
+ },
472
+ bodyTemplate: {
473
+ messages: [{ role: 'user', content: '{{input.message}}' }],
474
+ },
475
+ responseFormat: 'vercel-ai-stream',
476
+ },
477
+ },
478
+ }
479
+ ```
480
+
481
+ ### HTTP mode handler setup
482
+
483
+ ```ts
484
+ // app/api/YOUR_ENDPOINT/route.ts
485
+ import { initHttpRunContext, wrapTool, wrapAI } from 'elasticdash-test'
486
+
487
+ export async function POST(req: Request) {
488
+ const runId = req.headers.get('x-elasticdash-run-id')
489
+ const serverUrl = req.headers.get('x-elasticdash-server')
490
+ if (runId && serverUrl) {
491
+ await initHttpRunContext(runId, serverUrl)
492
+ }
493
+ // ... rest of handler
494
+ }
495
+ ```
496
+
497
+ ### AI call recording
498
+
499
+ ```
500
+ Does your workflow call LLMs via OpenAI/Gemini/Grok SDKs?
501
+ YES → Automatic interception, no code changes needed
502
+ NO (custom provider or Anthropic SDK) → Use wrapAI:
503
+ import { wrapAI } from 'elasticdash-test'
504
+ export const callLLM = wrapAI('model-name', async (messages) => {
505
+ return await yourLLMClient.call(messages)
506
+ })
507
+ ```
508
+
509
+ ### Agent setup (`ed_agents.ts`)
510
+
511
+ ```
512
+ Does your project use a multi-step agent with a planner/executor pattern?
513
+ YES → Create ed_agents.ts:
514
+ export { plannerAgent, executorAgent } from './your-agent-logic'
515
+ // OR use SDK reference implementations:
516
+ export { plannerAgent, executorAgent, resumeAgentFromTrace } from 'elasticdash-test'
517
+ NO → Skip ed_agents.ts
518
+ ```
519
+
520
+ ---
521
+
522
+ ## Troubleshooting
523
+
524
+ | Error | Cause | Fix |
525
+ |-------|-------|-----|
526
+ | `replay miss: tool_call::YOUR_TOOL` | Trace fixture is stale or workflow changed | Re-record: `ELASTICDASH_CAPTURE_TRACE=1 tsx your-workflow.ts` |
527
+ | `MODULE_NOT_FOUND: elasticdash-test` | SDK not installed or Next.js bundling issue | Run `npm install elasticdash-test`. For Next.js, add to `serverExternalPackages` |
528
+ | `Cannot find module '@/...'` | Path aliases not resolved at runtime | Use advanced dashboard script with `tsconfig-paths/register` |
529
+ | `test has no run function` | `run` field missing in `defineTest` | Add `run: async () => { ... }` to the test definition |
530
+ | `Tool "x" not found in registry` | Tool not exported from `ed_tools.ts` | Export the tool function from `ed_tools.ts` |
531
+ | `ERR_UNKNOWN_FILE_EXTENSION` | ESM/CJS mismatch | Check `package.json` `type` field and `tsconfig.json` `module` setting |
532
+ | Git metadata shows `unknown` | No `.git` directory | Ensure repo is checked out (common in CI with shallow clones) |
533
+
534
+ ---
535
+
536
+ ## Final checklist
537
+
538
+ After integration, verify these files exist:
539
+
540
+ ```
541
+ your-project/
542
+ ed_tools.ts # Instrumented tool wrappers
543
+ ed_workflows.ts # Workflow exports
544
+ elasticdash.config.ts # Test runner config
545
+ package.json # dashboard:ai and test:ai scripts added
546
+ .gitignore # .temp/ and .ed_traces/ added
547
+ ```
548
+
549
+ Verify with:
550
+
551
+ ```bash
552
+ npx elasticdash test # Should discover and run *.ai.test.ts files
553
+ npx elasticdash dashboard # Should open the dashboard UI
554
+ ```
package/docs/agents.md ADDED
@@ -0,0 +1,140 @@
1
+ # Agent Mid-Trace Replay
2
+
3
+ ElasticDash supports resuming long-running agents from any task in their plan — without re-executing already-completed steps.
4
+
5
+ ## Use Cases
6
+
7
+ - **Resuming after failures**: If task 3 of 5 fails, fix the issue and re-run from task 3 only
8
+ - **Pausing for approval**: Capture state after task 2, get human sign-off, then continue
9
+ - **Debugging in isolation**: Re-run a single task with modified input to diagnose a problem
10
+
11
+ ## How It Works
12
+
13
+ Agents are structured as an **AgentPlan** — an ordered list of **AgentTask** objects. When serialized with captured trace events, this forms an **AgentState** that can be saved and replayed later.
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { plannerAgent, executorAgent, resumeAgentFromTrace } from './ed_agents'
19
+ import { serializeAgentState, deserializeAgentState } from 'elasticdash-test'
20
+ import fs from 'node:fs'
21
+
22
+ // 1. Generate a plan
23
+ const plan = await plannerAgent('Show me sales for Q1', { userToken: 'tok-abc' })
24
+
25
+ // 2. Execute the plan (runs all tasks sequentially)
26
+ const completedPlan = await executorAgent(plan)
27
+
28
+ // 3. Serialize and save state (e.g., after partial execution)
29
+ const state = serializeAgentState(completedPlan, [] /* pass recorder.events in worker context */)
30
+ fs.writeFileSync('agent-state.json', JSON.stringify(state, null, 2))
31
+
32
+ // 4. Later: load saved state and resume from task 2 (0-based index 1)
33
+ const savedState = JSON.parse(fs.readFileSync('agent-state.json', 'utf8'))
34
+ const stateToResume = deserializeAgentState({ ...savedState, resumeFromTaskIndex: 1 })
35
+ const resumedPlan = await resumeAgentFromTrace(stateToResume)
36
+
37
+ console.log('Resumed plan status:', resumedPlan.status)
38
+ console.log('Task outputs:', resumedPlan.tasks.map((t) => ({ id: t.id, status: t.status })))
39
+ ```
40
+
41
+ ## Data Structures
42
+
43
+ ### AgentState
44
+
45
+ ```ts
46
+ interface AgentState {
47
+ plan: AgentPlan // Full plan with all tasks (completed and pending)
48
+ trace: WorkflowEvent[] // Captured trace events from previous execution
49
+ resumeFromTaskIndex: number // Zero-based index — tasks before this are loaded from cache
50
+ }
51
+ ```
52
+
53
+ ### AgentPlan
54
+
55
+ ```ts
56
+ interface AgentPlan {
57
+ id: string
58
+ tasks: AgentTask[]
59
+ status: 'planning' | 'executing' | 'completed' | 'failed' | 'paused'
60
+ currentTaskIndex: number
61
+ context: Record<string, unknown>
62
+ metadata: Record<string, unknown>
63
+ }
64
+ ```
65
+
66
+ ### AgentTask
67
+
68
+ ```ts
69
+ interface AgentTask {
70
+ id: string
71
+ status: 'pending' | 'in-progress' | 'completed' | 'failed'
72
+ description: string
73
+ tool: string // Name of the tool function to invoke
74
+ input: unknown // May contain { $ref: "task-N.output.fieldName" } placeholders
75
+ output?: unknown // Populated after execution
76
+ error?: string
77
+ startedAt?: number
78
+ completedAt?: number
79
+ }
80
+ ```
81
+
82
+ ## Task Input Placeholders
83
+
84
+ Task inputs can reference previous task outputs using `{ $ref: "taskId.output.fieldPath" }`:
85
+
86
+ ```ts
87
+ // task-2 uses the embedding produced by task-1
88
+ {
89
+ id: 'task-2',
90
+ tool: 'taskSelectorService',
91
+ input: {
92
+ queryEmbedding: { $ref: 'task-1.output.embedding' },
93
+ topK: 3,
94
+ }
95
+ }
96
+ ```
97
+
98
+ Placeholders are resolved at execution time by `resolveTaskInput()`.
99
+
100
+ ## Dashboard Integration
101
+
102
+ When running an agent workflow through the dashboard:
103
+
104
+ 1. **Agent task observations** are visually highlighted with a purple background and left border
105
+ 2. Each observation shows a **T1 / T2 / T3** badge indicating which task it belongs to
106
+ 3. In the observation detail panel, a **"Resume from Task N"** button appears (agent steps only)
107
+ 4. Clicking it calls `/api/resume-agent-from-task` with the serialized `AgentState` and chosen `taskIndex`
108
+ 5. The resumed run is added as a new trace in the comparison table
109
+
110
+ ## Best Practices
111
+
112
+ - **Keep tasks idempotent** where possible — if a task must be re-run, ensure it produces the same result
113
+ - **Store minimal outputs** — only record what downstream tasks need, not full API responses
114
+ - **Version your state schema** — if tool interfaces change, old states may need migration
115
+ - **Use sequential tasks** — the current implementation runs tasks one-by-one; parallel task support is planned
116
+
117
+ ## Example: Debugging a Failed Task
118
+
119
+ ```ts
120
+ // 1. Original execution fails at task 3
121
+ const plan = await plannerAgent('Process refund for order-123')
122
+ const result = await executorAgent(plan)
123
+ // Error: task 3 (calculateRefundAmount) failed
124
+
125
+ // 2. Save the state
126
+ const state = serializeAgentState(result, recorder.events)
127
+ fs.writeFileSync('failed-run.json', JSON.stringify(state))
128
+
129
+ // 3. Fix the issue in your tool/code
130
+
131
+ // 4. Resume from task 3 with corrected state
132
+ const savedState = JSON.parse(fs.readFileSync('failed-run.json'))
133
+ const fixed = await resumeAgentFromTrace({
134
+ ...deserializeAgentState(savedState),
135
+ resumeFromTaskIndex: 2 // 0-based: task 3 = index 2
136
+ })
137
+
138
+ // Tasks 1-2 use cached results; task 3+ execute with fixes
139
+ console.log('Fixed plan:', fixed.status)
140
+ ```