@amitdeshmukh/ax-crew 7.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,477 @@
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
+ import { AxACE, ai as buildAI, AxSignature, AxGen } from "@ax-llm/ax";
10
+ /**
11
+ * Create an empty playbook structure
12
+ */
13
+ export const createEmptyPlaybook = () => {
14
+ const now = new Date().toISOString();
15
+ return {
16
+ version: 1,
17
+ sections: {},
18
+ stats: {
19
+ bulletCount: 0,
20
+ helpfulCount: 0,
21
+ harmfulCount: 0,
22
+ tokenEstimate: 0,
23
+ },
24
+ updatedAt: now,
25
+ };
26
+ };
27
+ /**
28
+ * Render a playbook into markdown instruction block for injection into prompts.
29
+ * Mirrors the AxACE renderPlaybook function.
30
+ */
31
+ export const renderPlaybook = (playbook) => {
32
+ if (!playbook)
33
+ return '';
34
+ const sectionsObj = playbook.sections || {};
35
+ const header = playbook.description
36
+ ? `## Context Playbook\n${playbook.description.trim()}\n`
37
+ : '## Context Playbook\n';
38
+ const sectionEntries = Object.entries(sectionsObj);
39
+ if (sectionEntries.length === 0)
40
+ return '';
41
+ const sections = sectionEntries
42
+ .map(([sectionName, bullets]) => {
43
+ const body = bullets
44
+ .map((bullet) => `- [${bullet.id}] ${bullet.content}`)
45
+ .join('\n');
46
+ return body
47
+ ? `### ${sectionName}\n${body}`
48
+ : `### ${sectionName}\n_(empty)_`;
49
+ })
50
+ .join('\n\n');
51
+ return `${header}\n${sections}`.trim();
52
+ };
53
+ /**
54
+ * Check if running in Node.js environment (for file operations)
55
+ */
56
+ const isNodeLike = () => {
57
+ try {
58
+ return typeof process !== "undefined" && !!process.versions?.node;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ };
64
+ /**
65
+ * Read JSON file (Node.js only)
66
+ */
67
+ const readFileJSON = async (path) => {
68
+ if (!isNodeLike())
69
+ return undefined;
70
+ try {
71
+ const { readFile } = await import("fs/promises");
72
+ const buf = await readFile(path, "utf-8");
73
+ return JSON.parse(buf);
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ };
79
+ /**
80
+ * Write JSON file (Node.js only)
81
+ */
82
+ const writeFileJSON = async (path, data) => {
83
+ if (!isNodeLike())
84
+ return;
85
+ try {
86
+ const { mkdir, writeFile } = await import("fs/promises");
87
+ const { dirname } = await import("path");
88
+ await mkdir(dirname(path), { recursive: true });
89
+ await writeFile(path, JSON.stringify(data ?? {}, null, 2), "utf-8");
90
+ }
91
+ catch {
92
+ // Swallow persistence errors by default
93
+ }
94
+ };
95
+ /**
96
+ * Resolve environment variable
97
+ */
98
+ const resolveEnv = (name) => {
99
+ try {
100
+ if (typeof process !== "undefined" && process?.env) {
101
+ return process.env[name];
102
+ }
103
+ return globalThis?.[name];
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ };
109
+ /**
110
+ * Build teacher AI instance from config, falling back to student AI
111
+ */
112
+ const buildTeacherAI = (teacherCfg, fallback) => {
113
+ if (!teacherCfg)
114
+ return fallback;
115
+ const { provider, providerKeyName, apiURL, ai: aiConfig, providerArgs } = teacherCfg;
116
+ if (!provider || !providerKeyName || !aiConfig)
117
+ return fallback;
118
+ const apiKey = resolveEnv(providerKeyName) || "";
119
+ if (!apiKey)
120
+ return fallback;
121
+ const args = {
122
+ name: provider,
123
+ apiKey,
124
+ config: aiConfig,
125
+ options: {}
126
+ };
127
+ if (apiURL)
128
+ args.apiURL = apiURL;
129
+ if (providerArgs && typeof providerArgs === "object") {
130
+ Object.assign(args, providerArgs);
131
+ }
132
+ try {
133
+ return buildAI(args);
134
+ }
135
+ catch {
136
+ return fallback;
137
+ }
138
+ };
139
+ /**
140
+ * Build an AxACE optimizer for an agent
141
+ *
142
+ * @param studentAI - The agent's AI instance (used as student)
143
+ * @param cfg - ACE configuration
144
+ * @returns Configured AxACE optimizer
145
+ */
146
+ export const buildACEOptimizer = (studentAI, cfg) => {
147
+ const teacherAI = buildTeacherAI(cfg.teacher, studentAI);
148
+ // Build optimizer options, only include initialPlaybook if it has the right structure
149
+ const optimizerOptions = {
150
+ maxEpochs: cfg.options?.maxEpochs,
151
+ allowDynamicSections: cfg.options?.allowDynamicSections,
152
+ };
153
+ // Only pass initialPlaybook if it looks like a valid playbook structure
154
+ if (cfg.persistence?.initialPlaybook &&
155
+ typeof cfg.persistence.initialPlaybook === 'object' &&
156
+ 'sections' in cfg.persistence.initialPlaybook) {
157
+ optimizerOptions.initialPlaybook = cfg.persistence.initialPlaybook;
158
+ }
159
+ return new AxACE({
160
+ studentAI,
161
+ teacherAI,
162
+ verbose: !!cfg.options?.maxEpochs
163
+ }, optimizerOptions);
164
+ };
165
+ /**
166
+ * Load initial playbook from file, callback, or inline config
167
+ *
168
+ * @param cfg - Persistence configuration
169
+ * @returns Loaded playbook or undefined
170
+ */
171
+ export const loadInitialPlaybook = async (cfg) => {
172
+ if (!cfg)
173
+ return undefined;
174
+ // Try callback first
175
+ if (typeof cfg.onLoad === "function") {
176
+ try {
177
+ return await cfg.onLoad();
178
+ }
179
+ catch {
180
+ // Fall through to other methods
181
+ }
182
+ }
183
+ // Try inline playbook
184
+ if (cfg.initialPlaybook) {
185
+ return cfg.initialPlaybook;
186
+ }
187
+ // Try file path
188
+ if (cfg.playbookPath) {
189
+ return await readFileJSON(cfg.playbookPath);
190
+ }
191
+ return undefined;
192
+ };
193
+ /**
194
+ * Persist playbook to file or via callback
195
+ *
196
+ * @param pb - Playbook to persist
197
+ * @param cfg - Persistence configuration
198
+ */
199
+ export const persistPlaybook = async (pb, cfg) => {
200
+ if (!cfg || !pb)
201
+ return;
202
+ // Call persist callback if provided
203
+ if (typeof cfg.onPersist === "function") {
204
+ try {
205
+ await cfg.onPersist(pb);
206
+ }
207
+ catch {
208
+ // Ignore callback errors
209
+ }
210
+ }
211
+ // Write to file if auto-persist enabled
212
+ if (cfg.autoPersist && cfg.playbookPath) {
213
+ await writeFileJSON(cfg.playbookPath, pb);
214
+ }
215
+ };
216
+ /**
217
+ * Resolve metric function from registry or create equality-based metric
218
+ *
219
+ * @param cfg - Metric configuration
220
+ * @param registry - Function registry to search
221
+ * @returns Metric function or undefined
222
+ */
223
+ export const resolveMetric = (cfg, registry) => {
224
+ if (!cfg)
225
+ return undefined;
226
+ const { metricFnName, primaryOutputField } = cfg;
227
+ // Try to find a function by name in the registry
228
+ if (metricFnName) {
229
+ const candidate = registry[metricFnName];
230
+ if (typeof candidate === "function") {
231
+ return candidate;
232
+ }
233
+ }
234
+ // Create simple equality-based metric if primary output field specified
235
+ if (primaryOutputField) {
236
+ const field = primaryOutputField;
237
+ return ({ prediction, example }) => {
238
+ try {
239
+ return prediction?.[field] === example?.[field] ? 1 : 0;
240
+ }
241
+ catch {
242
+ return 0;
243
+ }
244
+ };
245
+ }
246
+ return undefined;
247
+ };
248
+ /**
249
+ * Run offline ACE compilation
250
+ *
251
+ * @param args - Compilation arguments
252
+ * @returns Compilation result with optimized program
253
+ */
254
+ export const runOfflineCompile = async (args) => {
255
+ const { program, optimizer, metric, examples = [], persistence } = args;
256
+ if (!optimizer || !metric || examples.length === 0) {
257
+ return null;
258
+ }
259
+ try {
260
+ // Run compilation
261
+ const result = await optimizer.compile(program, examples, metric);
262
+ // Extract and persist playbook
263
+ const playbook = result?.artifact?.playbook;
264
+ if (playbook && persistence) {
265
+ await persistPlaybook(playbook, persistence);
266
+ }
267
+ return result;
268
+ }
269
+ catch (error) {
270
+ console.warn("ACE offline compile failed:", error);
271
+ return null;
272
+ }
273
+ };
274
+ /**
275
+ * Apply online update with feedback
276
+ *
277
+ * @param args - Update arguments
278
+ * @returns Curator delta (operations applied)
279
+ */
280
+ export const runOnlineUpdate = async (args) => {
281
+ const { optimizer, example, prediction, feedback, persistence, debug } = args;
282
+ if (!optimizer)
283
+ return null;
284
+ try {
285
+ // Apply online update (per ACE API: example, prediction, feedback)
286
+ const curatorDelta = await optimizer.applyOnlineUpdate({
287
+ example,
288
+ prediction,
289
+ feedback
290
+ });
291
+ // Access the optimizer's private playbook property
292
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
+ const playbook = optimizer.playbook;
294
+ // Persist updated playbook if we have one and persistence is configured
295
+ if (playbook && persistence?.autoPersist) {
296
+ await persistPlaybook(playbook, persistence);
297
+ }
298
+ return curatorDelta;
299
+ }
300
+ catch (error) {
301
+ // AxACE's reflector sometimes returns bulletTags in non-array format, causing iteration errors.
302
+ // This is a known issue - we fall back to direct playbook updates via addFeedbackToPlaybook.
303
+ if (debug) {
304
+ console.warn("[ACE Debug] AxACE applyOnlineUpdate failed (falling back to direct update):", error);
305
+ }
306
+ return null;
307
+ }
308
+ };
309
+ /**
310
+ * Generate a unique bullet ID (mirrors AxACE's generateBulletId)
311
+ */
312
+ const generateBulletId = (section) => {
313
+ const normalized = section
314
+ .toLowerCase()
315
+ .replace(/[^a-z0-9]+/g, '-')
316
+ .replace(/^-+|-+$/g, '')
317
+ .slice(0, 6);
318
+ const randomHex = Math.random().toString(16).slice(2, 10);
319
+ return `${normalized || 'ctx'}-${randomHex}`;
320
+ };
321
+ /**
322
+ * Recompute playbook stats after modifications
323
+ */
324
+ const recomputePlaybookStats = (playbook) => {
325
+ let bulletCount = 0;
326
+ let helpfulCount = 0;
327
+ let harmfulCount = 0;
328
+ let tokenEstimate = 0;
329
+ const sections = playbook.sections || {};
330
+ for (const bullets of Object.values(sections)) {
331
+ for (const bullet of bullets) {
332
+ bulletCount += 1;
333
+ helpfulCount += bullet.helpfulCount;
334
+ harmfulCount += bullet.harmfulCount;
335
+ tokenEstimate += Math.ceil(bullet.content.length / 4);
336
+ }
337
+ }
338
+ playbook.stats = { bulletCount, helpfulCount, harmfulCount, tokenEstimate };
339
+ playbook.updatedAt = new Date().toISOString();
340
+ };
341
+ /**
342
+ * Apply curator operations to playbook (mirrors AxACE's applyCuratorOperations)
343
+ */
344
+ const applyCuratorOperations = (playbook, operations) => {
345
+ // Ensure playbook has sections initialized
346
+ if (!playbook.sections) {
347
+ playbook.sections = {};
348
+ }
349
+ const now = new Date().toISOString();
350
+ for (const op of operations) {
351
+ if (!op.section)
352
+ continue;
353
+ // Initialize section if needed
354
+ if (!playbook.sections[op.section]) {
355
+ playbook.sections[op.section] = [];
356
+ }
357
+ const section = playbook.sections[op.section];
358
+ switch (op.type) {
359
+ case 'ADD': {
360
+ if (!op.content?.trim())
361
+ continue;
362
+ // Check for duplicates
363
+ const isDuplicate = section.some(b => b.content.toLowerCase() === op.content.toLowerCase());
364
+ if (isDuplicate)
365
+ continue;
366
+ const bullet = {
367
+ id: op.bulletId || generateBulletId(op.section),
368
+ section: op.section,
369
+ content: op.content.trim(),
370
+ helpfulCount: 1,
371
+ harmfulCount: 0,
372
+ createdAt: now,
373
+ updatedAt: now,
374
+ };
375
+ section.push(bullet);
376
+ break;
377
+ }
378
+ case 'UPDATE': {
379
+ if (!op.bulletId)
380
+ continue;
381
+ const bullet = section.find(b => b.id === op.bulletId);
382
+ if (bullet && op.content) {
383
+ bullet.content = op.content.trim();
384
+ bullet.updatedAt = now;
385
+ }
386
+ break;
387
+ }
388
+ case 'REMOVE': {
389
+ if (!op.bulletId)
390
+ continue;
391
+ const idx = section.findIndex(b => b.id === op.bulletId);
392
+ if (idx >= 0)
393
+ section.splice(idx, 1);
394
+ break;
395
+ }
396
+ }
397
+ }
398
+ recomputePlaybookStats(playbook);
399
+ };
400
+ // Cached feedback analyzer program (created lazily)
401
+ let feedbackAnalyzerProgram = null;
402
+ /**
403
+ * Get or create the feedback analyzer program.
404
+ * Uses AxGen with a proper signature, just like AxACE's reflector/curator.
405
+ *
406
+ * Uses `class` type for section to get type-safe enums and better token efficiency.
407
+ * See: https://axllm.dev/signatures/
408
+ */
409
+ const getOrCreateFeedbackAnalyzer = () => {
410
+ if (!feedbackAnalyzerProgram) {
411
+ const signature = new AxSignature(`feedback:string "User feedback to analyze"
412
+ ->
413
+ section:class "Guidelines, Response Strategies, Common Pitfalls, Root Cause Notes" "Playbook section category",
414
+ content:string "The specific instruction to add to the playbook - keep all concrete details"`);
415
+ signature.setDescription(`Convert user feedback into a playbook instruction. Keep ALL specific details from the feedback (times, names, numbers, constraints).`);
416
+ feedbackAnalyzerProgram = new AxGen(signature);
417
+ }
418
+ return feedbackAnalyzerProgram;
419
+ };
420
+ /**
421
+ * Use LLM to analyze feedback and generate playbook operations.
422
+ *
423
+ * This leverages AxGen with a proper signature (like AxACE's reflector/curator)
424
+ * to properly categorize feedback and extract actionable insights.
425
+ *
426
+ * IMPORTANT: The prompt explicitly tells the LLM to preserve specificity.
427
+ *
428
+ * @param ai - The AI instance to use for analysis
429
+ * @param feedback - User feedback string
430
+ * @param debug - Whether to log debug info
431
+ * @returns Promise of curator operations
432
+ */
433
+ export const analyzeAndCategorizeFeedback = async (ai, feedback, debug = false) => {
434
+ if (!feedback?.trim())
435
+ return [];
436
+ try {
437
+ const analyzer = getOrCreateFeedbackAnalyzer();
438
+ const result = await analyzer.forward(ai, {
439
+ feedback: feedback.trim(),
440
+ });
441
+ if (debug) {
442
+ console.log('[ACE Debug] Feedback analysis result:', result);
443
+ }
444
+ // Section is guaranteed to be valid by the class type constraint
445
+ const section = result.section || 'Guidelines';
446
+ // Use the LLM's content, but fall back to raw feedback if empty
447
+ const content = result.content?.trim() || feedback.trim();
448
+ return [{ type: 'ADD', section, content }];
449
+ }
450
+ catch (error) {
451
+ if (debug) {
452
+ console.warn('[ACE Debug] Feedback analysis failed, using raw feedback:', error);
453
+ }
454
+ // Fallback: use the raw feedback as-is
455
+ return [{ type: 'ADD', section: 'Guidelines', content: feedback.trim() }];
456
+ }
457
+ };
458
+ /**
459
+ * Add feedback to playbook using LLM analysis.
460
+ *
461
+ * Uses the AI to properly understand and categorize the feedback,
462
+ * then applies it as a curator operation.
463
+ *
464
+ * @param playbook - The playbook to update (mutated in place)
465
+ * @param feedback - User feedback string to add
466
+ * @param ai - AI instance for smart categorization
467
+ * @param debug - Whether to log debug info
468
+ */
469
+ export const addFeedbackToPlaybook = async (playbook, feedback, ai, debug = false) => {
470
+ if (!playbook || !feedback?.trim())
471
+ return;
472
+ // Use LLM to categorize feedback while preserving specificity
473
+ const operations = await analyzeAndCategorizeFeedback(ai, feedback, debug);
474
+ if (operations.length > 0) {
475
+ applyCuratorOperations(playbook, operations);
476
+ }
477
+ };
@@ -35,5 +35,6 @@ declare const parseAgentConfig: (agentName: string, crewConfig: AxCrewConfig, fu
35
35
  subAgentNames: string[];
36
36
  examples: Record<string, any>[];
37
37
  tracker: AxDefaultCostTracker;
38
+ debug: any;
38
39
  }>;
39
40
  export { parseAgentConfig, parseCrewConfig };
@@ -179,6 +179,7 @@ const parseAgentConfig = async (agentName, crewConfig, functions, state, options
179
179
  subAgentNames: agentConfigData.agents || [],
180
180
  examples: agentConfigData.examples || [],
181
181
  tracker: costTracker,
182
+ debug: agentConfigData.options?.debug ?? agentConfigData.debug ?? false,
182
183
  };
183
184
  }
184
185
  catch (error) {
@@ -1,12 +1,17 @@
1
1
  import { AxAgent, AxAI } from "@ax-llm/ax";
2
2
  import type { AxSignature, AxAgentic, AxFunction, AxProgramForwardOptions, AxProgramStreamingForwardOptions, AxGenStreamingOut } from "@ax-llm/ax";
3
- import type { StateInstance, FunctionRegistryType, UsageCost, AxCrewConfig, AxCrewOptions, MCPTransportConfig } from "../types.js";
3
+ import type { StateInstance, FunctionRegistryType, UsageCost, AxCrewConfig, AxCrewOptions, MCPTransportConfig, ACEConfig } from "../types.js";
4
4
  declare class StatefulAxAgent extends AxAgent<any, any> {
5
5
  state: StateInstance;
6
6
  axai: any;
7
7
  private agentName;
8
8
  private costTracker?;
9
9
  private lastRecordedCostUSD;
10
+ private debugEnabled;
11
+ private aceConfig?;
12
+ private aceOptimizer?;
13
+ private acePlaybook?;
14
+ private aceBaseInstruction?;
10
15
  private isAxAIService;
11
16
  private isAxAIInstance;
12
17
  constructor(ai: AxAI, options: Readonly<{
@@ -18,6 +23,7 @@ declare class StatefulAxAgent extends AxAgent<any, any> {
18
23
  functions?: (AxFunction | (() => AxFunction))[] | undefined;
19
24
  examples?: Array<Record<string, any>> | undefined;
20
25
  mcpServers?: Record<string, MCPTransportConfig> | undefined;
26
+ debug?: boolean;
21
27
  }>, state: StateInstance);
22
28
  forward(values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
23
29
  forward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
@@ -37,6 +43,51 @@ declare class StatefulAxAgent extends AxAgent<any, any> {
37
43
  * Call this to start fresh measurement windows for the agent.
38
44
  */
39
45
  resetMetrics(): void;
46
+ /**
47
+ * Initialize ACE (Agentic Context Engineering) for this agent.
48
+ * Builds the optimizer and loads any initial playbook from persistence.
49
+ * Sets up the optimizer for online-only mode if compileOnStart is false.
50
+ */
51
+ initACE(ace?: ACEConfig): Promise<void>;
52
+ /**
53
+ * Run offline ACE compilation with examples and metric.
54
+ * Compiles the playbook based on training examples.
55
+ */
56
+ optimizeOffline(params?: {
57
+ metric?: any;
58
+ examples?: any[];
59
+ }): Promise<void>;
60
+ /**
61
+ * Apply online ACE update based on user feedback.
62
+ *
63
+ * For preference-based feedback (e.g., "only show flights between 9am-12pm"),
64
+ * we use our own feedback analyzer that preserves specificity.
65
+ *
66
+ * Note: AxACE's built-in curator is designed for error correction (severity mismatches)
67
+ * and tends to over-abstract preference feedback into generic guidelines.
68
+ * We bypass it and directly use our feedback analyzer for better results.
69
+ */
70
+ applyOnlineUpdate(params: {
71
+ example: any;
72
+ prediction: any;
73
+ feedback?: string;
74
+ }): Promise<void>;
75
+ /**
76
+ * Get the current ACE playbook for this agent.
77
+ */
78
+ getPlaybook(): any | undefined;
79
+ /**
80
+ * Apply an ACE playbook to this agent.
81
+ * Stores the playbook for use in next forward() call.
82
+ * Note: Playbook is composed into instruction BEFORE each forward(), mirroring AxACE.compile behavior.
83
+ */
84
+ applyPlaybook(pb: any): void;
85
+ /**
86
+ * Compose instruction with current playbook and set on agent.
87
+ * This mirrors what AxACE does internally before each forward() during compile().
88
+ * Should be called BEFORE forward() to ensure playbook is in the prompt.
89
+ */
90
+ private composeInstructionWithPlaybook;
40
91
  }
41
92
  /**
42
93
  * AxCrew orchestrates a set of Ax agents that share state,
@@ -64,6 +115,7 @@ declare class AxCrew {
64
115
  crewId: string;
65
116
  agents: Map<string, StatefulAxAgent> | null;
66
117
  state: StateInstance;
118
+ private executionHistory;
67
119
  /**
68
120
  * Creates an instance of AxCrew.
69
121
  * @param {AxCrewConfig} crewConfig - JSON object with crew configuration.
@@ -93,6 +145,36 @@ declare class AxCrew {
93
145
  */
94
146
  addAgentsToCrew(agentNames: string[]): Promise<Map<string, StatefulAxAgent> | null>;
95
147
  addAllAgents(): Promise<Map<string, StatefulAxAgent> | null>;
148
+ /**
149
+ * Track agent execution for ACE feedback routing
150
+ */
151
+ trackAgentExecution(taskId: string, agentName: string, input: any): void;
152
+ /**
153
+ * Record agent result for ACE feedback routing
154
+ */
155
+ recordAgentResult(taskId: string, agentName: string, result: any): void;
156
+ /**
157
+ * Get agent involvement for a task (used for ACE feedback routing)
158
+ */
159
+ getTaskAgentInvolvement(taskId: string): {
160
+ rootAgent: string;
161
+ involvedAgents: string[];
162
+ taskInput: any;
163
+ agentResults: Map<string, any>;
164
+ duration?: number;
165
+ } | null;
166
+ /**
167
+ * Apply feedback to agents involved in a task for ACE online learning
168
+ */
169
+ applyTaskFeedback(params: {
170
+ taskId: string;
171
+ feedback: string;
172
+ strategy?: 'all' | 'primary' | 'weighted';
173
+ }): Promise<void>;
174
+ /**
175
+ * Clean up old execution history (call periodically to prevent memory leaks)
176
+ */
177
+ cleanupOldExecutions(maxAgeMs?: number): void;
96
178
  /**
97
179
  * Cleans up the crew by dereferencing agents and resetting the state.
98
180
  */