@amitdeshmukh/ax-crew 8.0.0 → 8.0.2

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.
@@ -54,7 +54,13 @@ const initializeMCPServers = async (agentConfigData) => {
54
54
  const mcpClient = new AxMCPClient(transport, { debug: agentConfigData.debug || false });
55
55
  await mcpClient.init();
56
56
  initializedClients.push(mcpClient);
57
- functions.push(...mcpClient.toFunction());
57
+ // Normalize MCP tool schemas: some MCP servers omit `parameters` for
58
+ // zero-arg tools, but providers like Gemini require a valid schema.
59
+ const mcpFns = mcpClient.toFunction().map(fn => ({
60
+ ...fn,
61
+ parameters: fn.parameters ?? { type: 'object', properties: {} },
62
+ }));
63
+ functions.push(...mcpFns);
58
64
  }
59
65
  return functions;
60
66
  }
@@ -513,6 +513,22 @@ class AxCrew {
513
513
  uniqueFunctions.push(fn);
514
514
  }
515
515
  }
516
+ // Wrap each function handler to record call count and latency in MetricsRegistry
517
+ const crewId = this.crewId;
518
+ const agentNameForMetrics = name;
519
+ const instrumentedFunctions = uniqueFunctions.map(fn => ({
520
+ ...fn,
521
+ func: async (args, extra) => {
522
+ const fnStart = performance.now();
523
+ try {
524
+ return await fn.func(args, extra);
525
+ }
526
+ finally {
527
+ const latencyMs = performance.now() - fnStart;
528
+ MetricsRegistry.recordFunctionCall({ crewId, agent: agentNameForMetrics }, latencyMs, fn.name);
529
+ }
530
+ },
531
+ }));
516
532
  // Create an instance of StatefulAxAgent
517
533
  // Set crew reference in state for execution tracking (ACE feedback routing)
518
534
  const agentState = { ...this.state, crew: this };
@@ -521,7 +537,7 @@ class AxCrew {
521
537
  description,
522
538
  definition: agentConfig.definition,
523
539
  signature,
524
- functions: uniqueFunctions,
540
+ functions: instrumentedFunctions,
525
541
  agents: uniqueSubAgents,
526
542
  examples,
527
543
  debug: agentConfig.debug,
@@ -20,8 +20,11 @@ export declare function recordTokens(labels: LabelKeys, usage: TokenUsage): void
20
20
  export declare function recordEstimatedCost(labels: LabelKeys, usd: number): void;
21
21
  /**
22
22
  * Record a function call invocation and add its latency to totals.
23
+ * @param labels Crew/agent identifiers
24
+ * @param latencyMs Duration of the function call in milliseconds
25
+ * @param functionName Optional name of the function that was called
23
26
  */
24
- export declare function recordFunctionCall(labels: LabelKeys, latencyMs: number): void;
27
+ export declare function recordFunctionCall(labels: LabelKeys, latencyMs: number, functionName?: string): void;
25
28
  /**
26
29
  * Get a metrics snapshot for specific labels (crew + agent + optional provider/model).
27
30
  */
@@ -19,6 +19,7 @@ function getOrInit(labels) {
19
19
  estimatedCostUSD: 0,
20
20
  functionCalls: 0,
21
21
  functionLatencyMs: 0,
22
+ functionDetails: new Map(),
22
23
  };
23
24
  store.set(k, c);
24
25
  }
@@ -64,11 +65,20 @@ export function recordEstimatedCost(labels, usd) {
64
65
  }
65
66
  /**
66
67
  * Record a function call invocation and add its latency to totals.
68
+ * @param labels Crew/agent identifiers
69
+ * @param latencyMs Duration of the function call in milliseconds
70
+ * @param functionName Optional name of the function that was called
67
71
  */
