@amitdeshmukh/ax-crew 6.0.0 → 8.0.0

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,594 @@
1
+ /**
2
+ * ACE (Agentic Context Engineering) integration for AxCrew
3
+ *
4
+ * This module provides helpers to build and manage AxACE optimizers for agents,
5
+ * enabling offline compilation and online learning from feedback.
6
+ *
7
+ * Reference: https://axllm.dev/ace/
8
+ */
9
+
10
+ import { AxACE, ai as buildAI, type AxMetricFn, AxSignature, AxGen } from "@ax-llm/ax";
11
+ import type { AxAI } from "@ax-llm/ax";
12
+ import type {
13
+ ACEConfig,
14
+ ACEPersistenceConfig,
15
+ ACEMetricConfig,
16
+ ACETeacherConfig,
17
+ FunctionRegistryType
18
+ } from "../types.js";
19
+
20
+ // Re-export types for convenience
21
+ export type { AxACE, AxMetricFn };
22
+
23
+ /**
24
+ * Create an empty playbook structure
25
+ */
26
+ export const createEmptyPlaybook = (): ACEPlaybook => {
27
+ const now = new Date().toISOString();
28
+ return {
29
+ version: 1,
30
+ sections: {},
31
+ stats: {
32
+ bulletCount: 0,
33
+ helpfulCount: 0,
34
+ harmfulCount: 0,
35
+ tokenEstimate: 0,
36
+ },
37
+ updatedAt: now,
38
+ };
39
+ };
40
+
41
+ /**
42
+ * Playbook types (mirroring AxACEPlaybook structure)
43
+ */
44
+ export interface ACEBullet {
45
+ id: string;
46
+ section: string;
47
+ content: string;
48
+ helpfulCount: number;
49
+ harmfulCount: number;
50
+ createdAt: string;
51
+ updatedAt: string;
52
+ metadata?: Record<string, unknown>;
53
+ }
54
+
55
+ export interface ACEPlaybook {
56
+ version: number;
57
+ sections: Record<string, ACEBullet[]>;
58
+ stats: {
59
+ bulletCount: number;
60
+ helpfulCount: number;
61
+ harmfulCount: number;
62
+ tokenEstimate: number;
63
+ };
64
+ updatedAt: string;
65
+ description?: string;
66
+ }
67
+
68
+ /**
69
+ * Render a playbook into markdown instruction block for injection into prompts.
70
+ * Mirrors the AxACE renderPlaybook function.
71
+ */
72
+ export const renderPlaybook = (playbook: Readonly<ACEPlaybook>): string => {
73
+ if (!playbook) return '';
74
+
75
+ const sectionsObj = playbook.sections || {};
76
+ const header = playbook.description
77
+ ? `## Context Playbook\n${playbook.description.trim()}\n`
78
+ : '## Context Playbook\n';
79
+
80
+ const sectionEntries = Object.entries(sectionsObj);
81
+ if (sectionEntries.length === 0) return '';
82
+
83
+ const sections = sectionEntries
84
+ .map(([sectionName, bullets]) => {
85
+ const body = bullets
86
+ .map((bullet) => `- [${bullet.id}] ${bullet.content}`)
87
+ .join('\n');
88
+ return body
89
+ ? `### ${sectionName}\n${body}`
90
+ : `### ${sectionName}\n_(empty)_`;
91
+ })
92
+ .join('\n\n');
93
+
94
+ return `${header}\n${sections}`.trim();
95
+ };
96
+
97
+ /**
98
+ * Check if running in Node.js environment (for file operations)
99
+ */
100
+ const isNodeLike = (): boolean => {
101
+ try {
102
+ return typeof process !== "undefined" && !!process.versions?.node;
103
+ } catch {
104
+ return false;
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Read JSON file (Node.js only)
110
+ */
111
+ const readFileJSON = async (path: string): Promise<any | undefined> => {
112
+ if (!isNodeLike()) return undefined;
113
+ try {
114
+ const { readFile } = await import("fs/promises");
115
+ const buf = await readFile(path, "utf-8");
116
+ return JSON.parse(buf);
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ };
121
+
122
+ /**
123
+ * Write JSON file (Node.js only)
124
+ */
125
+ const writeFileJSON = async (path: string, data: any): Promise<void> => {
126
+ if (!isNodeLike()) return;
127
+ try {
128
+ const { mkdir, writeFile } = await import("fs/promises");
129
+ const { dirname } = await import("path");
130
+ await mkdir(dirname(path), { recursive: true });
131
+ await writeFile(path, JSON.stringify(data ?? {}, null, 2), "utf-8");
132
+ } catch {
133
+ // Swallow persistence errors by default
134
+ }
135
+ };
136
+
137
+ /**
138
+ * Resolve environment variable
139
+ */
140
+ const resolveEnv = (name: string): string | undefined => {
141
+ try {
142
+ if (typeof process !== "undefined" && process?.env) {
143
+ return process.env[name];
144
+ }
145
+ return (globalThis as any)?.[name];
146
+ } catch {
147
+ return undefined;
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Build teacher AI instance from config, falling back to student AI
153
+ */
154
+ const buildTeacherAI = (teacherCfg: ACETeacherConfig | undefined, fallback: AxAI): AxAI => {
155
+ if (!teacherCfg) return fallback;
156
+
157
+ const { provider, providerKeyName, apiURL, ai: aiConfig, providerArgs } = teacherCfg;
158
+ if (!provider || !providerKeyName || !aiConfig) return fallback;
159
+
160
+ const apiKey = resolveEnv(providerKeyName) || "";
161
+ if (!apiKey) return fallback;
162
+
163
+ const args: any = {
164
+ name: provider,
165
+ apiKey,
166
+ config: aiConfig,
167
+ options: {}
168
+ };
169
+
170
+ if (apiURL) args.apiURL = apiURL;
171
+ if (providerArgs && typeof providerArgs === "object") {
172
+ Object.assign(args, providerArgs);
173
+ }
174
+
175
+ try {
176
+ return buildAI(args);
177
+ } catch {
178
+ return fallback;
179
+ }
180
+ };
181
+
182
+ /**
183
+ * Build an AxACE optimizer for an agent
184
+ *
185
+ * @param studentAI - The agent's AI instance (used as student)
186
+ * @param cfg - ACE configuration
187
+ * @returns Configured AxACE optimizer
188
+ */
189
+ export const buildACEOptimizer = (studentAI: AxAI, cfg: ACEConfig): AxACE => {
190
+ const teacherAI = buildTeacherAI(cfg.teacher, studentAI);
191
+
192
+ // Build optimizer options, only include initialPlaybook if it has the right structure
193
+ const optimizerOptions: any = {
194
+ maxEpochs: cfg.options?.maxEpochs,
195
+ allowDynamicSections: cfg.options?.allowDynamicSections,
196
+ };
197
+
198
+ // Only pass initialPlaybook if it looks like a valid playbook structure
199
+ if (cfg.persistence?.initialPlaybook &&
200
+ typeof cfg.persistence.initialPlaybook === 'object' &&
201
+ 'sections' in cfg.persistence.initialPlaybook) {
202
+ optimizerOptions.initialPlaybook = cfg.persistence.initialPlaybook;
203
+ }
204
+
205
+ return new AxACE(
206
+ {
207
+ studentAI,
208
+ teacherAI,
209
+ verbose: !!cfg.options?.maxEpochs
210
+ },
211
+ optimizerOptions
212
+ );
213
+ };
214
+
215
+ /**
216
+ * Load initial playbook from file, callback, or inline config
217
+ *
218
+ * @param cfg - Persistence configuration
219
+ * @returns Loaded playbook or undefined
220
+ */
221
+ export const loadInitialPlaybook = async (cfg?: ACEPersistenceConfig): Promise<any | undefined> => {
222
+ if (!cfg) return undefined;
223
+
224
+ // Try callback first
225
+ if (typeof cfg.onLoad === "function") {
226
+ try {
227
+ return await cfg.onLoad();
228
+ } catch {
229
+ // Fall through to other methods
230
+ }
231
+ }
232
+
233
+ // Try inline playbook
234
+ if (cfg.initialPlaybook) {
235
+ return cfg.initialPlaybook;
236
+ }
237
+
238
+ // Try file path
239
+ if (cfg.playbookPath) {
240
+ return await readFileJSON(cfg.playbookPath);
241
+ }
242
+
243
+ return undefined;
244
+ };
245
+
246
+ /**
247
+ * Persist playbook to file or via callback
248
+ *
249
+ * @param pb - Playbook to persist
250
+ * @param cfg - Persistence configuration
251
+ */
252
+ export const persistPlaybook = async (pb: any, cfg?: ACEPersistenceConfig): Promise<void> => {
253
+ if (!cfg || !pb) return;
254
+
255
+ // Call persist callback if provided
256
+ if (typeof cfg.onPersist === "function") {
257
+ try {
258
+ await cfg.onPersist(pb);
259
+ } catch {
260
+ // Ignore callback errors
261
+ }
262
+ }
263
+
264
+ // Write to file if auto-persist enabled
265
+ if (cfg.autoPersist && cfg.playbookPath) {
266
+ await writeFileJSON(cfg.playbookPath, pb);
267
+ }
268
+ };
269
+
270
+ /**
271
+ * Resolve metric function from registry or create equality-based metric
272
+ *
273
+ * @param cfg - Metric configuration
274
+ * @param registry - Function registry to search
275
+ * @returns Metric function or undefined
276
+ */
277
+ export const resolveMetric = (
278
+ cfg: ACEMetricConfig | undefined,
279
+ registry: FunctionRegistryType
280
+ ): AxMetricFn | undefined => {
281
+ if (!cfg) return undefined;
282
+
283
+ const { metricFnName, primaryOutputField } = cfg;
284
+
285
+ // Try to find a function by name in the registry
286
+ if (metricFnName) {
287
+ const candidate = (registry as any)[metricFnName];
288
+ if (typeof candidate === "function") {
289
+ return candidate as AxMetricFn;
290
+ }
291
+ }
292
+
293
+ // Create simple equality-based metric if primary output field specified
294
+ if (primaryOutputField) {
295
+ const field = primaryOutputField;
296
+ return ({ prediction, example }: { prediction: any; example: any }) => {
297
+ try {
298
+ return prediction?.[field] === example?.[field] ? 1 : 0;
299
+ } catch {
300
+ return 0;
301
+ }
302
+ };
303
+ }
304
+
305
+ return undefined;
306
+ };
307
+
308
+ /**
309
+ * Run offline ACE compilation
310
+ *
311
+ * @param args - Compilation arguments
312
+ * @returns Compilation result with optimized program
313
+ */
314
+ export const runOfflineCompile = async (args: {
315
+ program: any;
316
+ optimizer: AxACE;
317
+ metric: AxMetricFn;
318
+ examples: any[];
319
+ persistence?: ACEPersistenceConfig;
320
+ }): Promise<any> => {
321
+ const { program, optimizer, metric, examples = [], persistence } = args;
322
+
323
+ if (!optimizer || !metric || examples.length === 0) {
324
+ return null;
325
+ }
326
+
327
+ try {
328
+ // Run compilation
329
+ const result = await optimizer.compile(program, examples, metric);
330
+
331
+ // Extract and persist playbook
332
+ const playbook = result?.artifact?.playbook;
333
+ if (playbook && persistence) {
334
+ await persistPlaybook(playbook, persistence);
335
+ }
336
+
337
+ return result;
338
+ } catch (error) {
339
+ console.warn("ACE offline compile failed:", error);
340
+ return null;
341
+ }
342
+ };
343
+
344
+ /**
345
+ * Apply online update with feedback
346
+ *
347
+ * @param args - Update arguments
348
+ * @returns Curator delta (operations applied)
349
+ */
350
+ export const runOnlineUpdate = async (args: {
351
+ optimizer: AxACE;
352
+ example: any;
353
+ prediction: any;
354
+ feedback?: string;
355
+ persistence?: ACEPersistenceConfig;
356
+ tokenBudget?: number; // Reserved for future use
357
+ debug?: boolean;
358
+ }): Promise<any> => {
359
+ const { optimizer, example, prediction, feedback, persistence, debug } = args;
360
+
361
+ if (!optimizer) return null;
362
+
363
+ try {
364
+ // Apply online update (per ACE API: example, prediction, feedback)
365
+ const curatorDelta = await optimizer.applyOnlineUpdate({
366
+ example,
367
+ prediction,
368
+ feedback
369
+ });
370
+
371
+ // Access the optimizer's private playbook property
372
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
+ const playbook = (optimizer as any).playbook;
374
+
375
+ // Persist updated playbook if we have one and persistence is configured
376
+ if (playbook && persistence?.autoPersist) {
377
+ await persistPlaybook(playbook, persistence);
378
+ }
379
+
380
+ return curatorDelta;
381
+ } catch (error) {
382
+ // AxACE's reflector sometimes returns bulletTags in non-array format, causing iteration errors.
383
+ // This is a known issue - we fall back to direct playbook updates via addFeedbackToPlaybook.
384
+ if (debug) {
385
+ console.warn("[ACE Debug] AxACE applyOnlineUpdate failed (falling back to direct update):", error);
386
+ }
387
+ return null;
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Generate a unique bullet ID (mirrors AxACE's generateBulletId)
393
+ */
394
+ const generateBulletId = (section: string): string => {
395
+ const normalized = section
396
+ .toLowerCase()
397
+ .replace(/[^a-z0-9]+/g, '-')
398
+ .replace(/^-+|-+$/g, '')
399
+ .slice(0, 6);
400
+ const randomHex = Math.random().toString(16).slice(2, 10);
401
+ return `${normalized || 'ctx'}-${randomHex}`;
402
+ };
403
+
404
+ /**
405
+ * Recompute playbook stats after modifications
406
+ */
407
+ const recomputePlaybookStats = (playbook: ACEPlaybook): void => {
408
+ let bulletCount = 0;
409
+ let helpfulCount = 0;
410
+ let harmfulCount = 0;
411
+ let tokenEstimate = 0;
412
+
413
+ const sections = playbook.sections || {};
414
+ for (const bullets of Object.values(sections)) {
415
+ for (const bullet of bullets) {
416
+ bulletCount += 1;
417
+ helpfulCount += bullet.helpfulCount;
418
+ harmfulCount += bullet.harmfulCount;
419
+ tokenEstimate += Math.ceil(bullet.content.length / 4);
420
+ }
421
+ }
422
+
423
+ playbook.stats = { bulletCount, helpfulCount, harmfulCount, tokenEstimate };
424
+ playbook.updatedAt = new Date().toISOString();
425
+ };
426
+
427
+ /**
428
+ * Apply curator operations to playbook (mirrors AxACE's applyCuratorOperations)
429
+ */
430
+ const applyCuratorOperations = (
431
+ playbook: ACEPlaybook,
432
+ operations: Array<{ type: 'ADD' | 'UPDATE' | 'REMOVE'; section: string; content?: string; bulletId?: string }>
433
+ ): void => {
434
+ // Ensure playbook has sections initialized
435
+ if (!playbook.sections) {
436
+ playbook.sections = {};
437
+ }
438
+
439
+ const now = new Date().toISOString();
440
+
441
+ for (const op of operations) {
442
+ if (!op.section) continue;
443
+
444
+ // Initialize section if needed
445
+ if (!playbook.sections[op.section]) {
446
+ playbook.sections[op.section] = [];
447
+ }
448
+
449
+ const section = playbook.sections[op.section]!;
450
+
451
+ switch (op.type) {
452
+ case 'ADD': {
453
+ if (!op.content?.trim()) continue;
454
+
455
+ // Check for duplicates
456
+ const isDuplicate = section.some(
457
+ b => b.content.toLowerCase() === op.content!.toLowerCase()
458
+ );
459
+ if (isDuplicate) continue;
460
+
461
+ const bullet: ACEBullet = {
462
+ id: op.bulletId || generateBulletId(op.section),
463
+ section: op.section,
464
+ content: op.content.trim(),
465
+ helpfulCount: 1,
466
+ harmfulCount: 0,
467
+ createdAt: now,
468
+ updatedAt: now,
469
+ };
470
+ section.push(bullet);
471
+ break;
472
+ }
473
+ case 'UPDATE': {
474
+ if (!op.bulletId) continue;
475
+ const bullet = section.find(b => b.id === op.bulletId);
476
+ if (bullet && op.content) {
477
+ bullet.content = op.content.trim();
478
+ bullet.updatedAt = now;
479
+ }
480
+ break;
481
+ }
482
+ case 'REMOVE': {
483
+ if (!op.bulletId) continue;
484
+ const idx = section.findIndex(b => b.id === op.bulletId);
485
+ if (idx >= 0) section.splice(idx, 1);
486
+ break;
487
+ }
488
+ }
489
+ }
490
+
491
+ recomputePlaybookStats(playbook);
492
+ };
493
+
494
+ // Cached feedback analyzer program (created lazily)
495
+ let feedbackAnalyzerProgram: AxGen<any, any> | null = null;
496
+
497
+ /**
498
+ * Get or create the feedback analyzer program.
499
+ * Uses AxGen with a proper signature, just like AxACE's reflector/curator.
500
+ *
501
+ * Uses `class` type for section to get type-safe enums and better token efficiency.
502
+ * See: https://axllm.dev/signatures/
503
+ */
504
+ const getOrCreateFeedbackAnalyzer = (): AxGen<any, any> => {
505
+ if (!feedbackAnalyzerProgram) {
506
+ const signature = new AxSignature(
507
+ `feedback:string "User feedback to analyze"
508
+ ->
509
+ section:class "Guidelines, Response Strategies, Common Pitfalls, Root Cause Notes" "Playbook section category",
510
+ content:string "The specific instruction to add to the playbook - keep all concrete details"`
511
+ );
512
+
513
+ signature.setDescription(
514
+ `Convert user feedback into a playbook instruction. Keep ALL specific details from the feedback (times, names, numbers, constraints).`
515
+ );
516
+
517
+ feedbackAnalyzerProgram = new AxGen(signature);
518
+ }
519
+ return feedbackAnalyzerProgram;
520
+ };
521
+
522
+ /**
523
+ * Use LLM to analyze feedback and generate playbook operations.
524
+ *
525
+ * This leverages AxGen with a proper signature (like AxACE's reflector/curator)
526
+ * to properly categorize feedback and extract actionable insights.
527
+ *
528
+ * IMPORTANT: The prompt explicitly tells the LLM to preserve specificity.
529
+ *
530
+ * @param ai - The AI instance to use for analysis
531
+ * @param feedback - User feedback string
532
+ * @param debug - Whether to log debug info
533
+ * @returns Promise of curator operations
534
+ */
535
+ export const analyzeAndCategorizeFeedback = async (
536
+ ai: AxAI,
537
+ feedback: string,
538
+ debug = false
539
+ ): Promise<Array<{ type: 'ADD' | 'UPDATE' | 'REMOVE'; section: string; content: string }>> => {
540
+ if (!feedback?.trim()) return [];
541
+
542
+ try {
543
+ const analyzer = getOrCreateFeedbackAnalyzer();
544
+
545
+ const result = await analyzer.forward(ai, {
546
+ feedback: feedback.trim(),
547
+ });
548
+
549
+ if (debug) {
550
+ console.log('[ACE Debug] Feedback analysis result:', result);
551
+ }
552
+
553
+ // Section is guaranteed to be valid by the class type constraint
554
+ const section = result.section || 'Guidelines';
555
+ // Use the LLM's content, but fall back to raw feedback if empty
556
+ const content = result.content?.trim() || feedback.trim();
557
+
558
+ return [{ type: 'ADD', section, content }];
559
+ } catch (error) {
560
+ if (debug) {
561
+ console.warn('[ACE Debug] Feedback analysis failed, using raw feedback:', error);
562
+ }
563
+ // Fallback: use the raw feedback as-is
564
+ return [{ type: 'ADD', section: 'Guidelines', content: feedback.trim() }];
565
+ }
566
+ };
567
+
568
+ /**
569
+ * Add feedback to playbook using LLM analysis.
570
+ *
571
+ * Uses the AI to properly understand and categorize the feedback,
572
+ * then applies it as a curator operation.
573
+ *
574
+ * @param playbook - The playbook to update (mutated in place)
575
+ * @param feedback - User feedback string to add
576
+ * @param ai - AI instance for smart categorization
577
+ * @param debug - Whether to log debug info
578
+ */
579
+ export const addFeedbackToPlaybook = async (
580
+ playbook: ACEPlaybook,
581
+ feedback: string,
582
+ ai: AxAI,
583
+ debug = false
584
+ ): Promise<void> => {
585
+ if (!playbook || !feedback?.trim()) return;
586
+
587
+ // Use LLM to categorize feedback while preserving specificity
588
+ const operations = await analyzeAndCategorizeFeedback(ai, feedback, debug);
589
+
590
+ if (operations.length > 0) {
591
+ applyCuratorOperations(playbook, operations);
592
+ }
593
+ };
594
+
@@ -7,6 +7,7 @@ import { AxMCPStdioTransport } from '@ax-llm/ax-tools'
7
7
  import type {
8
8
  AgentConfig,
9
9
  AxCrewConfig,
10
+ AxCrewOptions,
10
11
  FunctionRegistryType,
11
12
  MCPTransportConfig,
12
13
  MCPStdioTransportConfig,
@@ -112,7 +113,8 @@ const parseAgentConfig = async (
112
113
  agentName: string,
113
114
  crewConfig: AxCrewConfig,
114
115
  functions: FunctionRegistryType,
115
- state: Record<string, any>
116
+ state: Record<string, any>,
117
+ options?: AxCrewOptions
116
118
  ) => {
117
119
  try {
118
120
  // Retrieve the parameters for the specified AI agent from config
@@ -149,7 +151,10 @@ const parseAgentConfig = async (
149
151
  debug: agentConfigData.debug || false,
150
152
  ...agentConfigData.options,
151
153
  // Attach default cost tracker so usage/costs are recorded by provider layer
152
- trackers: [costTracker]
154
+ trackers: [costTracker],
155
+ // Inject telemetry if provided
156
+ tracer: options?.telemetry?.tracer,
157
+ meter: options?.telemetry?.meter
153
158
  }
154
159
  };
155
160
  if (agentConfigData.apiURL) {
@@ -205,6 +210,7 @@ const parseAgentConfig = async (
205
210
  subAgentNames: agentConfigData.agents || [],
206
211
  examples: agentConfigData.examples || [],
207
212
  tracker: costTracker,
213
+ debug: (agentConfigData as any).options?.debug ?? (agentConfigData as any).debug ?? false,
208
214
  };
209
215
  } catch (error) {
210
216
  if (error instanceof Error) {