groundswell 0.0.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.
Files changed (120) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.claude/system_prompts/task-breakdown.md +100 -0
  3. package/PRPs/001-hierarchical-workflow-engine.md +2438 -0
  4. package/PRPs/PRDs/001-hierarchical-workflow-engine.md +543 -0
  5. package/PRPs/PRDs/002-agent-prompt.md +390 -0
  6. package/PRPs/PRDs/003-agent-prompt.md +943 -0
  7. package/PRPs/PRDs/004-agent-prompt.md +1136 -0
  8. package/PRPs/PRDs/tasks-001.json +492 -0
  9. package/PRPs/README.md +83 -0
  10. package/PRPs/templates/prp_base.md +222 -0
  11. package/README.md +218 -0
  12. package/docs/agent.md +422 -0
  13. package/docs/prompt.md +419 -0
  14. package/docs/workflow.md +600 -0
  15. package/examples/README.md +244 -0
  16. package/examples/examples/01-basic-workflow.ts +100 -0
  17. package/examples/examples/02-decorator-options.ts +217 -0
  18. package/examples/examples/03-parent-child.ts +241 -0
  19. package/examples/examples/04-observers-debugger.ts +340 -0
  20. package/examples/examples/05-error-handling.ts +387 -0
  21. package/examples/examples/06-concurrent-tasks.ts +352 -0
  22. package/examples/examples/07-agent-loops.ts +432 -0
  23. package/examples/examples/08-sdk-features.ts +667 -0
  24. package/examples/examples/09-reflection.ts +573 -0
  25. package/examples/examples/10-introspection.ts +550 -0
  26. package/examples/index.ts +143 -0
  27. package/examples/utils/helpers.ts +57 -0
  28. package/llms_full.txt +5890 -0
  29. package/package.json +63 -0
  30. package/plan/P1P2/PRP.md +527 -0
  31. package/plan/P1P2/research/LRU_CACHE_BEST_PRACTICES.md +1929 -0
  32. package/plan/P1P2/research/LRU_CACHE_CODE_PATTERNS.md +857 -0
  33. package/plan/P1P2/research/LRU_CACHE_INTEGRATION_GUIDE.md +738 -0
  34. package/plan/P1P2/research/LRU_CACHE_RESEARCH_INDEX.md +424 -0
  35. package/plan/P1P2/research/REFLECTION_INDEX.md +291 -0
  36. package/plan/P1P2/research/REFLECTION_RESEARCH_REPORT.md +1342 -0
  37. package/plan/P1P2/research/RESEARCH_SUMMARY.md +342 -0
  38. package/plan/P1P2/research/anthropic-sdk.md +174 -0
  39. package/plan/P1P2/research/async-local-storage.md +200 -0
  40. package/plan/P1P2/research/reflection-code-patterns.md +1205 -0
  41. package/plan/P1P2/research/reflection-decision-matrix.md +421 -0
  42. package/plan/P1P2/research/reflection-implementation-guide.md +1341 -0
  43. package/plan/P1P2/research/reflection-integration-guide.md +834 -0
  44. package/plan/P1P2/research/reflection-patterns.md +1468 -0
  45. package/plan/P1P2/research/reflection-quick-reference.md +558 -0
  46. package/plan/P1P2/research/zod-schema.md +152 -0
  47. package/plan/P3P4/PRP.md +1388 -0
  48. package/plan/P3P4/research/caching-lru.md +116 -0
  49. package/plan/P3P4/research/introspection-tools.md +177 -0
  50. package/plan/P3P4/research/reflection-patterns.md +117 -0
  51. package/plan/P4P5/PRP.md +1136 -0
  52. package/plan/P4P5/research/RESEARCH_SUMMARY.md +151 -0
  53. package/plan/architecture/external_deps.md +358 -0
  54. package/plan/architecture/system_context.md +242 -0
  55. package/plan/backlog.json +867 -0
  56. package/plan/research/INTROSPECTION_RESEARCH_SUMMARY.md +378 -0
  57. package/plan/research/README-INTROSPECTION.md +352 -0
  58. package/plan/research/agent-introspection-patterns.md +1085 -0
  59. package/plan/research/introspection-security-guide.md +928 -0
  60. package/plan/research/introspection-tool-examples.md +875 -0
  61. package/scripts/generate-llms-full.ts +206 -0
  62. package/src/__tests__/integration/agent-workflow.test.ts +256 -0
  63. package/src/__tests__/integration/tree-mirroring.test.ts +114 -0
  64. package/src/__tests__/unit/agent.test.ts +169 -0
  65. package/src/__tests__/unit/cache-key.test.ts +182 -0
  66. package/src/__tests__/unit/cache.test.ts +172 -0
  67. package/src/__tests__/unit/context.test.ts +138 -0
  68. package/src/__tests__/unit/decorators.test.ts +100 -0
  69. package/src/__tests__/unit/introspection-tools.test.ts +277 -0
  70. package/src/__tests__/unit/prompt.test.ts +135 -0
  71. package/src/__tests__/unit/reflection.test.ts +210 -0
  72. package/src/__tests__/unit/tree-debugger.test.ts +85 -0
  73. package/src/__tests__/unit/workflow.test.ts +81 -0
  74. package/src/cache/cache-key.ts +244 -0
  75. package/src/cache/cache.ts +236 -0
  76. package/src/cache/index.ts +8 -0
  77. package/src/core/agent.ts +573 -0
  78. package/src/core/context.ts +119 -0
  79. package/src/core/event-tree.ts +260 -0
  80. package/src/core/factory.ts +123 -0
  81. package/src/core/index.ts +17 -0
  82. package/src/core/logger.ts +87 -0
  83. package/src/core/mcp-handler.ts +184 -0
  84. package/src/core/prompt.ts +150 -0
  85. package/src/core/workflow-context.ts +349 -0
  86. package/src/core/workflow.ts +302 -0
  87. package/src/debugger/index.ts +1 -0
  88. package/src/debugger/tree-debugger.ts +210 -0
  89. package/src/decorators/index.ts +3 -0
  90. package/src/decorators/observed-state.ts +95 -0
  91. package/src/decorators/step.ts +139 -0
  92. package/src/decorators/task.ts +96 -0
  93. package/src/examples/index.ts +2 -0
  94. package/src/examples/tdd-orchestrator.ts +65 -0
  95. package/src/examples/test-cycle-workflow.ts +64 -0
  96. package/src/index.ts +140 -0
  97. package/src/reflection/index.ts +5 -0
  98. package/src/reflection/reflection.ts +407 -0
  99. package/src/tools/index.ts +36 -0
  100. package/src/tools/introspection.ts +464 -0
  101. package/src/types/agent.ts +90 -0
  102. package/src/types/decorators.ts +25 -0
  103. package/src/types/error-strategy.ts +13 -0
  104. package/src/types/error.ts +20 -0
  105. package/src/types/events.ts +74 -0
  106. package/src/types/index.ts +55 -0
  107. package/src/types/logging.ts +24 -0
  108. package/src/types/observer.ts +18 -0
  109. package/src/types/prompt.ts +40 -0
  110. package/src/types/reflection.ts +117 -0
  111. package/src/types/sdk-primitives.ts +128 -0
  112. package/src/types/snapshot.ts +14 -0
  113. package/src/types/workflow-context.ts +163 -0
  114. package/src/types/workflow.ts +37 -0
  115. package/src/utils/id.ts +11 -0
  116. package/src/utils/index.ts +3 -0
  117. package/src/utils/observable.ts +77 -0
  118. package/tasks.json +0 -0
  119. package/tsconfig.json +22 -0
  120. package/vitest.config.ts +16 -0
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Agent - Lightweight wrapper around Anthropic's Agent SDK
3
+ *
4
+ * Agents execute prompts and manage tool invocation cycles.
5
+ * All configuration properties map 1:1 to Anthropic SDK.
6
+ */
7
+
8
+ import Anthropic from '@anthropic-ai/sdk';
9
+ import type {
10
+ AgentConfig,
11
+ PromptOverrides,
12
+ Tool,
13
+ AgentHooks,
14
+ TokenUsage,
15
+ PreToolUseContext,
16
+ PostToolUseContext,
17
+ SessionStartContext,
18
+ SessionEndContext,
19
+ WorkflowEvent,
20
+ } from '../types/index.js';
21
+ import type { Prompt } from './prompt.js';
22
+ import { MCPHandler } from './mcp-handler.js';
23
+ import { generateId } from '../utils/id.js';
24
+ import { getExecutionContext } from './context.js';
25
+ import { generateCacheKey, defaultCache } from '../cache/index.js';
26
+ import type { CacheKeyInputs } from '../cache/index.js';
27
+
28
+ /**
29
+ * Result from a prompt execution including metadata
30
+ */
31
+ export interface PromptResult<T> {
32
+ /** Validated response data */
33
+ data: T;
34
+ /** Token usage from the API */
35
+ usage: TokenUsage;
36
+ /** Total duration in milliseconds */
37
+ duration: number;
38
+ /** Number of tool invocations */
39
+ toolCalls: number;
40
+ }
41
+
42
+ /**
43
+ * Internal message type for conversation history
44
+ */
45
+ type Message = Anthropic.MessageParam;
46
+
47
+ /**
48
+ * Agent class - executes prompts via Anthropic SDK
49
+ */
50
+ export class Agent {
51
+ /** Unique identifier for this agent instance */
52
+ public readonly id: string;
53
+
54
+ /** Agent name */
55
+ public readonly name: string;
56
+
57
+ /** Stored configuration */
58
+ private readonly config: AgentConfig;
59
+
60
+ /** Anthropic client instance */
61
+ private readonly client: Anthropic;
62
+
63
+ /** MCP handler for tool management */
64
+ private readonly mcpHandler: MCPHandler;
65
+
66
+ /** Default model to use */
67
+ private readonly model: string;
68
+
69
+ /**
70
+ * Create a new Agent instance
71
+ * @param config Agent configuration
72
+ */
73
+ constructor(config: AgentConfig = {}) {
74
+ this.id = generateId();
75
+ this.name = config.name ?? 'Agent';
76
+ this.config = config;
77
+ this.model = config.model ?? 'claude-sonnet-4-20250514';
78
+
79
+ // Create Anthropic client
80
+ this.client = new Anthropic({
81
+ apiKey: process.env.ANTHROPIC_API_KEY,
82
+ });
83
+
84
+ // Initialize MCP handler
85
+ this.mcpHandler = new MCPHandler();
86
+
87
+ // Register MCP servers
88
+ if (config.mcps) {
89
+ for (const mcp of config.mcps) {
90
+ this.mcpHandler.registerServer(mcp);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Execute a prompt and return validated response
97
+ * @param prompt Prompt to execute
98
+ * @param overrides Optional overrides for this execution
99
+ * @returns Validated response of type T
100
+ */
101
+ public async prompt<T>(
102
+ prompt: Prompt<T>,
103
+ overrides?: PromptOverrides
104
+ ): Promise<T> {
105
+ const result = await this.executePrompt(prompt, overrides);
106
+ return result.data;
107
+ }
108
+
109
+ /**
110
+ * Execute a prompt with full result metadata
111
+ * @param prompt Prompt to execute
112
+ * @param overrides Optional overrides for this execution
113
+ * @returns Full result including metadata
114
+ */
115
+ public async promptWithMetadata<T>(
116
+ prompt: Prompt<T>,
117
+ overrides?: PromptOverrides
118
+ ): Promise<PromptResult<T>> {
119
+ return this.executePrompt(prompt, overrides);
120
+ }
121
+
122
+ /**
123
+ * Execute a prompt with reflection capabilities
124
+ * @param prompt Prompt to execute
125
+ * @param overrides Optional overrides for this execution
126
+ * @returns Validated response of type T
127
+ */
128
+ public async reflect<T>(
129
+ prompt: Prompt<T>,
130
+ overrides?: PromptOverrides
131
+ ): Promise<T> {
132
+ // Add reflection system prefix if reflection is enabled
133
+ const reflectionEnabled =
134
+ prompt.enableReflection ??
135
+ overrides?.enableReflection ??
136
+ this.config.enableReflection;
137
+
138
+ const systemPrefix = reflectionEnabled
139
+ ? 'Before answering, reflect on your reasoning step by step. Consider alternative approaches and potential errors. Then provide your final answer.\n\n'
140
+ : '';
141
+
142
+ const effectiveOverrides: PromptOverrides = {
143
+ ...overrides,
144
+ system:
145
+ systemPrefix +
146
+ (prompt.systemOverride ?? overrides?.system ?? this.config.system ?? ''),
147
+ };
148
+
149
+ const result = await this.executePrompt(prompt, effectiveOverrides);
150
+ return result.data;
151
+ }
152
+
153
+ /**
154
+ * Get the MCP handler for custom tool registration
155
+ */
156
+ public getMcpHandler(): MCPHandler {
157
+ return this.mcpHandler;
158
+ }
159
+
160
+ /**
161
+ * Emit an event if within workflow context
162
+ */
163
+ private emitWorkflowEvent(event: WorkflowEvent): void {
164
+ const ctx = getExecutionContext();
165
+ if (ctx) {
166
+ ctx.emitEvent(event);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Internal prompt execution with full flow
172
+ */
173
+ private async executePrompt<T>(
174
+ prompt: Prompt<T>,
175
+ overrides?: PromptOverrides
176
+ ): Promise<PromptResult<T>> {
177
+ const startTime = Date.now();
178
+ let toolCallCount = 0;
179
+ let totalUsage: TokenUsage = { input_tokens: 0, output_tokens: 0 };
180
+
181
+ // Get execution context for event emission
182
+ const ctx = getExecutionContext();
183
+
184
+ // Merge configuration: Prompt > Overrides > Config
185
+ const effectiveSystem =
186
+ prompt.systemOverride ?? overrides?.system ?? this.config.system;
187
+
188
+ const effectiveModel = overrides?.model ?? this.model;
189
+ const effectiveMaxTokens = overrides?.maxTokens ?? this.config.maxTokens ?? 4096;
190
+ const effectiveTemperature =
191
+ overrides?.temperature ?? this.config.temperature;
192
+
193
+ // Check cache if enabled
194
+ const cacheEnabled = this.config.enableCache && !overrides?.disableCache;
195
+ let cacheKey: string | undefined;
196
+
197
+ if (cacheEnabled) {
198
+ const cacheInputs: CacheKeyInputs = {
199
+ user: prompt.buildUserMessage(),
200
+ data: prompt.getData(),
201
+ system: effectiveSystem,
202
+ model: effectiveModel,
203
+ temperature: effectiveTemperature,
204
+ maxTokens: effectiveMaxTokens,
205
+ tools: this.config.tools,
206
+ mcps: this.config.mcps,
207
+ skills: this.config.skills,
208
+ responseFormat: prompt.getResponseFormat(),
209
+ };
210
+ cacheKey = generateCacheKey(cacheInputs);
211
+
212
+ const cached = await defaultCache.get(cacheKey) as PromptResult<T> | undefined;
213
+ if (cached) {
214
+ // Emit cache hit event
215
+ if (ctx) {
216
+ this.emitWorkflowEvent({
217
+ type: 'cacheHit',
218
+ key: cacheKey,
219
+ node: ctx.workflowNode,
220
+ });
221
+ }
222
+ return cached;
223
+ }
224
+
225
+ // Emit cache miss event
226
+ if (ctx) {
227
+ this.emitWorkflowEvent({
228
+ type: 'cacheMiss',
229
+ key: cacheKey,
230
+ node: ctx.workflowNode,
231
+ });
232
+ }
233
+ }
234
+
235
+ // Emit prompt start event if in workflow context
236
+ if (ctx) {
237
+ this.emitWorkflowEvent({
238
+ type: 'agentPromptStart',
239
+ agentId: this.id,
240
+ agentName: this.name,
241
+ promptId: prompt.id,
242
+ node: ctx.workflowNode,
243
+ });
244
+ }
245
+
246
+ const effectiveTools = this.mergeTools(
247
+ prompt.toolsOverride ?? overrides?.tools ?? this.config.tools
248
+ );
249
+
250
+ const effectiveHooks = this.mergeHooks(
251
+ prompt.hooksOverride,
252
+ overrides?.hooks,
253
+ this.config.hooks
254
+ );
255
+
256
+ const effectiveStop = overrides?.stop;
257
+
258
+ // Set up environment variables
259
+ const originalEnv = this.setupEnvironment(overrides?.env ?? this.config.env);
260
+
261
+ try {
262
+ // Call session start hooks
263
+ await this.callHooks(effectiveHooks?.sessionStart, {
264
+ agentId: this.id,
265
+ agentName: this.name,
266
+ } as SessionStartContext);
267
+
268
+ // Build initial messages
269
+ const messages: Message[] = [
270
+ { role: 'user', content: prompt.buildUserMessage() },
271
+ ];
272
+
273
+ // Execute conversation loop
274
+ let response = await this.callApi(
275
+ messages,
276
+ effectiveSystem,
277
+ effectiveTools,
278
+ effectiveModel,
279
+ effectiveMaxTokens,
280
+ effectiveTemperature,
281
+ effectiveStop
282
+ );
283
+
284
+ totalUsage = this.addUsage(totalUsage, response.usage);
285
+
286
+ // Handle tool use loop
287
+ while (response.stop_reason === 'tool_use') {
288
+ const toolUseBlocks = response.content.filter(
289
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
290
+ );
291
+
292
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
293
+
294
+ for (const toolUse of toolUseBlocks) {
295
+ toolCallCount++;
296
+
297
+ // Call pre-tool hooks
298
+ await this.callHooks(effectiveHooks?.preToolUse, {
299
+ toolName: toolUse.name,
300
+ toolInput: toolUse.input,
301
+ agentId: this.id,
302
+ } as PreToolUseContext);
303
+
304
+ const toolStartTime = Date.now();
305
+
306
+ // Execute tool
307
+ const result = await this.executeTool(toolUse.name, toolUse.input);
308
+
309
+ const toolDuration = Date.now() - toolStartTime;
310
+
311
+ // Emit tool invocation event if in workflow context
312
+ if (ctx) {
313
+ this.emitWorkflowEvent({
314
+ type: 'toolInvocation',
315
+ toolName: toolUse.name,
316
+ input: toolUse.input,
317
+ output: result,
318
+ duration: toolDuration,
319
+ node: ctx.workflowNode,
320
+ });
321
+ }
322
+
323
+ // Call post-tool hooks
324
+ await this.callHooks(effectiveHooks?.postToolUse, {
325
+ toolName: toolUse.name,
326
+ toolInput: toolUse.input,
327
+ toolOutput: result,
328
+ agentId: this.id,
329
+ duration: toolDuration,
330
+ } as PostToolUseContext);
331
+
332
+ toolResults.push({
333
+ type: 'tool_result',
334
+ tool_use_id: toolUse.id,
335
+ content:
336
+ typeof result === 'string' ? result : JSON.stringify(result),
337
+ });
338
+ }
339
+
340
+ // Add assistant message with tool uses
341
+ messages.push({ role: 'assistant', content: response.content });
342
+
343
+ // Add tool results
344
+ messages.push({ role: 'user', content: toolResults });
345
+
346
+ // Continue conversation
347
+ response = await this.callApi(
348
+ messages,
349
+ effectiveSystem,
350
+ effectiveTools,
351
+ effectiveModel,
352
+ effectiveMaxTokens,
353
+ effectiveTemperature,
354
+ effectiveStop
355
+ );
356
+
357
+ totalUsage = this.addUsage(totalUsage, response.usage);
358
+ }
359
+
360
+ // Extract text response
361
+ const textContent = response.content.find(
362
+ (block): block is Anthropic.TextBlock => block.type === 'text'
363
+ );
364
+
365
+ if (!textContent) {
366
+ throw new Error('No text response received from API');
367
+ }
368
+
369
+ // Parse JSON from response
370
+ const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
371
+ if (!jsonMatch) {
372
+ throw new Error('No JSON object found in response');
373
+ }
374
+
375
+ const parsed = JSON.parse(jsonMatch[0]);
376
+
377
+ // Validate with schema
378
+ const validated = prompt.validateResponse(parsed);
379
+
380
+ // Call session end hooks
381
+ await this.callHooks(effectiveHooks?.sessionEnd, {
382
+ agentId: this.id,
383
+ agentName: this.name,
384
+ totalDuration: Date.now() - startTime,
385
+ } as SessionEndContext);
386
+
387
+ const duration = Date.now() - startTime;
388
+
389
+ // Emit prompt end event if in workflow context
390
+ if (ctx) {
391
+ this.emitWorkflowEvent({
392
+ type: 'agentPromptEnd',
393
+ agentId: this.id,
394
+ agentName: this.name,
395
+ promptId: prompt.id,
396
+ node: ctx.workflowNode,
397
+ duration,
398
+ tokenUsage: totalUsage,
399
+ });
400
+ }
401
+
402
+ const result: PromptResult<T> = {
403
+ data: validated,
404
+ usage: totalUsage,
405
+ duration,
406
+ toolCalls: toolCallCount,
407
+ };
408
+
409
+ // Store in cache if enabled
410
+ if (cacheEnabled && cacheKey) {
411
+ await defaultCache.set(cacheKey, result, { prefix: this.id });
412
+ }
413
+
414
+ return result;
415
+ } finally {
416
+ // Restore environment
417
+ this.restoreEnvironment(originalEnv);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Call the Anthropic API
423
+ */
424
+ private async callApi(
425
+ messages: Message[],
426
+ system: string | undefined,
427
+ tools: Tool[] | undefined,
428
+ model: string,
429
+ maxTokens: number,
430
+ temperature: number | undefined,
431
+ stop: string[] | undefined
432
+ ): Promise<Anthropic.Message> {
433
+ const params: Anthropic.MessageCreateParams = {
434
+ model,
435
+ max_tokens: maxTokens,
436
+ messages,
437
+ };
438
+
439
+ if (system) {
440
+ params.system = system;
441
+ }
442
+
443
+ if (tools && tools.length > 0) {
444
+ params.tools = tools.map((tool) => ({
445
+ name: tool.name,
446
+ description: tool.description,
447
+ input_schema: tool.input_schema as Anthropic.Tool['input_schema'],
448
+ }));
449
+ }
450
+
451
+ if (temperature !== undefined) {
452
+ params.temperature = temperature;
453
+ }
454
+
455
+ if (stop && stop.length > 0) {
456
+ params.stop_sequences = stop;
457
+ }
458
+
459
+ return this.client.messages.create(params);
460
+ }
461
+
462
+ /**
463
+ * Execute a tool (either direct or via MCP)
464
+ */
465
+ private async executeTool(name: string, input: unknown): Promise<unknown> {
466
+ // Check if it's an MCP tool
467
+ if (this.mcpHandler.hasTool(name)) {
468
+ const result = await this.mcpHandler.executeTool(name, input);
469
+ if (result.is_error) {
470
+ throw new Error(result.content as string);
471
+ }
472
+ return result.content;
473
+ }
474
+
475
+ // Look for direct tool handler - this would be set by subclasses
476
+ throw new Error(`No handler found for tool '${name}'`);
477
+ }
478
+
479
+ /**
480
+ * Merge tools from config and MCP servers
481
+ */
482
+ private mergeTools(configTools?: Tool[]): Tool[] | undefined {
483
+ const mcpTools = this.mcpHandler.getTools();
484
+ const directTools = configTools ?? [];
485
+
486
+ const allTools = [...directTools, ...mcpTools];
487
+ return allTools.length > 0 ? allTools : undefined;
488
+ }
489
+
490
+ /**
491
+ * Merge hooks from multiple sources
492
+ */
493
+ private mergeHooks(
494
+ promptHooks?: AgentHooks,
495
+ overrideHooks?: AgentHooks,
496
+ configHooks?: AgentHooks
497
+ ): AgentHooks {
498
+ return {
499
+ preToolUse: [
500
+ ...(configHooks?.preToolUse ?? []),
501
+ ...(overrideHooks?.preToolUse ?? []),
502
+ ...(promptHooks?.preToolUse ?? []),
503
+ ],
504
+ postToolUse: [
505
+ ...(configHooks?.postToolUse ?? []),
506
+ ...(overrideHooks?.postToolUse ?? []),
507
+ ...(promptHooks?.postToolUse ?? []),
508
+ ],
509
+ sessionStart: [
510
+ ...(configHooks?.sessionStart ?? []),
511
+ ...(overrideHooks?.sessionStart ?? []),
512
+ ...(promptHooks?.sessionStart ?? []),
513
+ ],
514
+ sessionEnd: [
515
+ ...(configHooks?.sessionEnd ?? []),
516
+ ...(overrideHooks?.sessionEnd ?? []),
517
+ ...(promptHooks?.sessionEnd ?? []),
518
+ ],
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Call hooks of a specific type
524
+ */
525
+ private async callHooks<T>(
526
+ hooks: ((context: T) => Promise<void> | void)[] | undefined,
527
+ context: T
528
+ ): Promise<void> {
529
+ if (!hooks) return;
530
+ for (const hook of hooks) {
531
+ await hook(context);
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Set up environment variables
537
+ */
538
+ private setupEnvironment(
539
+ env?: Record<string, string>
540
+ ): Record<string, string | undefined> {
541
+ if (!env) return {};
542
+
543
+ const original: Record<string, string | undefined> = {};
544
+ for (const [key, value] of Object.entries(env)) {
545
+ original[key] = process.env[key];
546
+ process.env[key] = value;
547
+ }
548
+ return original;
549
+ }
550
+
551
+ /**
552
+ * Restore environment variables
553
+ */
554
+ private restoreEnvironment(original: Record<string, string | undefined>): void {
555
+ for (const [key, value] of Object.entries(original)) {
556
+ if (value === undefined) {
557
+ delete process.env[key];
558
+ } else {
559
+ process.env[key] = value;
560
+ }
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Add token usage from response
566
+ */
567
+ private addUsage(total: TokenUsage, usage: Anthropic.Usage): TokenUsage {
568
+ return {
569
+ input_tokens: total.input_tokens + usage.input_tokens,
570
+ output_tokens: total.output_tokens + usage.output_tokens,
571
+ };
572
+ }
573
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * AgentExecutionContext - Provides zero-plumbing context propagation
3
+ *
4
+ * Uses Node.js AsyncLocalStorage to automatically propagate context
5
+ * through async call chains without manual passing.
6
+ */
7
+
8
+ import { AsyncLocalStorage } from 'node:async_hooks';
9
+ import type { WorkflowNode, WorkflowEvent } from '../types/index.js';
10
+
11
+ /**
12
+ * Context available during agent/prompt execution
13
+ */
14
+ export interface AgentExecutionContext {
15
+ /** Current workflow node in the hierarchy */
16
+ workflowNode: WorkflowNode;
17
+
18
+ /** Function to emit events to the workflow tree */
19
+ emitEvent: (event: WorkflowEvent) => void;
20
+
21
+ /** Workflow ID for tracking */
22
+ workflowId: string;
23
+
24
+ /** Parent workflow ID if nested */
25
+ parentWorkflowId?: string;
26
+ }
27
+
28
+ /**
29
+ * Global AsyncLocalStorage instance for execution context
30
+ * Single shared instance per application (recommended pattern)
31
+ */
32
+ const executionContext = new AsyncLocalStorage<AgentExecutionContext>();
33
+
34
+ /**
35
+ * Get the current execution context
36
+ * @returns Current context or undefined if not in a workflow
37
+ */
38
+ export function getExecutionContext(): AgentExecutionContext | undefined {
39
+ return executionContext.getStore();
40
+ }
41
+
42
+ /**
43
+ * Get the current execution context, throwing if not available
44
+ * @param operation Name of the operation requiring context
45
+ * @returns Current context
46
+ * @throws Error if not within a workflow context
47
+ */
48
+ export function requireExecutionContext(
49
+ operation: string
50
+ ): AgentExecutionContext {
51
+ const context = executionContext.getStore();
52
+ if (!context) {
53
+ throw new Error(
54
+ `${operation} called outside of workflow context. ` +
55
+ `Agent/Prompt operations must be executed within a workflow step.`
56
+ );
57
+ }
58
+ return context;
59
+ }
60
+
61
+ /**
62
+ * Run a function within an execution context
63
+ * @param context Context to establish
64
+ * @param fn Function to execute
65
+ * @returns Result of the function
66
+ */
67
+ export async function runInContext<T>(
68
+ context: AgentExecutionContext,
69
+ fn: () => Promise<T>
70
+ ): Promise<T> {
71
+ return executionContext.run(context, fn);
72
+ }
73
+
74
+ /**
75
+ * Run a synchronous function within an execution context
76
+ * @param context Context to establish
77
+ * @param fn Function to execute
78
+ * @returns Result of the function
79
+ */
80
+ export function runInContextSync<T>(
81
+ context: AgentExecutionContext,
82
+ fn: () => T
83
+ ): T {
84
+ return executionContext.run(context, fn);
85
+ }
86
+
87
+ /**
88
+ * Check if currently within an execution context
89
+ * @returns true if context is available
90
+ */
91
+ export function hasExecutionContext(): boolean {
92
+ return executionContext.getStore() !== undefined;
93
+ }
94
+
95
+ /**
96
+ * Create a child context with updated node
97
+ * @param childNode The new workflow node for the child context
98
+ * @returns New context with child node
99
+ */
100
+ export function createChildContext(
101
+ childNode: WorkflowNode
102
+ ): AgentExecutionContext | undefined {
103
+ const parent = getExecutionContext();
104
+ if (!parent) {
105
+ return undefined;
106
+ }
107
+
108
+ return {
109
+ workflowNode: childNode,
110
+ emitEvent: parent.emitEvent,
111
+ workflowId: parent.workflowId,
112
+ parentWorkflowId: parent.parentWorkflowId,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Export the raw AsyncLocalStorage for advanced use cases
118
+ */
119
+ export { executionContext as agentExecutionStorage };