68
- export function recordFunctionCall(labels, latencyMs) {
72
+ export function recordFunctionCall(labels, latencyMs, functionName) {
69
73
  const c = getOrInit(labels);
70
74
  c.functionCalls += 1;
71
75
  c.functionLatencyMs += latencyMs || 0;
76
+ if (functionName) {
77
+ const detail = c.functionDetails.get(functionName) || { calls: 0, latencyMs: 0 };
78
+ detail.calls += 1;
79
+ detail.latencyMs += latencyMs || 0;
80
+ c.functionDetails.set(functionName, detail);
81
+ }
72
82
  }
73
83
  /**
74
84
  * Get a metrics snapshot for specific labels (crew + agent + optional provider/model).
@@ -96,9 +106,20 @@ export function snapshot(labels) {
96
106
  functions: {
97
107
  totalFunctionCalls: c.functionCalls,
98
108
  totalFunctionLatencyMs: c.functionLatencyMs,
109
+ details: detailsFromMap(c.functionDetails),
99
110
  },
100
111
  };
101
112
  }
113
+ /** Convert internal function detail map to sorted array */
114
+ function detailsFromMap(m) {
115
+ if (m.size === 0)
116
+ return undefined;
117
+ return Array.from(m.entries()).map(([name, d]) => ({
118
+ name,
119
+ calls: d.calls,
120
+ totalLatencyMs: d.latencyMs,
121
+ }));
122
+ }
102
123
  /**
103
124
  * Reset metrics for specific labels, or clear all if no labels provided.
104
125
  */
@@ -125,6 +146,7 @@ export function snapshotCrew(crewId) {
125
146
  estimatedCostUSD: 0,
126
147
  functionCalls: 0,
127
148
  functionLatencyMs: 0,
149
+ functionDetails: new Map(),
128
150
  };
129
151
  const agg = Array.from(store.entries()).reduce((acc, [k, v]) => {
130
152
  if (k.startsWith(crewId + '|')) {
@@ -138,9 +160,16 @@ export function snapshotCrew(crewId) {
138
160
  acc.estimatedCostUSD = Number(new Big(acc.estimatedCostUSD || 0).plus(v.estimatedCostUSD || 0));
139
161
  acc.functionCalls += v.functionCalls;
140
162
  acc.functionLatencyMs += v.functionLatencyMs;
163
+ // Merge per-function details
164
+ for (const [fnName, d] of v.functionDetails) {
165
+ const existing = acc.functionDetails.get(fnName) || { calls: 0, latencyMs: 0 };
166
+ existing.calls += d.calls;
167
+ existing.latencyMs += d.latencyMs;
168
+ acc.functionDetails.set(fnName, existing);
169
+ }
141
170
  }
142
171
  return acc;
143
- }, { ...empty });
172
+ }, empty);
144
173
  const totalTokens = agg.inputTokens + agg.outputTokens;
