@amitdeshmukh/ax-crew 8.0.3 → 8.1.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,231 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { AxCrewConfig } from "../src/types.js";
3
+ import { AxCrew, MetricsRegistry } from "../src/index.js";
4
+ import * as axModule from "@ax-llm/ax";
5
+
6
+ vi.mock("@ax-llm/ax", async (importOriginal) => {
7
+ const actual = (await importOriginal()) as typeof axModule;
8
+
9
+ class MockAxDefaultCostTracker {
10
+ private total = 0;
11
+
12
+ trackTokens(count: number) {
13
+ this.total += (count || 0) * 0.000001;
14
+ }
15
+
16
+ getCurrentCost() {
17
+ return this.total;
18
+ }
19
+ }
20
+
21
+ class MockAxGen {
22
+ private usage: any[] = [];
23
+ private signature: any;
24
+
25
+ constructor(signature: any) {
26
+ this.signature = signature;
27
+ }
28
+
29
+ async forward(_ai: any, values: Record<string, any>) {
30
+ this.usage.push({
31
+ ai: "mock-ai",
32
+ model: "mock-model",
33
+ tokens: { promptTokens: 5, completionTokens: 3 },
34
+ });
35
+ return { answer: `gen:${values.query ?? values.task ?? "ok"}` };
36
+ }
37
+
38
+ async *streamingForward(_ai: any, values: Record<string, any>) {
39
+ this.usage.push({
40
+ ai: "mock-ai",
41
+ model: "mock-model",
42
+ tokens: { promptTokens: 5, completionTokens: 3 },
43
+ });
44
+ yield { version: 1, index: 0, delta: { answer: `gen:${values.query ?? "ok"}` } };
45
+ }
46
+
47
+ register() {}
48
+
49
+ setExamples() {}
50
+
51
+ setDescription() {}
52
+
53
+ getUsage() {
54
+ return this.usage;
55
+ }
56
+
57
+ resetUsage() {
58
+ this.usage = [];
59
+ }
60
+
61
+ getSignature() {
62
+ return {
63
+ getDescription: () => "",
64
+ };
65
+ }
66
+ }
67
+
68
+ class MockAxAgent {
69
+ private usage: any[] = [];
70
+ private signature: any;
71
+ public program: any;
72
+ public actorDescription = "";
73
+ public responderDescription = "";
74
+
75
+ constructor(config: any) {
76
+ this.signature = {
77
+ getDescription: () => "",
78
+ };
79
+ this.program = {
80
+ setDescription: vi.fn(),
81
+ };
82
+ this.actorDescription = config?.agentIdentity?.description ?? "";
83
+ this.responderDescription = config?.agentIdentity?.description ?? "";
84
+ }
85
+
86
+ async forward(_ai: any, values: Record<string, any>) {
87
+ this.usage.push({
88
+ ai: "mock-ai",
89
+ model: "mock-model",
90
+ tokens: { promptTokens: 11, completionTokens: 7 },
91
+ });
92
+ return { answer: `agent:${values.query ?? values.task ?? "ok"}` };
93
+ }
94
+
95
+ async *streamingForward(_ai: any, values: Record<string, any>) {
96
+ this.usage.push({
97
+ ai: "mock-ai",
98
+ model: "mock-model",
99
+ tokens: { promptTokens: 11, completionTokens: 7 },
100
+ });
101
+ yield { version: 1, index: 0, delta: { answer: `agent:${values.query ?? "ok"}` } };
102
+ }
103
+
104
+ getUsage() {
105
+ return this.usage;
106
+ }
107
+
108
+ resetUsage() {
109
+ this.usage = [];
110
+ }
111
+
112
+ getFunction() {
113
+ return { name: "mockAgentFn", description: "mock", parameters: { type: "object", properties: {} } };
114
+ }
115
+
116
+ getSignature() {
117
+ return this.signature;
118
+ }
119
+
120
+ _buildSplitPrograms() {}
121
+ }
122
+
123
+ return {
124
+ ...actual,
125
+ ai: vi.fn().mockImplementation((args: any) => ({
126
+ getName: () => args.name,
127
+ chat: vi.fn(),
128
+ defaults: { model: args.config?.model ?? "mock-model" },
129
+ options: args.options,
130
+ })),
131
+ AxAgent: MockAxAgent as any,
132
+ AxGen: MockAxGen as any,
133
+ AxDefaultCostTracker: MockAxDefaultCostTracker as any,
134
+ };
135
+ });
136
+
137
+ const makeConfig = (mode: "axagent" | "axgen"): AxCrewConfig => ({
138
+ crew: [
139
+ {
140
+ name: `mode-${mode}`,
141
+ description: "Agent for execution mode metrics testing",
142
+ executionMode: mode,
143
+ signature: "query:string -> answer:string",
144
+ provider: "openai" as any,
145
+ providerKeyName: "OPENAI_API_KEY",
146
+ ai: { model: "mock-model" } as any,
147
+ axAgentOptions:
148
+ mode === "axagent"
149
+ ? {
150
+ contextFields: [],
151
+ mode: "simple",
152
+ }
153
+ : undefined,
154
+ },
155
+ ],
156
+ });
157
+
158
+ describe("Execution Mode Metrics + Tracing", () => {
159
+ beforeEach(() => {
160
+ vi.clearAllMocks();
161
+ MetricsRegistry.reset();
162
+ process.env.OPENAI_API_KEY = "dummy-key";
163
+ });
164
+
165
+ afterEach(() => {
166
+ MetricsRegistry.reset();
167
+ });
168
+
169
+ it("records built-in usage/cost and telemetry wiring in axagent mode", async () => {
170
+ const telemetry = { tracer: { id: "tracer" }, meter: { id: "meter" } };
171
+ const crew = new AxCrew(makeConfig("axagent"), {}, { telemetry } as any);
172
+ await crew.addAllAgents();
173
+
174
+ const agent = crew.agents?.get("mode-axagent");
175
+ expect(agent).toBeDefined();
176
+
177
+ const result = await agent!.forward({ query: "hello" });
178
+ expect(result.answer).toContain("agent:");
179
+
180
+ const aiCallArgs = vi.mocked(axModule.ai).mock.calls[0][0] as any;
181
+ expect(aiCallArgs.options.tracer).toBe(telemetry.tracer);
182
+ expect(aiCallArgs.options.meter).toBe(telemetry.meter);
183
+
184
+ const crewMetrics = crew.getCrewMetrics();
185
+ expect(crewMetrics.requests.totalRequests).toBe(1);
186
+ expect(crewMetrics.tokens.totalTokens).toBe(18); // 11 + 7 from MockAxAgent
187
+ expect(crewMetrics.estimatedCostUSD).toBeGreaterThan(0);
188
+ });
189
+
190
+ it("records built-in usage/cost and telemetry wiring in axgen mode", async () => {
191
+ const telemetry = { tracer: { id: "tracer" }, meter: { id: "meter" } };
192
+ const crew = new AxCrew(makeConfig("axgen"), {}, { telemetry } as any);
193
+ await crew.addAllAgents();
194
+
195
+ const agent = crew.agents?.get("mode-axgen");
196
+ expect(agent).toBeDefined();
197
+
198
+ const result = await agent!.forward({ query: "hello" });
199
+ expect(result.answer).toContain("gen:");
200
+
201
+ const aiCallArgs = vi.mocked(axModule.ai).mock.calls[0][0] as any;
202
+ expect(aiCallArgs.options.tracer).toBe(telemetry.tracer);
203
+ expect(aiCallArgs.options.meter).toBe(telemetry.meter);
204
+
205
+ const crewMetrics = crew.getCrewMetrics();
206
+ expect(crewMetrics.requests.totalRequests).toBe(1);
207
+ expect(crewMetrics.tokens.totalTokens).toBe(8); // 5 + 3 from MockAxGen
208
+ expect(crewMetrics.estimatedCostUSD).toBeGreaterThan(0);
209
+ });
210
+
211
+ it("tracks streaming requests in both execution modes", async () => {
212
+ for (const mode of ["axagent", "axgen"] as const) {
213
+ MetricsRegistry.reset();
214
+ const crew = new AxCrew(makeConfig(mode), {}, { telemetry: {} } as any);
215
+ await crew.addAllAgents();
216
+ const agent = crew.agents?.get(`mode-${mode}`);
217
+ expect(agent).toBeDefined();
218
+
219
+ const stream = agent!.streamingForward({ query: "streaming" });
220
+ for await (const _chunk of stream) {
221
+ // consume
222
+ }
223
+
224
+ const crewMetrics = crew.getCrewMetrics();
225
+ expect(crewMetrics.requests.totalRequests).toBe(1);
226
+ expect(crewMetrics.requests.totalStreamingRequests).toBe(1);
227
+ expect(crewMetrics.tokens.totalTokens).toBe(mode === "axagent" ? 18 : 8);
228
+ expect(crewMetrics.estimatedCostUSD).toBeGreaterThan(0);
229
+ }
230
+ });
231
+ });
@@ -1,174 +0,0 @@
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
- }
@@ -1,291 +0,0 @@
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
- });