@cleocode/adapters 2026.4.92 → 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.
Files changed (36) hide show
  1. package/dist/index.js +40795 -18064
  2. package/dist/index.js.map +4 -4
  3. package/dist/providers/claude-sdk/index.d.ts +10 -4
  4. package/dist/providers/claude-sdk/index.d.ts.map +1 -1
  5. package/dist/providers/claude-sdk/spawn.d.ts +29 -28
  6. package/dist/providers/claude-sdk/spawn.d.ts.map +1 -1
  7. package/dist/providers/openai-sdk/adapter.d.ts +18 -17
  8. package/dist/providers/openai-sdk/adapter.d.ts.map +1 -1
  9. package/dist/providers/openai-sdk/guardrails.d.ts +71 -18
  10. package/dist/providers/openai-sdk/guardrails.d.ts.map +1 -1
  11. package/dist/providers/openai-sdk/handoff.d.ts +51 -21
  12. package/dist/providers/openai-sdk/handoff.d.ts.map +1 -1
  13. package/dist/providers/openai-sdk/index.d.ts +8 -5
  14. package/dist/providers/openai-sdk/index.d.ts.map +1 -1
  15. package/dist/providers/openai-sdk/install.d.ts +1 -1
  16. package/dist/providers/openai-sdk/spawn.d.ts +54 -21
  17. package/dist/providers/openai-sdk/spawn.d.ts.map +1 -1
  18. package/dist/providers/openai-sdk/tracing.d.ts +87 -21
  19. package/dist/providers/openai-sdk/tracing.d.ts.map +1 -1
  20. package/dist/providers/shared/sdk-result-mapper.d.ts +9 -7
  21. package/dist/providers/shared/sdk-result-mapper.d.ts.map +1 -1
  22. package/package.json +6 -5
  23. package/src/__tests__/harness-interop.test.ts +451 -0
  24. package/src/providers/claude-sdk/__tests__/spawn.test.ts +100 -265
  25. package/src/providers/claude-sdk/index.ts +10 -4
  26. package/src/providers/claude-sdk/spawn.ts +69 -106
  27. package/src/providers/openai-sdk/__tests__/openai-sdk-spawn.test.ts +134 -103
  28. package/src/providers/openai-sdk/adapter.ts +19 -18
  29. package/src/providers/openai-sdk/guardrails.ts +106 -25
  30. package/src/providers/openai-sdk/handoff.ts +73 -37
  31. package/src/providers/openai-sdk/index.ts +28 -4
  32. package/src/providers/openai-sdk/install.ts +1 -1
  33. package/src/providers/openai-sdk/manifest.json +4 -4
  34. package/src/providers/openai-sdk/spawn.ts +213 -48
  35. package/src/providers/openai-sdk/tracing.ts +105 -22
  36. package/src/providers/shared/sdk-result-mapper.ts +9 -7
@@ -1,21 +1,26 @@
1
1
  /**
2
- * Claude SDK Spawn Provider
2
+ * Claude SDK Spawn Provider — Vercel AI SDK edition.
3
3
  *
4
- * Implements `AdapterSpawnProvider` using the `@anthropic-ai/claude-agent-sdk`
5
- * programmatic API instead of shelling out to the `claude` CLI.
4
+ * Implements {@link AdapterSpawnProvider} using the Vercel AI SDK
5
+ * (`ai` v6 + `@ai-sdk/anthropic`) instead of the legacy
6
+ * `@anthropic-ai/claude-agent-sdk`. CLEO retains its own orchestration
7
+ * primitives (`composeSpawnPayload`, playbook runtime, agent registry);
8
+ * the SDK is strictly the LLM bridge.
6
9
  *
7
10
  * Differences from `ClaudeCodeSpawnProvider`:
8
- * - Uses SDK `query()` instead of a detached child process
11
+ * - Uses `generateText()` via Vercel AI SDK instead of a detached child process
9
12
  * - Awaits full completion before returning (synchronous output capture)
10
- * - Session IDs from the SDK enable future multi-turn resumption
13
+ * - Session IDs are generated by CLEO and tracked in `SessionStore`
11
14
  * - No temp files, no OS PIDs — tracking is purely in-memory session IDs
12
15
  * - `canSpawn()` uses 3-tier key resolution (env var → stored key → Claude Code OAuth)
13
16
  *
14
17
  * CANT enrichment is identical to the CLI provider: `buildCantEnrichedPrompt()`
15
- * is called before `query()` and the result is passed as the SDK prompt string.
18
+ * is called before `generateText()` and the result is passed as the user prompt.
16
19
  *
17
- * @task T581
20
+ * @task T581 (original)
21
+ * @task T933 (SDK consolidation — Vercel AI SDK migration)
18
22
  * @see T752 — canSpawn() OAuth fix
23
+ * @see ADR-052 — SDK consolidation decision
19
24
  */
20
25
 
21
26
  import { existsSync, readFileSync } from 'node:fs';
@@ -23,7 +28,6 @@ import { homedir } from 'node:os';
23
28
  import { join } from 'node:path';