145
174
  return {
146
175
  requests: {
@@ -160,6 +189,7 @@ export function snapshotCrew(crewId) {
160
189
  functions: {
161
190
  totalFunctionCalls: agg.functionCalls,
162
191
  totalFunctionLatencyMs: agg.functionLatencyMs,
192
+ details: detailsFromMap(agg.functionDetails),
163
193
  },
164
194
  };
165
195
  }
@@ -15,9 +15,16 @@ export interface RequestStats {
15
15
  durationMsSum: number;
16
16
  durationCount: number;
17
17
  }
18
+ export interface FunctionCallDetail {
19
+ name: string;
20
+ calls: number;
21
+ totalLatencyMs: number;
22
+ }
18
23
  export interface FunctionStats {
19
24
  totalFunctionCalls: number;
20
25
  totalFunctionLatencyMs: number;
26
+ /** Per-function breakdown of calls and latency */
27
+ details?: FunctionCallDetail[];
21
28
  }
22
29
  export interface MetricsSnapshot {
23
30
  provider?: string;
@@ -0,0 +1,174 @@
1
+ // Crew Factory - Manages pool of crew instances for concurrent requests
2
+
3
+ import { AxCrew } from '../dist/index.js';
4
+ import type { AxCrewConfig } from '../dist/types.js';
5
+ import type { AxFunction } from '@ax-llm/ax';
6
+
7
+ interface CrewInstance {
8
+ crew: InstanceType<typeof AxCrew>;
9
+ busy: boolean;
10
+ id: number;
11
+ createdAt: number;
12
+ }
13
+
14
+ interface FactoryConfig {
15
+ crewConfig: AxCrewConfig;
16
+ crewFunctions: Record<string, AxFunction>;
17
+ agentNames: string[];
18
+ }
19
+
20
+ const MAX_POOL_SIZE = 5;
21
+ const INSTANCE_TTL_MS = 5 * 60 * 1000; // 5 minutes idle before cleanup
22
+
23
+ let instanceIdCounter = 0;
24
+ const pool: CrewInstance[] = [];
25
+ let isInitialized = false;
26
+ let factoryConfig: FactoryConfig;
27
+
28
+ /**
29
+ * Initialize the factory with at least one crew instance
30
+ */
31
+ export async function initCrewFactory(config: FactoryConfig): Promise<void> {
32
+ if (isInitialized) {
33
+ console.log('[CrewFactory] Already initialized');
34
+ return;
35
+ }
36
+
37
+ factoryConfig = config;
38
+
39
+ console.log('[CrewFactory] Initializing...');
40
+
41
+ // Create initial instance
42
+ await createCrewInstance();
43
+ isInitialized = true;
44
+
45
+ // Start cleanup interval
46
+ setInterval(cleanupIdleInstances, 60000);
47
+
48
+ console.log('[CrewFactory] Initialized with 1 instance');
49
+ }
50
+
51
+ /**
52
+ * Create a new crew instance
53
+ */
54
+ async function createCrewInstance(): Promise<CrewInstance> {
55
+ const id = ++instanceIdCounter;
56
+ console.log(`[CrewFactory] Creating instance #${id}...`);
57
+
58
+ const crew = new AxCrew(factoryConfig.crewConfig, factoryConfig.crewFunctions);
59
+ for (const name of factoryConfig.agentNames) {
60
+ await crew.addAgentsToCrew([name]);
61
+ }
62
+
63
+ const instance: CrewInstance = {
64
+ crew,
65
+ busy: false,
66
+ id,
67
+ createdAt: Date.now(),
68
+ };
69
+
70
+ pool.push(instance);
71
+ console.log(`[CrewFactory] Instance #${id} ready. Pool size: ${pool.length}`);
72
+
73
+ return instance;
74
+ }
75
+
76
+ /**
77
+ * Acquire an available crew instance, creating one if needed
78
+ */
79
+ export async function acquireCrew(): Promise<{ crew: InstanceType<typeof AxCrew>; release: () => void }> {
80
+ if (!isInitialized) {
81
+ throw new Error('CrewFactory not initialized. Call initCrewFactory() first.');
82
+ }
83
+
84
+ // Find an available instance
85
+ let instance = pool.find(i => !i.busy);
86
+
87
+ // If none available and pool not at max, create new instance
88
+ if (!instance && pool.length < MAX_POOL_SIZE) {
89
+ instance = await createCrewInstance();
90
+ }
91
+
92
+ // If still none available, wait for one to become free
93
+ if (!instance) {
94
+ console.log('[CrewFactory] Pool exhausted, waiting for available instance...');
95
+ instance = await waitForAvailableInstance();
96
+ }
97
+
98
+ instance.busy = true;
99
+ const instanceId = instance.id;
100
+
101
+ console.log(`[CrewFactory] Acquired instance #${instanceId}. Active: ${pool.filter(i => i.busy).length}/${pool.length}`);
102
+
103
+ return {
104
+ crew: instance.crew,
105
+ release: () => {
106
+ const inst = pool.find(i => i.id === instanceId);
107
+ if (inst) {
108
+ inst.busy = false;
109
+ inst.createdAt = Date.now(); // Reset TTL on release
110
+ console.log(`[CrewFactory] Released instance #${instanceId}. Active: ${pool.filter(i => i.busy).length}/${pool.length}`);
111
+ }
112
+ },
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Wait for an instance to become available
118
+ */
119
+ function waitForAvailableInstance(): Promise<CrewInstance> {
120
+ return new Promise((resolve) => {
121
+ const check = () => {
122
+ const instance = pool.find(i => !i.busy);
123
+ if (instance) {
124
+ resolve(instance);
125
+ } else {
126
+ setTimeout(check, 100);
127
+ }
128
+ };
129
+ check();
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Clean up idle instances beyond the first one
135
+ */
136
+ function cleanupIdleInstances(): void {
137
+ const now = Date.now();
138
+ const toRemove: number[] = [];
139
+
140
+ for (const instance of pool) {
141
+ // Keep at least one instance, only cleanup idle instances past TTL
142
+ if (pool.length - toRemove.length > 1 &&
143
+ !instance.busy &&
144
+ now - instance.createdAt > INSTANCE_TTL_MS) {
145
+ toRemove.push(instance.id);
146
+ }
147
+ }
148
+
149
+ for (const id of toRemove) {
150
+ const index = pool.findIndex(i => i.id === id);
151
+ if (index !== -1) {
152
+ pool.splice(index, 1);
153
+ console.log(`[CrewFactory] Cleaned up idle instance #${id}. Pool size: ${pool.length}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Check if factory is ready
160
+ */
161
+ export function isFactoryReady(): boolean {
162
+ return isInitialized;
163
+ }
164
+
165
+ /**
166
+ * Get factory stats
167
+ */
168
+ export function getFactoryStats() {
169
+ return {
170
+ poolSize: pool.length,
171
+ activeCount: pool.filter(i => i.busy).length,
172
+ maxPoolSize: MAX_POOL_SIZE,
173
+ };
174
+ }
@@ -0,0 +1,291 @@
1
+ // Standalone test: json[] signature pattern with Google Gemini provider
2
+ //
3
+ // Tests that RoutePlanner returns waypoints as actual objects (json[]),
4
+ // not stringified JSON, after the Gemini schema type-union fix.
5
+
6
+ import type { AxCrewConfig } from '../dist/types.js';
7
+ import type { AxFunction } from '@ax-llm/ax';
8
+ import { FlightConstantSpeedPlanner, type LLA } from './planner.js';
9
+ import { initCrewFactory, acquireCrew } from './factory.js';
10
+ import dotenv from 'dotenv';
11
+ dotenv.config();
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Crew Configuration
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const crewConfig: AxCrewConfig = {
18
+ crew: [
19
+ {
20
+ name: 'RoutePlanner',
21
+ description: `
22
+ Plans optimal flight routes between geolocations.
23
+ Uses the PlanFlightRoute function to generate smooth paths with fly-by turns.
24
+ Call PlanFlightRoute with current position and destination to get detailed waypoints.
25
+ `,
26
+ signature: `
27
+ currentLat:number "current latitude in degrees",
28
+ currentLon:number "current longitude in degrees",
29
+ currentAlt:number "current altitude in meters",
30
+ heading?:number "current heading in degrees",
31
+ speedKts?:number "speed in knots",
32
+ destLat:number "destination latitude in degrees",
33
+ destLon:number "destination longitude in degrees",
34
+ destAlt:number "destination altitude in meters",
35
+ vehicleType?:string "type of vehicle"
36
+ ->
37
+ waypoints:json[] "Array of {lat, lon, alt, heading}",
38
+ totalDistanceKm:number "total distance in kilometers",
39
+ eta:string "estimated time of arrival in MM:SS format"
40
+ `,
41
+ provider: 'openrouter' as const,
42
+ providerKeyName: 'OPENROUTER_API_KEY',
43
+ ai: {
44
+ model: 'stepfun/step-3.5-flash:free',
45
+ temperature: 0,
46
+ },
47
+ options: {
48
+ debug: true,
49
+ },
50
+ functions: ['PlanFlightRoute'],
51
+ },
52
+ {
53
+ name: 'Manager',
54
+ description: `Command center assistant that handles user requests for route planning.
55
+ IMPORTANT: When calling RoutePlanner, you MUST include the waypoints array it returns in your response output.`,
56
+ signature: `
57
+ userCommand:string "user request or command"
58
+ ->
59
+ responseToCommand:string "A response to the command given.",
60
+ waypoints?:json[] "Array of {lat, lon, alt, heading} from RoutePlanner",
61
+ totalDistanceKm?:number "total distance in km from RoutePlanner",
62
+ eta?:string "ETA from RoutePlanner"
63
+ `,
64
+ provider: 'openrouter' as const,
65
+ providerKeyName: 'OPENROUTER_API_KEY',
66
+ ai: {
67
+ model: 'stepfun/step-3.5-flash:free',
68
+ temperature: 0,
69
+ },
70
+ options: {
71
+ debug: true,
72
+ },
73
+ agents: ['RoutePlanner'],
74
+ },
75
+ ],
76
+ };
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Haversine distance (for dynamic Hz calculation)
80
+ // ---------------------------------------------------------------------------
81
+
82
+ const EARTH_RADIUS = 6378137.0;
83
+ const MAX_WAYPOINTS = 25;
84
+ const DEFAULT_HZ = 0.0333;
85
+
86
+ function getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
87
+ const dLat = (lat2 - lat1) * (Math.PI / 180);
88
+ const dLon = (lon2 - lon1) * (Math.PI / 180);
89
+ const a =
90
+ Math.sin(dLat / 2) ** 2 +
91
+ Math.cos(lat1 * (Math.PI / 180)) *
92
+ Math.cos(lat2 * (Math.PI / 180)) *
93
+ Math.sin(dLon / 2) ** 2;
94
+ return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * EARTH_RADIUS;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // PlanFlightRoute function (uses FlightConstantSpeedPlanner)
99
+ // ---------------------------------------------------------------------------
100
+
101
+ const planFlightRoute: AxFunction = {
102
+ name: 'PlanFlightRoute',
103
+ description: `Plans a flight route from current position through waypoints to destination.
104
+ Uses constant speed with smooth fly-by turns at 25-degree bank angle.
105
+ Returns detailed waypoints along the path plus mission summary (distance, time, ETA).`,
106
+ parameters: {
107
+ type: 'object',
108
+ properties: {
109
+ currentLat: { type: 'number', description: 'Current latitude in degrees' },
110
+ currentLon: { type: 'number', description: 'Current longitude in degrees' },
111
+ currentAlt: { type: 'number', description: 'Current altitude in meters' },
112
+ heading: { type: 'number', description: 'Current heading in degrees (0-360)' },
113
+ destLat: { type: 'number', description: 'Destination latitude in degrees' },
114
+ destLon: { type: 'number', description: 'Destination longitude in degrees' },
115
+ destAlt: { type: 'number', description: 'Destination altitude in meters' },
116
+ speedKts: { type: 'number', description: 'Speed in knots' },
117
+ },
118
+ required: [
119
+ 'currentLat', 'currentLon', 'currentAlt', 'heading',
120
+ 'destLat', 'destLon', 'destAlt', 'speedKts',
121
+ ],
122
+ },
123
+ func: async (args) => {
124
+ const {
125
+ currentLat, currentLon, currentAlt, heading,
126
+ destLat, destLon, destAlt, speedKts,
127
+ } = args as Record<string, number>;
128
+
129
+ // Convert knots to meters per second (1 knot = 0.514444 m/s)
130
+ const speedMps = speedKts * 0.514444;
131
+
132
+ // Calculate dynamic Hz to limit waypoints for long routes
133
+ const estimatedDistanceM = getDistance(currentLat, currentLon, destLat, destLon);
134
+ const estimatedTimeS = estimatedDistanceM / speedMps;
135
+ const maxHz = MAX_WAYPOINTS / estimatedTimeS;
136
+ const hz = Math.min(DEFAULT_HZ, maxHz);
137
+
138
+ // Create waypoints for the planner
139
+ const waypoints: LLA[] = [
140
+ { lat: currentLat, lon: currentLon, alt: currentAlt, heading },
141
+ { lat: destLat, lon: destLon, alt: destAlt, heading: 0 },
142
+ ];
143
+
144
+ const planner = new FlightConstantSpeedPlanner(speedMps, hz);
145
+ const { path, summary } = planner.getMissionDetails(waypoints);
146
+
147
+ // Return actual objects — tests that json[] works with Gemini
148
+ const routeWaypoints = path.map((p) => ({
149
+ lat: Number(p.lat.toFixed(6)),
150
+ lon: Number(p.lon.toFixed(6)),
151
+ alt: Math.round(p.alt),
152
+ heading: p.heading,
153
+ }));
154
+
155
+ return {
156
+ waypoints: routeWaypoints,
157
+ totalDistanceKm: summary.totalDistanceKm,
158
+ totalTimeSeconds: summary.totalTimeSeconds,
159
+ etaFormatted: summary.etaFormatted,
160
+ waypointCount: routeWaypoints.length,
161
+ };
162
+ },
163
+ };
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Function registry
167
+ // ---------------------------------------------------------------------------
168
+
169
+ const crewFunctions: Record<string, AxFunction> = {
170
+ PlanFlightRoute: planFlightRoute,
171
+ };
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Waypoint validation helpers
175
+ // ---------------------------------------------------------------------------
176
+
177
+ interface Waypoint {
178
+ lon: number;
179
+ lat: number;
180
+ alt: number;
181
+ }
182
+
183
+ function normalizeWaypoint(wp: unknown): Waypoint | null {
184
+ let obj: Record<string, unknown> | null = null;
185
+
186
+ // Handle string waypoints (fallback if Gemini returns strings)
187
+ if (typeof wp === 'string') {
188
+ try {
189
+ obj = JSON.parse(wp);
190
+ } catch {
191
+ console.log('[Test] Failed to parse waypoint string:', wp);
192
+ return null;
193
+ }
194
+ } else if (typeof wp === 'object' && wp !== null) {
195
+ obj = wp as Record<string, unknown>;
196
+ }
197
+
198
+ if (!obj) return null;
199
+
200
+ const lon =
201
+ typeof obj.lon === 'number' ? obj.lon :
202
+ typeof obj.lng === 'number' ? obj.lng : null;
203
+ const lat = typeof obj.lat === 'number' ? obj.lat : null;
204
+ const alt =
205
+ typeof obj.alt === 'number' ? obj.alt :
206
+ typeof obj.altitude === 'number' ? obj.altitude : 1000;
207
+
208
+ if (lon === null || lat === null) return null;
209
+ if (lat === 0 && lon === 0) return null;
210
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
211
+
212
+ return { lon, lat, alt };
213
+ }
214
+
215
+ function filterValidWaypoints(waypoints: unknown[]): Waypoint[] {
216
+ return waypoints
217
+ .map(normalizeWaypoint)
218
+ .filter((wp): wp is Waypoint => wp !== null);
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Main
223
+ // ---------------------------------------------------------------------------
224
+
225
+ async function main() {
226
+ console.log('=== Google Gemini json[] Test ===\n');
227
+
228
+ if (!process.env.GEMINI_API_KEY) {
229
+ console.error('GEMINI_API_KEY not set. Add it to .env or export it.');
230
+ process.exit(1);
231
+ }
232
+
233
+ // Initialize the factory with our config
234
+ await initCrewFactory({
235
+ crewConfig,
236
+ crewFunctions,
237
+ agentNames: ['RoutePlanner', 'Manager'],
238
+ });
239
+
240
+ const { crew, release } = await acquireCrew();
241
+
242
+ const manager = crew.agents?.get('Manager');
243
+ if (!manager) {
244
+ throw new Error('Manager agent not found');
245
+ }
246
+
247
+ try {
248
+ console.log('[Test] Sending route planning request...\n');
249
+
250
+ const result = await manager.forward({
251
+ userCommand:
252
+ 'Plan a flight route from San Francisco (37.7749, -122.4194, alt 500m) ' +
253
+ 'to Los Angeles (34.0522, -118.2437, alt 500m) at 250 knots, heading 150 degrees.',
254
+ });
255
+
256
+ console.log('\n[Test] Raw Manager result:', JSON.stringify(result, null, 2));
257
+
258
+ // Validate response
259
+ console.log('\n--- Validation ---');
260
+ console.log('responseToCommand:', result.responseToCommand);
261
+ console.log('totalDistanceKm:', result.totalDistanceKm);
262
+ console.log('eta:', result.eta);
263
+
264
+ if (Array.isArray(result.waypoints)) {
265
+ console.log(`\nWaypoints received: ${result.waypoints.length}`);
266
+
267
+ // Check if waypoints are objects or strings
268
+ const first = result.waypoints[0];
269
+ console.log('First waypoint type:', typeof first);
270
+ console.log('First waypoint:', JSON.stringify(first));
271
+
272
+ const valid = filterValidWaypoints(result.waypoints);
273
+ console.log(`Valid waypoints: ${valid.length}/${result.waypoints.length}`);
274
+
275
+ if (valid.length > 0) {
276
+ console.log('\n[PASS] json[] pattern worked with Google Gemini');
277
+ } else {
278
+ console.log('\n[FAIL] Waypoints received but none were valid');
279
+ }
280
+ } else {
281
+ console.log('\n[FAIL] No waypoints array in response');
282
+ }
283
+ } finally {
284
+ release();
285
+ }
286
+ }
287
+
288
+ main().catch((err) => {
289
+ console.error('[FATAL]', err);
290
+ process.exit(1);
291
+ });