@cleocode/adapters 2026.4.91 → 2026.4.94
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/dist/index.js +41069 -18278
- package/dist/index.js.map +4 -4
- package/dist/providers/claude-code/install.d.ts.map +1 -1
- package/dist/providers/claude-sdk/index.d.ts +10 -4
- package/dist/providers/claude-sdk/index.d.ts.map +1 -1
- package/dist/providers/claude-sdk/spawn.d.ts +29 -28
- package/dist/providers/claude-sdk/spawn.d.ts.map +1 -1
- package/dist/providers/codex/install.d.ts.map +1 -1
- package/dist/providers/cursor/install.d.ts.map +1 -1
- package/dist/providers/openai-sdk/adapter.d.ts +18 -17
- package/dist/providers/openai-sdk/adapter.d.ts.map +1 -1
- package/dist/providers/openai-sdk/guardrails.d.ts +71 -18
- package/dist/providers/openai-sdk/guardrails.d.ts.map +1 -1
- package/dist/providers/openai-sdk/handoff.d.ts +51 -21
- package/dist/providers/openai-sdk/handoff.d.ts.map +1 -1
- package/dist/providers/openai-sdk/index.d.ts +8 -5
- package/dist/providers/openai-sdk/index.d.ts.map +1 -1
- package/dist/providers/openai-sdk/install.d.ts +1 -1
- package/dist/providers/openai-sdk/spawn.d.ts +54 -21
- package/dist/providers/openai-sdk/spawn.d.ts.map +1 -1
- package/dist/providers/openai-sdk/tracing.d.ts +87 -21
- package/dist/providers/openai-sdk/tracing.d.ts.map +1 -1
- package/dist/providers/opencode/install.d.ts.map +1 -1
- package/dist/providers/opencode/spawn.d.ts.map +1 -1
- package/dist/providers/pi/install.d.ts.map +1 -1
- package/dist/providers/shared/paths.d.ts +32 -0
- package/dist/providers/shared/paths.d.ts.map +1 -0
- package/dist/providers/shared/sdk-result-mapper.d.ts +9 -7
- package/dist/providers/shared/sdk-result-mapper.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/__tests__/claude-code-adapter.test.ts +9 -4
- package/src/__tests__/cursor-adapter.test.ts +9 -8
- package/src/__tests__/harness-interop.test.ts +451 -0
- package/src/__tests__/opencode-adapter.test.ts +9 -4
- package/src/providers/claude-code/install.ts +10 -2
- package/src/providers/claude-sdk/__tests__/spawn.test.ts +100 -265
- package/src/providers/claude-sdk/index.ts +10 -4
- package/src/providers/claude-sdk/spawn.ts +69 -106
- package/src/providers/codex/install.ts +10 -2
- package/src/providers/cursor/install.ts +10 -2
- package/src/providers/openai-sdk/__tests__/openai-sdk-spawn.test.ts +134 -103
- package/src/providers/openai-sdk/adapter.ts +19 -18
- package/src/providers/openai-sdk/guardrails.ts +106 -25
- package/src/providers/openai-sdk/handoff.ts +73 -37
- package/src/providers/openai-sdk/index.ts +28 -4
- package/src/providers/openai-sdk/install.ts +1 -1
- package/src/providers/openai-sdk/manifest.json +4 -4
- package/src/providers/openai-sdk/spawn.ts +213 -48
- package/src/providers/openai-sdk/tracing.ts +105 -22
- package/src/providers/opencode/install.ts +10 -2
- package/src/providers/opencode/spawn.ts +2 -1
- package/src/providers/pi/install.ts +10 -2
- package/src/providers/shared/paths.ts +79 -0
- package/src/providers/shared/sdk-result-mapper.ts +9 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the OpenAI
|
|
2
|
+
* Tests for the OpenAI SDK spawn provider — Vercel AI SDK edition.
|
|
3
3
|
*
|
|
4
|
-
* All
|
|
5
|
-
*
|
|
4
|
+
* All LLM calls are mocked — no real API keys or network calls are required
|
|
5
|
+
* to run this suite.
|
|
6
6
|
*
|
|
7
7
|
* Test coverage:
|
|
8
8
|
* - GuardrailTests: path ACL logic and guardrail builders
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* - AdapterTests: identity, capabilities, initialize, dispose, healthCheck
|
|
12
12
|
* - InstallProviderTests: isInstalled, install, uninstall
|
|
13
13
|
* - TraceProcessorTests: onSpanEnd event capture
|
|
14
|
-
* - HandoffIntegrationTest: lead + worker topology with mocked
|
|
14
|
+
* - HandoffIntegrationTest: lead + worker topology with mocked generateText
|
|
15
15
|
*
|
|
16
|
-
* @task T582
|
|
16
|
+
* @task T582 (original)
|
|
17
|
+
* @task T933 (SDK consolidation — Vercel AI SDK migration)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -22,61 +23,33 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
22
23
|
// Hoist shared state so vi.mock factories can reference it
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
|
|
25
|
-
const {
|
|
26
|
+
const { generateTextCalls, mockRunState, mockFsState } = vi.hoisted(() => {
|
|
26
27
|
return {
|
|
27
|
-
|
|
28
|
-
mockRunState: {
|
|
28
|
+
generateTextCalls: [] as Array<{ model: unknown; system?: string; prompt: string }>,
|
|
29
|
+
mockRunState: { output: 'mock output', shouldThrow: false },
|
|
29
30
|
mockFsState: { exists: false, content: '' },
|
|
30
31
|
};
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// Mock @openai/
|
|
35
|
+
// Mock Vercel AI SDK surface — @ai-sdk/openai + ai/generateText
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
|
|
37
|
-
vi.mock('@openai
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
inputGuardrails: unknown[];
|
|
43
|
-
instructions: string;
|
|
44
|
-
|
|
45
|
-
constructor(opts: {
|
|
46
|
-
name?: string;
|
|
47
|
-
instructions?: string;
|
|
48
|
-
model?: string;
|
|
49
|
-
handoffs?: unknown[];
|
|
50
|
-
inputGuardrails?: unknown[];
|
|
51
|
-
}) {
|
|
52
|
-
this.name = opts.name ?? 'agent';
|
|
53
|
-
this.instructions = opts.instructions ?? '';
|
|
54
|
-
this.model = opts.model ?? 'gpt-4.1';
|
|
55
|
-
this.handoffs = opts.handoffs ?? [];
|
|
56
|
-
this.inputGuardrails = opts.inputGuardrails ?? [];
|
|
57
|
-
createdAgents.push({ name: this.name, handoffs: this.handoffs, model: this.model });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const mockRunFn = async (agent: { name: string }) => {
|
|
62
|
-
if (mockRunState.shouldThrow) throw new Error('mock SDK error');
|
|
63
|
-
return { ...mockRunState.result, _agent: agent };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
class MockRunner {
|
|
67
|
-
run = vi.fn(mockRunFn);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
class MockOpenAIProvider {}
|
|
38
|
+
vi.mock('@ai-sdk/openai', () => ({
|
|
39
|
+
createOpenAI: vi.fn((_config: { apiKey: string }) => {
|
|
40
|
+
return (modelId: string) => ({ __cleoMockModel: true, modelId });
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
44
|
+
vi.mock('ai', () => ({
|
|
45
|
+
generateText: vi.fn(
|
|
46
|
+
async ({ model, system, prompt }: { model: unknown; system?: string; prompt: string }) => {
|
|
47
|
+
generateTextCalls.push({ model, system, prompt });
|
|
48
|
+
if (mockRunState.shouldThrow) throw new Error('mock SDK error');
|
|
49
|
+
return { text: mockRunState.output };
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
}));
|
|
80
53
|
|
|
81
54
|
// ---------------------------------------------------------------------------
|
|
82
55
|
// Mock node:fs for install provider tests
|
|
@@ -124,6 +97,7 @@ import {
|
|
|
124
97
|
buildDefaultGuardrails,
|
|
125
98
|
buildPathGuardrail,
|
|
126
99
|
buildToolAllowlistGuardrail,
|
|
100
|
+
evaluateGuardrails,
|
|
127
101
|
isPathAllowed,
|
|
128
102
|
} from '../guardrails.js';
|
|
129
103
|
import {
|
|
@@ -131,17 +105,18 @@ import {
|
|
|
131
105
|
buildLeadAgent,
|
|
132
106
|
buildStandaloneAgent,
|
|
133
107
|
buildWorkerAgent,
|
|
108
|
+
type CleoAgent,
|
|
134
109
|
WORKER_ARCHETYPES,
|
|
135
110
|
} from '../handoff.js';
|
|
136
111
|
import { OpenAiSdkInstallProvider } from '../install.js';
|
|
137
112
|
import { OpenAiSdkSpawnProvider } from '../spawn.js';
|
|
138
|
-
import { CleoConduitTraceProcessor } from '../tracing.js';
|
|
113
|
+
import { CleoConduitTraceProcessor, type CleoSpan } from '../tracing.js';
|
|
139
114
|
|
|
140
115
|
// ---------------------------------------------------------------------------
|
|
141
116
|
// Helpers
|
|
142
117
|
// ---------------------------------------------------------------------------
|
|
143
118
|
|
|
144
|
-
function makeSpanLike(overrides:
|
|
119
|
+
function makeSpanLike(overrides: Partial<CleoSpan> = {}): CleoSpan {
|
|
145
120
|
return {
|
|
146
121
|
spanId: 'span-001',
|
|
147
122
|
startedAt: new Date().toISOString(),
|
|
@@ -186,9 +161,9 @@ describe('buildPathGuardrail', () => {
|
|
|
186
161
|
it('passes when no path fields in input', async () => {
|
|
187
162
|
const guard = buildPathGuardrail(['/allowed/**']);
|
|
188
163
|
const result = await guard.execute({
|
|
189
|
-
agent: {}
|
|
164
|
+
agent: {},
|
|
190
165
|
input: 'Tell me about the project',
|
|
191
|
-
context: {}
|
|
166
|
+
context: {},
|
|
192
167
|
});
|
|
193
168
|
expect(result.tripwireTriggered).toBe(false);
|
|
194
169
|
});
|
|
@@ -196,9 +171,9 @@ describe('buildPathGuardrail', () => {
|
|
|
196
171
|
it('trips when path field violates ACL', async () => {
|
|
197
172
|
const guard = buildPathGuardrail(['/allowed/**']);
|
|
198
173
|
const result = await guard.execute({
|
|
199
|
-
agent: {}
|
|
174
|
+
agent: {},
|
|
200
175
|
input: JSON.stringify({ path: '/etc/shadow' }),
|
|
201
|
-
context: {}
|
|
176
|
+
context: {},
|
|
202
177
|
});
|
|
203
178
|
expect(result.tripwireTriggered).toBe(true);
|
|
204
179
|
expect((result.outputInfo as Record<string, unknown>).deniedPath).toBe('/etc/shadow');
|
|
@@ -207,9 +182,9 @@ describe('buildPathGuardrail', () => {
|
|
|
207
182
|
it('passes when path field is within ACL', async () => {
|
|
208
183
|
const guard = buildPathGuardrail(['/allowed/**']);
|
|
209
184
|
const result = await guard.execute({
|
|
210
|
-
agent: {}
|
|
185
|
+
agent: {},
|
|
211
186
|
input: JSON.stringify({ path: '/allowed/file.ts' }),
|
|
212
|
-
context: {}
|
|
187
|
+
context: {},
|
|
213
188
|
});
|
|
214
189
|
expect(result.tripwireTriggered).toBe(false);
|
|
215
190
|
});
|
|
@@ -224,9 +199,9 @@ describe('buildToolAllowlistGuardrail', () => {
|
|
|
224
199
|
it('always passes (structural enforcement)', async () => {
|
|
225
200
|
const guard = buildToolAllowlistGuardrail(['read', 'bash']);
|
|
226
201
|
const result = await guard.execute({
|
|
227
|
-
agent: {}
|
|
202
|
+
agent: {},
|
|
228
203
|
input: 'run this',
|
|
229
|
-
context: {}
|
|
204
|
+
context: {},
|
|
230
205
|
});
|
|
231
206
|
expect(result.tripwireTriggered).toBe(false);
|
|
232
207
|
expect((result.outputInfo as Record<string, unknown>).checked).toBe(true);
|
|
@@ -257,6 +232,19 @@ describe('buildDefaultGuardrails', () => {
|
|
|
257
232
|
});
|
|
258
233
|
});
|
|
259
234
|
|
|
235
|
+
describe('evaluateGuardrails', () => {
|
|
236
|
+
it('passes when no guardrails are provided', async () => {
|
|
237
|
+
const result = await evaluateGuardrails([], 'anything');
|
|
238
|
+
expect(result.tripwireTriggered).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('returns the first tripwire result', async () => {
|
|
242
|
+
const guard = buildPathGuardrail(['/allowed/**']);
|
|
243
|
+
const result = await evaluateGuardrails([guard], JSON.stringify({ path: '/blocked/file.ts' }));
|
|
244
|
+
expect(result.tripwireTriggered).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
260
248
|
// ---------------------------------------------------------------------------
|
|
261
249
|
// Handoff topology
|
|
262
250
|
// ---------------------------------------------------------------------------
|
|
@@ -278,29 +266,33 @@ describe('buildWorkerAgent', () => {
|
|
|
278
266
|
expect(buildWorkerAgent('unknown-archetype', [])).toBeNull();
|
|
279
267
|
});
|
|
280
268
|
|
|
281
|
-
it('returns an
|
|
269
|
+
it('returns an agent for a known archetype', () => {
|
|
282
270
|
const agent = buildWorkerAgent('worker-read', []);
|
|
283
271
|
expect(agent).not.toBeNull();
|
|
272
|
+
expect(agent?.name).toBe('worker-read');
|
|
284
273
|
});
|
|
285
274
|
});
|
|
286
275
|
|
|
287
276
|
describe('buildLeadAgent', () => {
|
|
288
277
|
it('creates a lead agent with handoff workers', () => {
|
|
289
|
-
const worker = buildWorkerAgent('worker-read', [])
|
|
278
|
+
const worker = buildWorkerAgent('worker-read', []);
|
|
279
|
+
if (!worker) throw new Error('worker-read archetype missing');
|
|
290
280
|
const lead = buildLeadAgent('You are a lead.', 'gpt-4.1', [worker], []);
|
|
291
|
-
expect(lead).
|
|
281
|
+
expect(lead.name).toBe('cleo-lead');
|
|
282
|
+
expect(lead.handoffs).toHaveLength(1);
|
|
292
283
|
});
|
|
293
284
|
});
|
|
294
285
|
|
|
295
286
|
describe('buildStandaloneAgent', () => {
|
|
296
|
-
it('creates an agent
|
|
287
|
+
it('creates an agent descriptor', () => {
|
|
297
288
|
const agent = buildStandaloneAgent('Instructions', 'gpt-4.1-mini', []);
|
|
298
|
-
expect(agent).
|
|
289
|
+
expect(agent.name).toBe('cleo-worker');
|
|
290
|
+
expect(agent.model).toBe('gpt-4.1-mini');
|
|
299
291
|
});
|
|
300
292
|
});
|
|
301
293
|
|
|
302
294
|
describe('buildAgentTopology', () => {
|
|
303
|
-
it('returns
|
|
295
|
+
it('returns a standalone agent when tier is worker', () => {
|
|
304
296
|
const agent = buildAgentTopology({
|
|
305
297
|
instructions: 'Do work',
|
|
306
298
|
model: 'gpt-4.1-mini',
|
|
@@ -308,10 +300,11 @@ describe('buildAgentTopology', () => {
|
|
|
308
300
|
handoffNames: ['worker-read'],
|
|
309
301
|
guardrails: [],
|
|
310
302
|
});
|
|
311
|
-
expect(agent).
|
|
303
|
+
expect(agent.name).toBe('cleo-worker');
|
|
304
|
+
expect(agent.handoffs).toBeUndefined();
|
|
312
305
|
});
|
|
313
306
|
|
|
314
|
-
it('returns
|
|
307
|
+
it('returns a lead agent with handoffs when tier is lead', () => {
|
|
315
308
|
const agent = buildAgentTopology({
|
|
316
309
|
instructions: 'Lead the team',
|
|
317
310
|
model: 'gpt-4.1',
|
|
@@ -319,7 +312,8 @@ describe('buildAgentTopology', () => {
|
|
|
319
312
|
handoffNames: ['worker-read', 'worker-write'],
|
|
320
313
|
guardrails: [],
|
|
321
314
|
});
|
|
322
|
-
expect(agent).
|
|
315
|
+
expect(agent.name).toBe('cleo-lead');
|
|
316
|
+
expect(agent.handoffs).toHaveLength(2);
|
|
323
317
|
});
|
|
324
318
|
|
|
325
319
|
it('handles unknown archetype names gracefully', () => {
|
|
@@ -330,10 +324,11 @@ describe('buildAgentTopology', () => {
|
|
|
330
324
|
handoffNames: ['worker-read', 'nonexistent-worker'],
|
|
331
325
|
guardrails: [],
|
|
332
326
|
});
|
|
333
|
-
expect(agent).
|
|
327
|
+
expect(agent.name).toBe('cleo-lead');
|
|
328
|
+
expect(agent.handoffs).toHaveLength(1);
|
|
334
329
|
});
|
|
335
330
|
|
|
336
|
-
it('
|
|
331
|
+
it('falls back to standalone when tier is lead but no valid handoffs', () => {
|
|
337
332
|
const agent = buildAgentTopology({
|
|
338
333
|
instructions: 'Lead',
|
|
339
334
|
model: 'gpt-4.1',
|
|
@@ -341,7 +336,7 @@ describe('buildAgentTopology', () => {
|
|
|
341
336
|
handoffNames: [],
|
|
342
337
|
guardrails: [],
|
|
343
338
|
});
|
|
344
|
-
expect(agent).
|
|
339
|
+
expect(agent.name).toBe('cleo-worker');
|
|
345
340
|
});
|
|
346
341
|
});
|
|
347
342
|
|
|
@@ -355,12 +350,14 @@ describe('OpenAiSdkSpawnProvider', () => {
|
|
|
355
350
|
beforeEach(() => {
|
|
356
351
|
provider = new OpenAiSdkSpawnProvider();
|
|
357
352
|
mockRunState.shouldThrow = false;
|
|
358
|
-
mockRunState.
|
|
359
|
-
|
|
353
|
+
mockRunState.output = 'completed output';
|
|
354
|
+
generateTextCalls.length = 0;
|
|
355
|
+
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
360
356
|
});
|
|
361
357
|
|
|
362
358
|
afterEach(() => {
|
|
363
359
|
vi.clearAllMocks();
|
|
360
|
+
delete process.env.OPENAI_API_KEY;
|
|
364
361
|
});
|
|
365
362
|
|
|
366
363
|
describe('canSpawn', () => {
|
|
@@ -376,7 +373,6 @@ describe('OpenAiSdkSpawnProvider', () => {
|
|
|
376
373
|
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
377
374
|
const result = await provider.canSpawn();
|
|
378
375
|
expect(result).toBe(true);
|
|
379
|
-
delete process.env.OPENAI_API_KEY;
|
|
380
376
|
});
|
|
381
377
|
});
|
|
382
378
|
|
|
@@ -439,6 +435,27 @@ describe('OpenAiSdkSpawnProvider', () => {
|
|
|
439
435
|
options: { model: 'gpt-4o', tier: 'worker', tracingDisabled: true },
|
|
440
436
|
});
|
|
441
437
|
expect(result.status).toBe('completed');
|
|
438
|
+
const model = generateTextCalls[0]?.model as
|
|
439
|
+
| { __cleoMockModel: true; modelId: string }
|
|
440
|
+
| undefined;
|
|
441
|
+
expect(model?.modelId).toBe('gpt-4o');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('spawn — guardrail tripwire', () => {
|
|
446
|
+
it('aborts with failed status when a guardrail trips', async () => {
|
|
447
|
+
const result = await provider.spawn({
|
|
448
|
+
taskId: 'T582-guard',
|
|
449
|
+
prompt: JSON.stringify({ path: '/etc/shadow' }),
|
|
450
|
+
options: {
|
|
451
|
+
allowedGlobs: ['/mnt/projects/**'],
|
|
452
|
+
tier: 'worker',
|
|
453
|
+
tracingDisabled: true,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
expect(result.status).toBe('failed');
|
|
457
|
+
expect(result.error).toContain('guardrail tripped');
|
|
458
|
+
expect(generateTextCalls.length).toBe(0);
|
|
442
459
|
});
|
|
443
460
|
});
|
|
444
461
|
});
|
|
@@ -461,8 +478,9 @@ describe('OpenAiSdkAdapter', () => {
|
|
|
461
478
|
|
|
462
479
|
describe('identity', () => {
|
|
463
480
|
it('has id openai-sdk', () => expect(adapter.id).toBe('openai-sdk'));
|
|
464
|
-
it('has name OpenAI
|
|
465
|
-
|
|
481
|
+
it('has display name "OpenAI SDK (Vercel AI SDK)"', () =>
|
|
482
|
+
expect(adapter.name).toBe('OpenAI SDK (Vercel AI SDK)'));
|
|
483
|
+
it('has version 2.0.0', () => expect(adapter.version).toBe('2.0.0'));
|
|
466
484
|
});
|
|
467
485
|
|
|
468
486
|
describe('capabilities', () => {
|
|
@@ -602,30 +620,30 @@ describe('CleoConduitTraceProcessor', () => {
|
|
|
602
620
|
|
|
603
621
|
describe('onTraceStart', () => {
|
|
604
622
|
it('resolves without error', async () => {
|
|
605
|
-
await expect(processor.onTraceStart({}
|
|
623
|
+
await expect(processor.onTraceStart({})).resolves.toBeUndefined();
|
|
606
624
|
});
|
|
607
625
|
});
|
|
608
626
|
|
|
609
627
|
describe('onTraceEnd', () => {
|
|
610
628
|
it('resolves without error', async () => {
|
|
611
|
-
await expect(processor.onTraceEnd({}
|
|
629
|
+
await expect(processor.onTraceEnd({})).resolves.toBeUndefined();
|
|
612
630
|
});
|
|
613
631
|
});
|
|
614
632
|
|
|
615
633
|
describe('onSpanStart', () => {
|
|
616
634
|
it('resolves without error', async () => {
|
|
617
|
-
await expect(processor.onSpanStart(
|
|
635
|
+
await expect(processor.onSpanStart(makeSpanLike())).resolves.toBeUndefined();
|
|
618
636
|
});
|
|
619
637
|
});
|
|
620
638
|
|
|
621
639
|
describe('onSpanEnd', () => {
|
|
622
640
|
it('does not throw for a well-formed span', async () => {
|
|
623
641
|
const span = makeSpanLike();
|
|
624
|
-
await expect(processor.onSpanEnd(span
|
|
642
|
+
await expect(processor.onSpanEnd(span)).resolves.toBeUndefined();
|
|
625
643
|
});
|
|
626
644
|
|
|
627
645
|
it('does not throw for a span with missing fields', async () => {
|
|
628
|
-
await expect(processor.onSpanEnd({
|
|
646
|
+
await expect(processor.onSpanEnd({ spanId: 'x' })).resolves.toBeUndefined();
|
|
629
647
|
});
|
|
630
648
|
});
|
|
631
649
|
|
|
@@ -643,24 +661,26 @@ describe('CleoConduitTraceProcessor', () => {
|
|
|
643
661
|
});
|
|
644
662
|
|
|
645
663
|
// ---------------------------------------------------------------------------
|
|
646
|
-
// Handoff integration: lead + workers with mocked
|
|
664
|
+
// Handoff integration: lead + workers with mocked generateText
|
|
647
665
|
// ---------------------------------------------------------------------------
|
|
648
666
|
|
|
649
|
-
describe('Handoff integration — lead routes to workers via SDK', () => {
|
|
667
|
+
describe('Handoff integration — lead routes to workers via Vercel AI SDK', () => {
|
|
650
668
|
let provider: OpenAiSdkSpawnProvider;
|
|
651
669
|
|
|
652
670
|
beforeEach(() => {
|
|
653
671
|
provider = new OpenAiSdkSpawnProvider();
|
|
654
672
|
mockRunState.shouldThrow = false;
|
|
655
|
-
mockRunState.
|
|
656
|
-
|
|
673
|
+
mockRunState.output = 'handoff result';
|
|
674
|
+
generateTextCalls.length = 0;
|
|
675
|
+
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
657
676
|
});
|
|
658
677
|
|
|
659
678
|
afterEach(() => {
|
|
660
679
|
vi.clearAllMocks();
|
|
680
|
+
delete process.env.OPENAI_API_KEY;
|
|
661
681
|
});
|
|
662
682
|
|
|
663
|
-
it('
|
|
683
|
+
it('executes the lead then each handoff worker sequentially', async () => {
|
|
664
684
|
const result = await provider.spawn({
|
|
665
685
|
taskId: 'T582-handoff',
|
|
666
686
|
prompt: 'Research and implement feature',
|
|
@@ -672,12 +692,8 @@ describe('Handoff integration — lead routes to workers via SDK', () => {
|
|
|
672
692
|
});
|
|
673
693
|
|
|
674
694
|
expect(result.status).toBe('completed');
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// A cleo-lead agent should have been created with 2 handoff workers
|
|
678
|
-
const leadAgent = createdAgents.find((a) => a.name === 'cleo-lead');
|
|
679
|
-
expect(leadAgent).toBeDefined();
|
|
680
|
-
expect(leadAgent?.handoffs).toHaveLength(2);
|
|
695
|
+
// 1 lead + 2 workers = 3 generateText calls
|
|
696
|
+
expect(generateTextCalls.length).toBe(3);
|
|
681
697
|
});
|
|
682
698
|
|
|
683
699
|
it('result reflects correct providerId and taskId', async () => {
|
|
@@ -693,7 +709,6 @@ describe('Handoff integration — lead routes to workers via SDK', () => {
|
|
|
693
709
|
});
|
|
694
710
|
|
|
695
711
|
it('handoff workers use worker archetype model (gpt-4.1-mini)', async () => {
|
|
696
|
-
createdAgents.length = 0;
|
|
697
712
|
await provider.spawn({
|
|
698
713
|
taskId: 'T582-model',
|
|
699
714
|
prompt: 'Work',
|
|
@@ -704,13 +719,29 @@ describe('Handoff integration — lead routes to workers via SDK', () => {
|
|
|
704
719
|
},
|
|
705
720
|
});
|
|
706
721
|
|
|
707
|
-
//
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
722
|
+
// Workers receive gpt-4.1-mini — calls[1] and calls[2] correspond to the two workers.
|
|
723
|
+
const workerCallA = generateTextCalls[1]?.model as
|
|
724
|
+
| { __cleoMockModel: true; modelId: string }
|
|
725
|
+
| undefined;
|
|
726
|
+
const workerCallB = generateTextCalls[2]?.model as
|
|
727
|
+
| { __cleoMockModel: true; modelId: string }
|
|
728
|
+
| undefined;
|
|
729
|
+
expect(workerCallA?.modelId).toBe('gpt-4.1-mini');
|
|
730
|
+
expect(workerCallB?.modelId).toBe('gpt-4.1-mini');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('aggregates lead + worker outputs in the final SpawnResult', async () => {
|
|
734
|
+
mockRunState.output = 'step complete';
|
|
735
|
+
const result = await provider.spawn({
|
|
736
|
+
taskId: 'T582-output',
|
|
737
|
+
prompt: 'Do',
|
|
738
|
+
options: { tier: 'lead', handoffs: ['worker-read'], tracingDisabled: true },
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
expect(result.output).toContain('step complete');
|
|
742
|
+
expect(result.output).toContain('[worker-read]');
|
|
715
743
|
});
|
|
716
744
|
});
|
|
745
|
+
|
|
746
|
+
// Dummy reference so type-only import doesn't get tree-shaken before test runs.
|
|
747
|
+
void ({} as CleoAgent);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenAI
|
|
2
|
+
* OpenAI SDK Adapter — Vercel AI SDK edition.
|
|
3
3
|
*
|
|
4
|
-
* Main `CLEOProviderAdapter` implementation for the OpenAI
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Main `CLEOProviderAdapter` implementation for the OpenAI provider, backed
|
|
5
|
+
* by the Vercel AI SDK (`ai` v6 + `@ai-sdk/openai`). Provides spawn and
|
|
6
|
+
* install capabilities. Hooks are not supported — the Vercel AI SDK does not
|
|
7
|
+
* expose a CLI hook system equivalent to Claude Code's.
|
|
7
8
|
*
|
|
8
|
-
* @task T582
|
|
9
|
+
* @task T582 (original)
|
|
10
|
+
* @task T933 (SDK consolidation — Vercel AI SDK migration)
|
|
11
|
+
* @see ADR-052 — SDK consolidation decision
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import type {
|
|
@@ -17,29 +20,27 @@ import { OpenAiSdkInstallProvider } from './install.js';
|
|
|
17
20
|
import { OpenAiSdkSpawnProvider } from './spawn.js';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
* CLEO provider adapter for the OpenAI
|
|
23
|
+
* CLEO provider adapter for the OpenAI provider.
|
|
21
24
|
*
|
|
22
|
-
* Bridges CLEO's adapter system with the
|
|
23
|
-
* - Spawn: Launches agents via the SDK
|
|
25
|
+
* Bridges CLEO's adapter system with the Vercel AI SDK:
|
|
26
|
+
* - Spawn: Launches agents via the SDK with CLEO-native handoff topology
|
|
24
27
|
* - Install: Manages AGENTS.md @-references and .openai/ config directory
|
|
25
28
|
* - Tracing: Default-on conduit span persistence via `CleoConduitTraceProcessor`
|
|
26
29
|
*
|
|
27
30
|
* @remarks
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* This is also the only provider supporting 100+ LLMs via the Vercel AI SDK
|
|
34
|
-
* bridge (capability flag: `supportsMultiModel`).
|
|
31
|
+
* Handoff topology is CLEO-owned (see `handoff.ts`): lead agents delegate to
|
|
32
|
+
* worker archetypes in sequence, and the concatenated output is returned.
|
|
33
|
+
* The Vercel AI SDK surface (`generateText` / `streamText`) works uniformly
|
|
34
|
+
* across Anthropic, OpenAI, and compatible providers, so the provider keeps
|
|
35
|
+
* the `supportsMultiModel` capability flag.
|
|
35
36
|
*/
|
|
36
37
|
export class OpenAiSdkAdapter implements CLEOProviderAdapter {
|
|
37
38
|
/** Unique provider identifier. */
|
|
38
39
|
readonly id = 'openai-sdk';
|
|
39
40
|
/** Human-readable provider name. */
|
|
40
|
-
readonly name = 'OpenAI
|
|
41
|
+
readonly name = 'OpenAI SDK (Vercel AI SDK)';
|
|
41
42
|
/** Adapter version string. */
|
|
42
|
-
readonly version = '
|
|
43
|
+
readonly version = '2.0.0';
|
|
43
44
|
|
|
44
45
|
/** Declared capabilities for this provider. */
|
|
45
46
|
capabilities: AdapterCapabilities = {
|
|
@@ -117,7 +118,7 @@ export class OpenAiSdkAdapter implements CLEOProviderAdapter {
|
|
|
117
118
|
details: {
|
|
118
119
|
apiKeyPresent,
|
|
119
120
|
projectDir: this.projectDir,
|
|
120
|
-
sdkVersion: '
|
|
121
|
+
sdkVersion: 'ai@6 + @ai-sdk/openai',
|
|
121
122
|
},
|
|
122
123
|
};
|
|
123
124
|
}
|