24
29
  import type { AdapterSpawnProvider, SpawnContext, SpawnResult } from '@cleocode/contracts';
25
30
  import { getErrorMessage } from '@cleocode/contracts';
26
- import { getServers } from './mcp-registry.js';
27
31
  import { SessionStore } from './session-store.js';
28
32
  import { resolveTools } from './tool-bridge.js';
29
33
 
@@ -86,19 +90,19 @@ function resolveAnthropicApiKey(): string | null {
86
90
  const DEFAULT_MODEL = 'claude-sonnet-4-5';
87
91
 
88
92
  /**
89
- * Spawn provider that uses the Anthropic Claude Agent SDK for programmatic
90
- * subagent execution.
93
+ * Spawn provider that uses the Vercel AI SDK (`ai` v6 + `@ai-sdk/anthropic`)
94
+ * for programmatic subagent execution.
91
95
  *
92
- * Each call to `spawn()` runs a full SDK `query()` to completion and
93
- * captures the output. Sessions are tracked in `SessionStore` so callers
94
- * can inspect active sessions via `listRunning()` and cancel them via
95
- * `terminate()`.
96
+ * Each call to `spawn()` runs a single `generateText()` call to completion and
97
+ * captures the output. Sessions are tracked in `SessionStore` so callers can
98
+ * inspect active sessions via `listRunning()` and remove them via `terminate()`.
96
99
  *
97
100
  * @remarks
98
- * The `permissionMode: 'bypassPermissions'` + `allowDangerouslySkipPermissions: true`
99
- * combination mirrors the `--dangerously-skip-permissions` flag used by
100
- * the CLI provider. Both are required by the SDK when bypassing all tool
101
- * permission prompts.
101
+ * Unlike the legacy `@anthropic-ai/claude-agent-sdk`, the Vercel AI SDK is
102
+ * strictly an LLM bridge it does not manage a Claude Code subprocess or
103
+ * provide built-in MCP server wiring. Tool use is available through the SDK's
104
+ * tool system, but CLEO orchestration (composeSpawnPayload, playbooks, the
105
+ * agent registry) remains the source of truth for scaffolding.
102
106
  */
103
107
  export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
104
108
  /** In-memory session registry. */
@@ -112,9 +116,6 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
112
116
  * - `~/.local/share/cleo/anthropic-key` (user-stored via cleo config)
113
117
  * - Claude Code OAuth token (zero-config for Claude Code users)
114
118
  *
115
- * No binary check is needed because the SDK manages the Claude Code
116
- * subprocess internally.
117
- *
118
119
  * @returns `true` when any Anthropic credential is available
119
120
  */
120
121
  async canSpawn(): Promise<boolean> {
@@ -122,24 +123,24 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
122
123
  }
123
124
 
124
125
  /**
125
- * Spawn a subagent using the Claude Agent SDK.
126
+ * Spawn a subagent using the Vercel AI SDK.
126
127
  *
127
- * Enriches the prompt via CANT context, runs the SDK `query()` to
128
- * completion, captures all assistant text output, and returns a
129
- * `SpawnResult` with the final output and exit code.
128
+ * Enriches the prompt via CANT context, runs `generateText()` to completion,
129
+ * and returns a `SpawnResult` with the final output and exit code.
130
130
  *
131
131
  * @param context - Spawn context with taskId, prompt, options
132
132
  * @returns Resolved spawn result (status: 'completed' or 'failed')
133
133
  */
134
134
  async spawn(context: SpawnContext): Promise<SpawnResult> {
135
135
  const instanceId = `sdk-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
136
+ const sessionId = `cleo-${instanceId}`;
136
137
  const startTime = new Date().toISOString();
137
138
 
138
139
  // Register in session store immediately so listRunning() reflects it
139
- // before the async query starts.
140
+ // before the async generation starts.
140
141
  this.sessions.add({
141
142
  instanceId,
142
- sessionId: undefined,
143
+ sessionId,
143
144
  taskId: context.taskId,
144
145
  startTime,
145
146
  });
@@ -158,92 +159,54 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
158
159
  // CANT enrichment unavailable — use raw prompt
159
160
  }
160
161
 
161
- // Lazy-import the SDK to avoid hard failures when ANTHROPIC_API_KEY
162
- // is absent (canSpawn() guards the normal path, but tests may import
163
- // this module without the key).
164
- const { query } = await import('@anthropic-ai/claude-agent-sdk');
162
+ // Lazy-import the SDK to avoid hard failures when credentials are absent
163
+ // (canSpawn() guards the normal path, but tests may import this module
164
+ // without a key).
165
+ const { createAnthropic } = await import('@ai-sdk/anthropic');
166
+ const { generateText } = await import('ai');
165
167
 
166
- // Build allowedTools from the spawn context or fall back to CLEO defaults.
168
+ const apiKey = resolveAnthropicApiKey();
169
+ if (!apiKey) {
170
+ this.sessions.remove(instanceId);
171
+ return {
172
+ instanceId,
173
+ taskId: context.taskId,
174
+ providerId: 'claude-sdk',
175
+ status: 'failed',
176
+ startTime,
177
+ endTime: new Date().toISOString(),
178
+ exitCode: 1,
179
+ error: 'No Anthropic credentials available (env, stored key, or Claude Code OAuth)',
180
+ };
181
+ }
182
+
183
+ // Build the Anthropic provider with the resolved key.
184
+ const anthropicProvider = createAnthropic({ apiKey });
185
+
186
+ // Record the tool allowlist in metadata even though the AI SDK lacks
187
+ // built-in tool execution for arbitrary Claude Code tools. Downstream
188
+ // orchestration (composeSpawnPayload) applies allowlists before prompt
189
+ // composition; this array is surfaced so telemetry remains complete.
167
190
  const toolAllowlist = context.options?.toolAllowlist as string[] | undefined;
168
191
  const allowedTools = resolveTools(toolAllowlist);
169
192
 
170
- // Resolve available MCP servers for the working directory.
171
- const workDir = context.workingDirectory ?? process.cwd();
172
- const mcpServers = getServers(workDir);
173
-
174
- // Resume support: pass a prior session ID if provided.
175
- const resumeSessionId = context.options?.resumeSessionId as string | undefined;
193
+ const modelId = (context.options?.model as string) ?? DEFAULT_MODEL;
176
194
 
177
- const sdkQuery = query({
195
+ const result = await generateText({
196
+ model: anthropicProvider(modelId),
178
197
  prompt: enrichedPrompt,
179
- options: {
180
- cwd: workDir,
181
- model: (context.options?.model as string) ?? DEFAULT_MODEL,
182
- allowedTools,
183
- permissionMode: 'bypassPermissions',
184
- allowDangerouslySkipPermissions: true,
185
- ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
186
- ...(resumeSessionId ? { resume: resumeSessionId } : {}),
198
+ providerOptions: {
199
+ anthropic: {
200
+ // Preserve allowlist metadata for trace visibility.
201
+ cleoAllowedTools: allowedTools,
202
+ },
187
203
  },
188
204
  });
189
205
 
190
- // Stream messages from the SDK, collecting text output and session ID.
191
- const textParts: string[] = [];
192
- let exitCode = 0;
193
- let finalError: string | undefined;
194
-
195
- for await (const message of sdkQuery) {
196
- // Capture the session ID from the first message that carries it.
197
- if ('session_id' in message && typeof message.session_id === 'string') {
198
- this.sessions.setSessionId(instanceId, message.session_id);
199
- }
200
-
201
- if (message.type === 'assistant') {
202
- // Aggregate text blocks from assistant messages.
203
- for (const block of message.message.content) {
204
- if (block.type === 'text') {
205
- textParts.push(block.text);
206
- }
207
- }
208
- } else if (message.type === 'result') {
209
- if (message.subtype === 'success') {
210
- // The result field on success contains the final summary text.
211
- if (message.result) {
212
- textParts.push(message.result);
213
- }
214
- exitCode = message.is_error ? 1 : 0;
215
- } else {
216
- // Error subtypes: error_max_turns, error_during_execution, etc.
217
- exitCode = 1;
218
- if ('errors' in message && Array.isArray(message.errors) && message.errors.length > 0) {
219
- finalError = (message.errors as string[]).join('; ');
220
- } else {
221
- // Fallback: use the subtype string as the error description so
222
- // `finalError` is always truthy for non-success result messages.
223
- finalError = String(message.subtype);
224
- }
225
- }
226
- }
227
- }
228
-
229
206
  const endTime = new Date().toISOString();
230
207
  this.sessions.remove(instanceId);
231
208
 
232
- const output = textParts.join('\n').trim();
233
-
234
- if (finalError) {
235
- return {
236
- instanceId,
237
- taskId: context.taskId,
238
- providerId: 'claude-sdk',
239
- status: 'failed',
240
- output,
241
- exitCode,
242
- startTime,
243
- endTime,
244
- error: finalError,
245
- };
246
- }
209
+ const output = (result.text ?? '').trim();
247
210
 
248
211
  return {
249
212
  instanceId,
@@ -251,7 +214,7 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
251
214
  providerId: 'claude-sdk',
252
215
  status: 'completed',
253
216
  output,
254
- exitCode,
217
+ exitCode: 0,
255
218
  startTime,
256
219
  endTime,
257
220
  };
@@ -293,10 +256,10 @@ export class ClaudeSDKSpawnProvider implements AdapterSpawnProvider {
293
256
  /**
294
257
  * Remove a session from tracking.
295
258
  *
296
- * The underlying SDK query runs inside `spawn()` and cannot be cancelled
297
- * externally once the async iterator is in flight. Removing the entry from
298
- * the store prevents it from appearing in `listRunning()` but does not
299
- * interrupt the in-progress HTTP request.
259
+ * The underlying `generateText()` call runs inside `spawn()` and cannot be
260
+ * cancelled externally once the HTTP request is in flight. Removing the
261
+ * entry from the store prevents it from appearing in `listRunning()` but
262
+ * does not interrupt the in-progress request.
300
263
  *
301
264
  * @param instanceId - ID of the spawn instance to terminate
302
265
  */