@amitdeshmukh/ax-crew 8.7.3 → 9.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.
- package/CHANGELOG.md +40 -0
- package/README.md +9 -9
- package/dist/agents/agentConfig.d.ts +3 -0
- package/dist/agents/agentConfig.js +12 -4
- package/dist/agents/crew.d.ts +59 -0
- package/dist/agents/crew.js +356 -0
- package/dist/agents/deferredTools.d.ts +49 -0
- package/dist/agents/deferredTools.js +237 -0
- package/dist/agents/index.d.ts +4 -266
- package/dist/agents/index.js +3 -1014
- package/dist/agents/lazyAgent.d.ts +33 -0
- package/dist/agents/lazyAgent.js +78 -0
- package/dist/agents/statefulAgent.d.ts +97 -0
- package/dist/agents/statefulAgent.js +478 -0
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +18 -1
- package/examples/graphjin-database-agent.ts +85 -64
- package/examples/write-post-and-publish-to-wordpress.ts +1 -1
- package/package.json +1 -1
- package/src/agents/agentConfig.ts +15 -8
- package/src/agents/crew.ts +444 -0
- package/src/agents/deferredTools.ts +275 -0
- package/src/agents/index.ts +4 -1281
- package/src/agents/lazyAgent.ts +95 -0
- package/src/agents/statefulAgent.ts +668 -0
- package/src/index.ts +7 -4
- package/src/skills/axcrew-functions.md +2 -2
- package/src/skills/axcrew-mcp.md +41 -1
- package/src/skills/axcrew-patterns.md +48 -4
- package/src/skills/axcrew-state.md +16 -16
- package/src/skills/axcrew.md +14 -1
- package/src/types.ts +19 -0
- package/.claude/settings.local.json +0 -13
- package/.claude/skills/ax-crew/SKILL.md +0 -466
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { AxFunction, AxStepHooks } from '@ax-llm/ax';
|
|
2
|
+
|
|
3
|
+
export interface DeferredToolsConfig {
|
|
4
|
+
/** Enable deferred tool loading. Default: auto (true when tool count > threshold) */
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** Tool count threshold to activate deferred mode. Default: 20 */
|
|
7
|
+
threshold?: number;
|
|
8
|
+
/** Max tools returned per search. Default: 10 */
|
|
9
|
+
maxSearchResults?: number;
|
|
10
|
+
/** Tool names to always keep active (bypasses deferral) */
|
|
11
|
+
coreTools?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_THRESHOLD = 20;
|
|
15
|
+
const DEFAULT_MAX_RESULTS = 10;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages deferred tool loading for agents with many tools.
|
|
19
|
+
*
|
|
20
|
+
* When tool count exceeds a threshold, only core tools + a `search_tools`
|
|
21
|
+
* meta-function are visible to the LLM. The LLM calls `search_tools` to
|
|
22
|
+
* discover and activate deferred tools, which are injected via ax-llm
|
|
23
|
+
* step hooks for subsequent turns.
|
|
24
|
+
*/
|
|
25
|
+
export class DeferredToolManager {
|
|
26
|
+
private registry: Map<string, AxFunction>;
|
|
27
|
+
private deferredNames: Set<string>;
|
|
28
|
+
private activatedNames: Set<string>;
|
|
29
|
+
private coreNames: Set<string>;
|
|
30
|
+
private readonly maxSearchResults: number;
|
|
31
|
+
private readonly _isActive: boolean;
|
|
32
|
+
private resourceCache: Map<string, unknown>;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
allFunctions: readonly AxFunction[],
|
|
36
|
+
mcpFunctionNames: ReadonlySet<string>,
|
|
37
|
+
config?: DeferredToolsConfig
|
|
38
|
+
) {
|
|
39
|
+
const threshold = config?.threshold ?? DEFAULT_THRESHOLD;
|
|
40
|
+
this.maxSearchResults = config?.maxSearchResults ?? DEFAULT_MAX_RESULTS;
|
|
41
|
+
this.activatedNames = new Set();
|
|
42
|
+
this.resourceCache = new Map();
|
|
43
|
+
|
|
44
|
+
// Build full registry
|
|
45
|
+
this.registry = new Map();
|
|
46
|
+
for (const fn of allFunctions) {
|
|
47
|
+
this.registry.set(fn.name, fn);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Determine if we should activate deferred mode
|
|
51
|
+
const explicitEnabled = config?.enabled;
|
|
52
|
+
this._isActive = explicitEnabled !== undefined
|
|
53
|
+
? explicitEnabled
|
|
54
|
+
: allFunctions.length > threshold;
|
|
55
|
+
|
|
56
|
+
// Classify core vs deferred
|
|
57
|
+
const explicitCore = new Set(config?.coreTools ?? []);
|
|
58
|
+
this.coreNames = new Set<string>();
|
|
59
|
+
this.deferredNames = new Set<string>();
|
|
60
|
+
|
|
61
|
+
if (this._isActive) {
|
|
62
|
+
for (const fn of allFunctions) {
|
|
63
|
+
const isMcp = mcpFunctionNames.has(fn.name);
|
|
64
|
+
const isResource = fn.name.startsWith('resource_');
|
|
65
|
+
const isExplicitCore = explicitCore.has(fn.name);
|
|
66
|
+
|
|
67
|
+
if (isExplicitCore || isResource || !isMcp) {
|
|
68
|
+
this.coreNames.add(fn.name);
|
|
69
|
+
} else {
|
|
70
|
+
this.deferredNames.add(fn.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (const fn of allFunctions) {
|
|
75
|
+
this.coreNames.add(fn.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether deferred mode is active */
|
|
81
|
+
get isActive(): boolean {
|
|
82
|
+
return this._isActive;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Get the initial function set (core tools + search_tools) */
|
|
86
|
+
getInitialFunctions(): AxFunction[] {
|
|
87
|
+
const initial: AxFunction[] = [];
|
|
88
|
+
for (const name of this.coreNames) {
|
|
89
|
+
const fn = this.registry.get(name);
|
|
90
|
+
if (fn) initial.push(fn.name.startsWith('resource_') ? this.wrapWithCache(fn) : fn);
|
|
91
|
+
}
|
|
92
|
+
if (this._isActive) {
|
|
93
|
+
initial.push(this.createSearchToolFunction());
|
|
94
|
+
}
|
|
95
|
+
return initial;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Wrap a resource function so repeated calls return cached results */
|
|
99
|
+
private wrapWithCache(fn: AxFunction): AxFunction {
|
|
100
|
+
const cache = this.resourceCache;
|
|
101
|
+
const originalFunc = fn.func;
|
|
102
|
+
if (!originalFunc) return fn;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...fn,
|
|
106
|
+
func: async (args: Record<string, unknown>) => {
|
|
107
|
+
const cached = cache.get(fn.name);
|
|
108
|
+
if (cached !== undefined) return cached;
|
|
109
|
+
const result = await originalFunc(args);
|
|
110
|
+
cache.set(fn.name, result);
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get step hooks for dynamic tool activation.
|
|
118
|
+
* - beforeStep: re-injects previously activated tools at the start of each
|
|
119
|
+
* forward() call so tools discovered in earlier calls persist.
|
|
120
|
+
* - afterFunctionExecution: injects newly discovered tools after search_tools
|
|
121
|
+
* runs, and auto-activates tools mentioned in function results.
|
|
122
|
+
*/
|
|
123
|
+
getStepHooks(): AxStepHooks {
|
|
124
|
+
const injectedNames = new Set<string>();
|
|
125
|
+
|
|
126
|
+
const injectActivated = (ctx: { addFunctions: (fns: AxFunction[]) => void }) => {
|
|
127
|
+
const toInject: AxFunction[] = [];
|
|
128
|
+
for (const name of this.activatedNames) {
|
|
129
|
+
if (!this.coreNames.has(name) && !injectedNames.has(name)) {
|
|
130
|
+
const fn = this.registry.get(name);
|
|
131
|
+
if (fn) {
|
|
132
|
+
toInject.push(fn);
|
|
133
|
+
injectedNames.add(name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (toInject.length > 0) {
|
|
138
|
+
ctx.addFunctions(toInject);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
beforeStep: async (ctx) => {
|
|
144
|
+
if (this.activatedNames.size > 0) {
|
|
145
|
+
injectActivated(ctx as any);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
afterFunctionExecution: async (ctx) => {
|
|
149
|
+
if (ctx.functionsExecuted.has('search_tools')) {
|
|
150
|
+
injectActivated(ctx);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Auto-activate deferred tools mentioned in function results.
|
|
154
|
+
// Handles cases where a tool error suggests using another tool
|
|
155
|
+
// (e.g., GraphJin's "recommended_tool": "fix_query_error").
|
|
156
|
+
this.autoActivateFromResults(ctx.lastFunctionCalls);
|
|
157
|
+
injectActivated(ctx);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Scan function results for mentions of deferred tool names and auto-activate them. */
|
|
163
|
+
private autoActivateFromResults(
|
|
164
|
+
functionCalls: readonly { readonly name: string; readonly result: unknown }[] | undefined
|
|
165
|
+
): void {
|
|
166
|
+
if (!functionCalls || functionCalls.length === 0 || this.deferredNames.size === 0) return;
|
|
167
|
+
|
|
168
|
+
for (const call of functionCalls) {
|
|
169
|
+
const resultText = typeof call.result === 'string'
|
|
170
|
+
? call.result
|
|
171
|
+
: JSON.stringify(call.result ?? '');
|
|
172
|
+
if (!resultText) continue;
|
|
173
|
+
|
|
174
|
+
for (const name of this.deferredNames) {
|
|
175
|
+
if (!this.activatedNames.has(name) && resultText.includes(name)) {
|
|
176
|
+
this.activatedNames.add(name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Search deferred tools by keyword matching on name + description */
|
|
183
|
+
private search(query: string): string {
|
|
184
|
+
const queryLower = query.toLowerCase();
|
|
185
|
+
const terms = queryLower.split(/[\s_\-./]+/).filter(t => t.length > 1);
|
|
186
|
+
|
|
187
|
+
const scored: Array<{ name: string; score: number }> = [];
|
|
188
|
+
for (const name of this.deferredNames) {
|
|
189
|
+
const fn = this.registry.get(name);
|
|
190
|
+
if (!fn) continue;
|
|
191
|
+
|
|
192
|
+
const searchText = `${fn.name} ${fn.description ?? ''}`.toLowerCase();
|
|
193
|
+
let score = 0;
|
|
194
|
+
for (const term of terms) {
|
|
195
|
+
if (fn.name.toLowerCase().includes(term)) score += 3;
|
|
196
|
+
if ((fn.description ?? '').toLowerCase().includes(term)) score += 1;
|
|
197
|
+
}
|
|
198
|
+
if (searchText.includes(queryLower)) score += 2;
|
|
199
|
+
|
|
200
|
+
if (score > 0) {
|
|
201
|
+
scored.push({ name, score });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
scored.sort((a, b) => b.score - a.score);
|
|
206
|
+
const matchedNames = scored.slice(0, this.maxSearchResults).map(s => s.name);
|
|
207
|
+
|
|
208
|
+
if (matchedNames.length === 0) {
|
|
209
|
+
const names = Array.from(this.deferredNames).slice(0, 30);
|
|
210
|
+
return `No tools found matching "${query}". Available tools: ${names.join(', ')}${this.deferredNames.size > 30 ? '...' : ''}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Separate newly activated from already-active
|
|
214
|
+
const newlyActivated: AxFunction[] = [];
|
|
215
|
+
const alreadyActive: string[] = [];
|
|
216
|
+
for (const name of matchedNames) {
|
|
217
|
+
if (this.activatedNames.has(name)) {
|
|
218
|
+
alreadyActive.push(name);
|
|
219
|
+
} else {
|
|
220
|
+
this.activatedNames.add(name);
|
|
221
|
+
const fn = this.registry.get(name);
|
|
222
|
+
if (fn) newlyActivated.push(fn);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (newlyActivated.length === 0) {
|
|
227
|
+
return `All ${alreadyActive.length} matching tools are already active: ${alreadyActive.join(', ')}. Call them directly.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lines = newlyActivated.map((fn) => {
|
|
231
|
+
const params = fn.parameters?.properties
|
|
232
|
+
? Object.keys(fn.parameters.properties).join(', ')
|
|
233
|
+
: 'none';
|
|
234
|
+
return `- **${fn.name}**: ${fn.description ?? 'No description'} (params: ${params})`;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const parts = [
|
|
238
|
+
`Found ${newlyActivated.length} new tool(s) matching "${query}":`,
|
|
239
|
+
'',
|
|
240
|
+
...lines,
|
|
241
|
+
'',
|
|
242
|
+
'These tools are now available. Call them directly.',
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
if (alreadyActive.length > 0) {
|
|
246
|
+
parts.push(`Also already active: ${alreadyActive.join(', ')}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return parts.join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Create the search_tools meta-function */
|
|
253
|
+
private createSearchToolFunction(): AxFunction {
|
|
254
|
+
return {
|
|
255
|
+
name: 'search_tools',
|
|
256
|
+
description:
|
|
257
|
+
'Search for available tools by describing what you need. This agent has additional specialized tools not shown by default. ' +
|
|
258
|
+
'Describe the task (e.g., "query database tables" or "list available schemas") and matching tools will be activated. ' +
|
|
259
|
+
'Call this ONCE to discover tools, then use them directly. Do NOT call search_tools again for already discovered tools.',
|
|
260
|
+
parameters: {
|
|
261
|
+
type: 'object' as const,
|
|
262
|
+
properties: {
|
|
263
|
+
query: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Describe what you need to do (e.g., "query database", "list tables", "execute graphql")',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
required: ['query'],
|
|
269
|
+
},
|
|
270
|
+
func: async (args: { query: string }) => {
|
|
271
|
+
return this.search(args.query);
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|