@cleocode/adapters 2026.4.47 → 2026.4.49

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 (61) hide show
  1. package/dist/cant-context.d.ts +132 -1
  2. package/dist/cant-context.d.ts.map +1 -1
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +19765 -375
  6. package/dist/index.js.map +4 -4
  7. package/dist/providers/claude-code/adapter.d.ts +12 -6
  8. package/dist/providers/claude-code/adapter.d.ts.map +1 -1
  9. package/dist/providers/claude-code/hooks.d.ts.map +1 -1
  10. package/dist/providers/claude-code/spawn.d.ts.map +1 -1
  11. package/dist/providers/claude-sdk/index.d.ts +18 -0
  12. package/dist/providers/claude-sdk/index.d.ts.map +1 -0
  13. package/dist/providers/claude-sdk/mcp-registry.d.ts +40 -0
  14. package/dist/providers/claude-sdk/mcp-registry.d.ts.map +1 -0
  15. package/dist/providers/claude-sdk/session-store.d.ts +78 -0
  16. package/dist/providers/claude-sdk/session-store.d.ts.map +1 -0
  17. package/dist/providers/claude-sdk/spawn.d.ts +79 -0
  18. package/dist/providers/claude-sdk/spawn.d.ts.map +1 -0
  19. package/dist/providers/claude-sdk/tool-bridge.d.ts +38 -0
  20. package/dist/providers/claude-sdk/tool-bridge.d.ts.map +1 -0
  21. package/dist/providers/openai-sdk/adapter.d.ts +77 -0
  22. package/dist/providers/openai-sdk/adapter.d.ts.map +1 -0
  23. package/dist/providers/openai-sdk/guardrails.d.ts +67 -0
  24. package/dist/providers/openai-sdk/guardrails.d.ts.map +1 -0
  25. package/dist/providers/openai-sdk/handoff.d.ts +94 -0
  26. package/dist/providers/openai-sdk/handoff.d.ts.map +1 -0
  27. package/dist/providers/openai-sdk/index.d.ts +39 -0
  28. package/dist/providers/openai-sdk/index.d.ts.map +1 -0
  29. package/dist/providers/openai-sdk/install.d.ts +61 -0
  30. package/dist/providers/openai-sdk/install.d.ts.map +1 -0
  31. package/dist/providers/openai-sdk/spawn.d.ts +146 -0
  32. package/dist/providers/openai-sdk/spawn.d.ts.map +1 -0
  33. package/dist/providers/openai-sdk/tracing.d.ts +89 -0
  34. package/dist/providers/openai-sdk/tracing.d.ts.map +1 -0
  35. package/dist/providers/shared/conduit-trace-writer.d.ts +72 -0
  36. package/dist/providers/shared/conduit-trace-writer.d.ts.map +1 -0
  37. package/dist/providers/shared/sdk-result-mapper.d.ts +51 -0
  38. package/dist/providers/shared/sdk-result-mapper.d.ts.map +1 -0
  39. package/package.json +5 -3
  40. package/src/cant-context.ts +397 -3
  41. package/src/index.ts +24 -2
  42. package/src/providers/claude-code/adapter.ts +41 -4
  43. package/src/providers/claude-code/hooks.ts +7 -1
  44. package/src/providers/claude-code/spawn.ts +5 -1
  45. package/src/providers/claude-sdk/__tests__/spawn.test.ts +448 -0
  46. package/src/providers/claude-sdk/index.ts +18 -0
  47. package/src/providers/claude-sdk/mcp-registry.ts +96 -0
  48. package/src/providers/claude-sdk/session-store.ts +103 -0
  49. package/src/providers/claude-sdk/spawn.ts +242 -0
  50. package/src/providers/claude-sdk/tool-bridge.ts +51 -0
  51. package/src/providers/openai-sdk/__tests__/openai-sdk-spawn.test.ts +716 -0
  52. package/src/providers/openai-sdk/adapter.ts +138 -0
  53. package/src/providers/openai-sdk/guardrails.ts +158 -0
  54. package/src/providers/openai-sdk/handoff.ts +187 -0
  55. package/src/providers/openai-sdk/index.ts +55 -0
  56. package/src/providers/openai-sdk/install.ts +135 -0
  57. package/src/providers/openai-sdk/manifest.json +45 -0
  58. package/src/providers/openai-sdk/spawn.ts +300 -0
  59. package/src/providers/openai-sdk/tracing.ts +175 -0
  60. package/src/providers/shared/conduit-trace-writer.ts +101 -0
  61. package/src/providers/shared/sdk-result-mapper.ts +83 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "id": "openai-sdk",
3
+ "name": "OpenAI Agents SDK Adapter",
4
+ "version": "1.0.0",
5
+ "description": "CLEO adapter for the OpenAI Agents SDK with first-class handoffs and multi-model support",
6
+ "provider": "openai-sdk",
7
+ "entryPoint": "src/providers/openai-sdk/index.ts",
8
+ "capabilities": {
9
+ "supportsHooks": false,
10
+ "supportedHookEvents": [],
11
+ "supportsSpawn": true,
12
+ "supportsInstall": true,
13
+ "supportsMcp": false,
14
+ "supportsInstructionFiles": true,
15
+ "instructionFilePattern": "AGENTS.md",
16
+ "supportsContextMonitor": false,
17
+ "supportsStatusline": false,
18
+ "supportsProviderPaths": false,
19
+ "supportsTransport": false,
20
+ "supportsHandoffs": true,
21
+ "supportsMultiModel": true,
22
+ "supportsTracing": true
23
+ },
24
+ "config": {
25
+ "provider.openai.model": {
26
+ "type": "string",
27
+ "default": "gpt-4.1",
28
+ "description": "Default model for lead/orchestrator agents"
29
+ },
30
+ "provider.openai.workerModel": {
31
+ "type": "string",
32
+ "default": "gpt-4.1-mini",
33
+ "description": "Default model for worker agents"
34
+ },
35
+ "provider.openai.tracingEnabled": {
36
+ "type": "boolean",
37
+ "default": true,
38
+ "description": "Whether to write SDK span events to conduit.db"
39
+ }
40
+ },
41
+ "detectionPatterns": [
42
+ { "type": "env", "pattern": "OPENAI_API_KEY", "description": "OpenAI API key must be set" }
43
+ ],
44
+ "workerArchetypes": ["worker-read", "worker-write", "worker-bash"]
45
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * OpenAI Agents SDK spawn provider.
3
+ *
4
+ * Implements `AdapterSpawnProvider` using the `@openai/agents` SDK runner.
5
+ * Unlike the Claude Code provider (detached fire-and-forget), this provider
6
+ * awaits the run and returns `status: 'completed'` or `status: 'failed'`
7
+ * so the orchestrator receives rich output immediately.
8
+ *
9
+ * Key features:
10
+ * - Tier-based model selection (lead → gpt-4.1, worker → gpt-4.1-mini)
11
+ * - Handoff topology built from `SpawnContext.options.handoffs`
12
+ * - CLEO path ACL guardrails applied at input stage
13
+ * - Default-on tracing via `CleoConduitTraceProcessor`
14
+ * - CANT prompt enrichment (best-effort, same as Claude Code provider)
15
+ *
16
+ * @task T582
17
+ */
18
+
19
+ import type { AdapterSpawnProvider, SpawnContext, SpawnResult } from '@cleocode/contracts';
20
+ import { getErrorMessage } from '@cleocode/contracts';
21
+ import { addTraceProcessor, OpenAIProvider, Runner, setTracingDisabled } from '@openai/agents';
22
+ import { mapSdkRunOutcome } from '../shared/sdk-result-mapper.js';
23
+ import { buildDefaultGuardrails } from './guardrails.js';
24
+ import { buildAgentTopology } from './handoff.js';
25
+ import { CleoConduitTraceProcessor } from './tracing.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * OpenAI SDK-specific spawn options carried in `SpawnContext.options`.
33
+ *
34
+ * @remarks
35
+ * All fields are optional. Unknown fields are ignored.
36
+ */
37
+ export interface OpenAiSdkSpawnOptions {
38
+ /**
39
+ * OpenAI model to use.
40
+ *
41
+ * @defaultValue Derived from `tier`:
42
+ * - `'lead'` / `'orchestrator'` → `'gpt-4.1'`
43
+ * - `'worker'` → `'gpt-4.1-mini'`
44
+ */
45
+ model?: string;
46
+
47
+ /**
48
+ * Agent archetype tier. Controls model selection and topology shape.
49
+ *
50
+ * @defaultValue `'worker'`
51
+ */
52
+ tier?: 'lead' | 'worker' | 'orchestrator';
53
+
54
+ /**
55
+ * Worker archetype names this agent may hand off to.
56
+ * References keys in `WORKER_ARCHETYPES` from `handoff.ts`.
57
+ *
58
+ * @defaultValue `[]`
59
+ */
60
+ handoffs?: string[];
61
+
62
+ /**
63
+ * File-path glob ACL allowlist. Paths outside this list trip the path guardrail.
64
+ *
65
+ * @defaultValue `[]` (all paths allowed)
66
+ */
67
+ allowedGlobs?: string[];
68
+
69
+ /**
70
+ * Tool name allowlist. Tools not in this list are documented but not enforced
71
+ * (enforcement is structural via the `tools` array on the Agent).
72
+ *
73
+ * @defaultValue `[]` (all tools allowed)
74
+ */
75
+ allowedTools?: string[];
76
+
77
+ /**
78
+ * Disable tracing to conduit.db.
79
+ *
80
+ * @defaultValue `false`
81
+ */
82
+ tracingDisabled?: boolean;
83
+
84
+ /**
85
+ * Agent display name used as the CANT persona.
86
+ */
87
+ agentName?: string;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Constants
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const MODEL_LEAD = 'gpt-4.1';
95
+ const MODEL_WORKER = 'gpt-4.1-mini';
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Provider
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * Spawn provider for the OpenAI Agents SDK.
103
+ *
104
+ * Spawns SDK-backed agent runs for a given `SpawnContext`. The run is
105
+ * awaited synchronously and the result mapped to a `SpawnResult` with
106
+ * `status: 'completed'` or `status: 'failed'`. In-flight runs are tracked
107
+ * by instance ID so `listRunning()` and `terminate()` work correctly.
108
+ *
109
+ * @remarks
110
+ * The provider creates a fresh `Runner` per spawn so that `RunConfig`
111
+ * settings do not bleed across parallel spawns. Trace processors are
112
+ * registered globally via `addTraceProcessor` and removed by disabling
113
+ * tracing when the option is set.
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const provider = new OpenAiSdkSpawnProvider();
118
+ * const result = await provider.spawn({
119
+ * taskId: 'T582',
120
+ * prompt: 'Implement feature X',
121
+ * options: { tier: 'lead', handoffs: ['worker-read', 'worker-write'] },
122
+ * });
123
+ * console.log(result.status); // 'completed'
124
+ * ```
125
+ */
126
+ export class OpenAiSdkSpawnProvider implements AdapterSpawnProvider {
127
+ /** Currently running instance IDs (completed runs are removed). */
128
+ private readonly runningInstances = new Set<string>();
129
+
130
+ /**
131
+ * Check whether the OpenAI SDK can spawn in the current environment.
132
+ *
133
+ * Requires `OPENAI_API_KEY` to be set. Does not make a network call.
134
+ *
135
+ * @returns `true` when `OPENAI_API_KEY` is present in the environment.
136
+ */
137
+ async canSpawn(): Promise<boolean> {
138
+ return typeof process.env.OPENAI_API_KEY === 'string' && process.env.OPENAI_API_KEY.length > 0;
139
+ }
140
+
141
+ /**
142
+ * Spawn a subagent via the OpenAI Agents SDK runner.
143
+ *
144
+ * Awaits the run to completion and returns a fully-resolved `SpawnResult`.
145
+ *
146
+ * @param context - Spawn context with task ID, prompt, and options.
147
+ * @returns Resolved spawn result with `status: 'completed'` or `'failed'`.
148
+ */
149
+ async spawn(context: SpawnContext): Promise<SpawnResult> {
150
+ const instanceId = `openai-sdk-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
151
+ const startTime = new Date().toISOString();
152
+
153
+ this.runningInstances.add(instanceId);
154
+
155
+ try {
156
+ const opts = this.parseOptions(context.options);
157
+
158
+ // Enrich prompt with CANT bundle (best-effort, same pattern as Claude Code provider).
159
+ let finalPrompt = context.prompt;
160
+ try {
161
+ const { buildCantEnrichedPrompt } = await import('../../cant-context.js');
162
+ finalPrompt = await buildCantEnrichedPrompt({
163
+ projectDir: context.workingDirectory ?? process.cwd(),
164
+ basePrompt: context.prompt,
165
+ agentName: opts.agentName,
166
+ });
167
+ } catch {
168
+ // CANT enrichment unavailable — use raw prompt.
169
+ }
170
+
171
+ // Build guardrails from ACL options.
172
+ const guardrails = buildDefaultGuardrails(opts.allowedGlobs ?? [], opts.allowedTools ?? []);
173
+
174
+ // Derive model from tier when not explicitly set.
175
+ const model = opts.model ?? this.modelForTier(opts.tier ?? 'worker');
176
+
177
+ // Build agent topology (lead + workers, or standalone worker).
178
+ const agent = buildAgentTopology({
179
+ instructions: finalPrompt,
180
+ model,
181
+ tier: opts.tier ?? 'worker',
182
+ handoffNames: opts.handoffs ?? [],
183
+ guardrails,
184
+ });
185
+
186
+ // Register trace processor globally (default-on).
187
+ // The SDK uses global processor registration via addTraceProcessor().
188
+ let traceProcessor: CleoConduitTraceProcessor | undefined;
189
+ if (!opts.tracingDisabled) {
190
+ traceProcessor = new CleoConduitTraceProcessor(context.taskId);
191
+ addTraceProcessor(traceProcessor);
192
+ } else {
193
+ setTracingDisabled(true);
194
+ }
195
+
196
+ // Build OpenAI model provider and runner.
197
+ const modelProvider = new OpenAIProvider();
198
+ const runner = new Runner({ modelProvider });
199
+
200
+ // Run the agent — no extra options needed beyond what is set on the runner.
201
+ const runResult = await runner.run(agent, finalPrompt);
202
+
203
+ // Restore tracing state if we disabled it for this spawn.
204
+ if (opts.tracingDisabled) {
205
+ setTracingDisabled(false);
206
+ }
207
+
208
+ const finalOutput =
209
+ typeof runResult.finalOutput === 'string'
210
+ ? runResult.finalOutput
211
+ : JSON.stringify(runResult.finalOutput);
212
+
213
+ this.runningInstances.delete(instanceId);
214
+
215
+ return mapSdkRunOutcome(instanceId, context.taskId, 'openai-sdk', startTime, {
216
+ finalOutput,
217
+ succeeded: true,
218
+ });
219
+ } catch (error: unknown) {
220
+ this.runningInstances.delete(instanceId);
221
+
222
+ return mapSdkRunOutcome(instanceId, context.taskId, 'openai-sdk', startTime, {
223
+ finalOutput: '',
224
+ succeeded: false,
225
+ errorMessage: getErrorMessage(error),
226
+ });
227
+ }
228
+ }
229
+
230
+ /**
231
+ * List currently running OpenAI SDK agent instances.
232
+ *
233
+ * @returns Array of in-progress spawn results.
234
+ */
235
+ async listRunning(): Promise<SpawnResult[]> {
236
+ return [...this.runningInstances].map((instanceId) => ({
237
+ instanceId,
238
+ taskId: 'unknown',
239
+ providerId: 'openai-sdk',
240
+ status: 'running' as const,
241
+ startTime: new Date().toISOString(),
242
+ }));
243
+ }
244
+
245
+ /**
246
+ * Terminate a running spawn by instance ID.
247
+ *
248
+ * The OpenAI SDK runner does not support external termination of in-flight
249
+ * runs; this method removes the instance from the tracking set so it will
250
+ * no longer appear in `listRunning()`.
251
+ *
252
+ * @param instanceId - ID of the spawn instance to terminate.
253
+ */
254
+ async terminate(instanceId: string): Promise<void> {
255
+ this.runningInstances.delete(instanceId);
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Private helpers
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Parse and validate `SpawnContext.options` into typed `OpenAiSdkSpawnOptions`.
264
+ *
265
+ * Unknown fields are silently ignored.
266
+ */
267
+ private parseOptions(raw?: Record<string, unknown>): OpenAiSdkSpawnOptions {
268
+ if (!raw) return {};
269
+
270
+ const opts: OpenAiSdkSpawnOptions = {};
271
+
272
+ if (typeof raw.model === 'string') opts.model = raw.model;
273
+ if (raw.tier === 'lead' || raw.tier === 'worker' || raw.tier === 'orchestrator') {
274
+ opts.tier = raw.tier;
275
+ }
276
+ if (Array.isArray(raw.handoffs) && raw.handoffs.every((h) => typeof h === 'string')) {
277
+ opts.handoffs = raw.handoffs as string[];
278
+ }
279
+ if (Array.isArray(raw.allowedGlobs) && raw.allowedGlobs.every((g) => typeof g === 'string')) {
280
+ opts.allowedGlobs = raw.allowedGlobs as string[];
281
+ }
282
+ if (Array.isArray(raw.allowedTools) && raw.allowedTools.every((t) => typeof t === 'string')) {
283
+ opts.allowedTools = raw.allowedTools as string[];
284
+ }
285
+ if (typeof raw.tracingDisabled === 'boolean') opts.tracingDisabled = raw.tracingDisabled;
286
+ if (typeof raw.agentName === 'string') opts.agentName = raw.agentName;
287
+
288
+ return opts;
289
+ }
290
+
291
+ /**
292
+ * Derive the default model for a given tier.
293
+ *
294
+ * @param tier - Agent tier.
295
+ * @returns Model identifier string.
296
+ */
297
+ private modelForTier(tier: 'lead' | 'worker' | 'orchestrator'): string {
298
+ return tier === 'worker' ? MODEL_WORKER : MODEL_LEAD;
299
+ }
300
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * OpenAI Agents SDK trace processor that writes spans to conduit.db.
3
+ *
4
+ * `CleoConduitTraceProcessor` implements the SDK `TracingProcessor` interface.
5
+ * On every span end it serialises the span and writes a structured event to
6
+ * conduit.db via the shared `conduit-trace-writer` transport layer.
7
+ *
8
+ * Tracing is on by default for all OpenAI SDK spawns. Set
9
+ * `options.tracingDisabled = true` in `SpawnContext.options` to opt out when
10
+ * conduit is unavailable.
11
+ *
12
+ * @task T582
13
+ */
14
+
15
+ import type { Span, SpanData, Trace, TracingProcessor } from '@openai/agents';
16
+ import type { ConduitSpanEvent } from '../shared/conduit-trace-writer.js';
17
+ import { writeSpanBatchToConduit } from '../shared/conduit-trace-writer.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Processor
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * CLEO trace processor that persists OpenAI Agents SDK spans to conduit.db.
25
+ *
26
+ * Implements the `TracingProcessor` interface from `@openai/agents-core`.
27
+ * Each `onSpanEnd` call extracts span metadata and enqueues a write to
28
+ * conduit via the shared `conduit-trace-writer` module. Writes are
29
+ * fire-and-forget — failures are logged but never propagated to the caller.
30
+ *
31
+ * @remarks
32
+ * `onTraceEnd` performs a batch flush of any buffered spans. Individual
33
+ * `onSpanEnd` calls also write immediately so spans are not lost if the run
34
+ * is interrupted.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { addTraceProcessor } from '@openai/agents';
39
+ * import { CleoConduitTraceProcessor } from './tracing.js';
40
+ *
41
+ * const processor = new CleoConduitTraceProcessor('T582');
42
+ * addTraceProcessor(processor);
43
+ * ```
44
+ */
45
+ export class CleoConduitTraceProcessor implements TracingProcessor {
46
+ /** CLEO task ID included in every span event for correlation. */
47
+ private readonly taskId: string;
48
+
49
+ /** Pending span events buffered within the current trace. */
50
+ private pendingEvents: ConduitSpanEvent[] = [];
51
+
52
+ /**
53
+ * @param taskId - CLEO task ID to attach to every written span.
54
+ */
55
+ constructor(taskId: string) {
56
+ this.taskId = taskId;
57
+ }
58
+
59
+ /**
60
+ * Called when a new trace starts. Resets the pending event buffer.
61
+ *
62
+ * @param _trace - The trace that just started (unused).
63
+ */
64
+ async onTraceStart(_trace: Trace): Promise<void> {
65
+ this.pendingEvents = [];
66
+ }
67
+
68
+ /**
69
+ * Called when a trace ends. Flushes all pending span events to conduit.
70
+ *
71
+ * @param _trace - The trace that just ended (unused — spans were captured via `onSpanEnd`).
72
+ */
73
+ async onTraceEnd(_trace: Trace): Promise<void> {
74
+ if (this.pendingEvents.length > 0) {
75
+ await writeSpanBatchToConduit(this.pendingEvents);
76
+ this.pendingEvents = [];
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Called when a new span starts. No-op — we capture on end to have full timing.
82
+ *
83
+ * @param _span - The span that just started (unused).
84
+ */
85
+ async onSpanStart(_span: Span<SpanData>): Promise<void> {
86
+ // Capture on end so startedAt / endedAt are both populated.
87
+ }
88
+
89
+ /**
90
+ * Called when a span ends. Serialises and writes the span to conduit.
91
+ *
92
+ * @param span - The completed span from the SDK.
93
+ */
94
+ async onSpanEnd(span: Span<SpanData>): Promise<void> {
95
+ const event = this.extractSpanEvent(span);
96
+ if (event) {
97
+ this.pendingEvents.push(event);
98
+ // Also write immediately to survive partial run interruptions.
99
+ await writeSpanBatchToConduit([event]);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Called during graceful shutdown. Flushes any remaining pending events.
105
+ *
106
+ * @param _timeout - Shutdown timeout in milliseconds (unused).
107
+ */
108
+ async shutdown(_timeout?: number): Promise<void> {
109
+ if (this.pendingEvents.length > 0) {
110
+ await writeSpanBatchToConduit(this.pendingEvents);
111
+ this.pendingEvents = [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Force-flush all pending span events to conduit immediately.
117
+ */
118
+ async forceFlush(): Promise<void> {
119
+ if (this.pendingEvents.length > 0) {
120
+ await writeSpanBatchToConduit(this.pendingEvents);
121
+ this.pendingEvents = [];
122
+ }
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Private helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Extract a {@link ConduitSpanEvent} from an SDK span, or `null` if the
131
+ * span cannot be meaningfully serialised.
132
+ *
133
+ * @param span - The SDK span to serialise.
134
+ * @returns A conduit span event or `null`.
135
+ */
136
+ private extractSpanEvent(span: Span<SpanData>): ConduitSpanEvent | null {
137
+ const spanId =
138
+ span.spanId ?? `span-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
139
+
140
+ // Guard against malformed/empty spans (e.g. in unit tests with minimal mocks).
141
+ if (!span.spanData) return null;
142
+
143
+ const data = span.spanData;
144
+ const spanType = data.type;
145
+
146
+ // Extract agent name from span data shape based on type.
147
+ let agentName = 'unknown';
148
+ let handoffTarget: string | undefined;
149
+
150
+ if (data.type === 'agent') {
151
+ agentName = data.name;
152
+ } else if (data.type === 'handoff') {
153
+ agentName = data.from_agent ?? 'unknown';
154
+ handoffTarget = data.to_agent ?? undefined;
155
+ } else if (data.type === 'function') {
156
+ agentName = data.name;
157
+ }
158
+
159
+ const startTime = span.startedAt ?? new Date().toISOString();
160
+ const endTime = span.endedAt ?? new Date().toISOString();
161
+
162
+ return {
163
+ spanId,
164
+ taskId: this.taskId,
165
+ agentName,
166
+ spanType,
167
+ startTime,
168
+ endTime,
169
+ handoffTarget,
170
+ metadata: {
171
+ rawSpanData: data,
172
+ },
173
+ };
174
+ }
175
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared conduit trace writer for SDK-backed providers.
3
+ *
4
+ * Writes structured span events to conduit.db via the CLEO transport layer.
5
+ * Both T581 (Claude SDK) and T582 (OpenAI SDK) use this module so the
6
+ * conduit write path stays DRY.
7
+ *
8
+ * The writer is fire-and-forget: if conduit is unavailable, write failures are
9
+ * silently swallowed so that missing tracing never breaks agent execution.
10
+ *
11
+ * @task T582
12
+ */
13
+
14
+ /**
15
+ * A single normalised span event written to conduit.
16
+ *
17
+ * @remarks
18
+ * This shape is intentionally minimal — only fields that both SDK providers
19
+ * can populate consistently are required. Provider-specific fields go into
20
+ * the `metadata` bag.
21
+ */
22
+ export interface ConduitSpanEvent {
23
+ /** Unique identifier for this span (from the SDK). */
24
+ spanId: string;
25
+ /** The task ID this span belongs to. */
26
+ taskId: string;
27
+ /** The name of the agent that produced this span. */
28
+ agentName: string;
29
+ /** Type of span: `'agent'`, `'function'`, `'handoff'`, `'generation'`, etc. */
30
+ spanType: string;
31
+ /** ISO timestamp when the span started. */
32
+ startTime: string;
33
+ /** ISO timestamp when the span ended. */
34
+ endTime: string;
35
+ /** Optional tool name for function/tool spans. */
36
+ toolName?: string;
37
+ /** Optional handoff target agent name. */
38
+ handoffTarget?: string;
39
+ /** Extra provider-specific metadata. */
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+
43
+ /** Conduit write result — only used internally for error handling. */
44
+ interface WriteResult {
45
+ written: boolean;
46
+ error?: string;
47
+ }
48
+
49
+ /**
50
+ * Write a single span event to conduit via `cleo` CLI transport.
51
+ *
52
+ * Falls back gracefully when conduit is unavailable. All errors are caught
53
+ * and returned in the result rather than thrown.
54
+ *
55
+ * @param event - The span event to persist.
56
+ * @returns Result indicating whether the write succeeded.
57
+ *
58
+ * @remarks
59
+ * The current implementation writes to conduit using the `cleo conduit send`
60
+ * CLI command. This keeps the trace writer free of direct DB dependencies and
61
+ * consistent with the no-direct-SQLite rule (ADR).
62
+ *
63
+ * When conduit grows a native TS API this writer can be updated without
64
+ * changing caller code.
65
+ */
66
+ export async function writeSpanToConduit(event: ConduitSpanEvent): Promise<WriteResult> {
67
+ try {
68
+ const { exec } = await import('node:child_process');
69
+ const { promisify } = await import('node:util');
70
+ const execAsync = promisify(exec);
71
+
72
+ const payload = JSON.stringify({
73
+ type: 'agent_span',
74
+ version: '1',
75
+ ...event,
76
+ });
77
+
78
+ // Use the CLEO conduit send command which handles DB access via the
79
+ // business logic layer (no direct SQLite per ADR-013 §9).
80
+ await execAsync(`cleo conduit send --type agent_span --payload ${JSON.stringify(payload)}`);
81
+ return { written: true };
82
+ } catch (err: unknown) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ return { written: false, error: message };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Write multiple span events to conduit, swallowing individual write failures.
90
+ *
91
+ * @param events - Array of span events to persist.
92
+ * @returns Number of events successfully written.
93
+ */
94
+ export async function writeSpanBatchToConduit(events: ConduitSpanEvent[]): Promise<number> {
95
+ let written = 0;
96
+ for (const event of events) {
97
+ const result = await writeSpanToConduit(event);
98
+ if (result.written) written++;
99
+ }
100
+ return written;
101
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shared SDK result mapper for CLEO spawn providers.
3
+ *
4
+ * Normalises provider-specific run results (OpenAI Agents SDK, Claude Agent SDK)
5
+ * into the canonical {@link SpawnResult} contract used by CLEO orchestration.
6
+ *
7
+ * Both T581 (Claude SDK) and T582 (OpenAI SDK) import from this module so the
8
+ * mapping logic stays DRY.
9
+ *
10
+ * @task T582
11
+ */
12
+
13
+ import type { SpawnResult } from '@cleocode/contracts';
14
+
15
+ /**
16
+ * Raw run outcome from any SDK provider, normalised before mapping.
17
+ *
18
+ * @remarks
19
+ * Both `@anthropic-ai/claude-agent-sdk` and `@openai/agents` surface a
20
+ * `finalOutput` string plus an optional error. This interface captures the
21
+ * minimal shared shape so the mapper stays provider-agnostic.
22
+ */
23
+ export interface RawSdkRunOutcome {
24
+ /** Final text produced by the agent run. Empty string when the run failed. */
25
+ finalOutput: string;
26
+ /** True when the run completed without error. */
27
+ succeeded: boolean;
28
+ /** Human-readable error message when `succeeded` is false. */
29
+ errorMessage?: string;
30
+ /** Exit / stop reason surfaced by the SDK (optional). */
31
+ stopReason?: string;
32
+ }
33
+
34
+ /**
35
+ * Map a raw SDK run outcome to the canonical CLEO {@link SpawnResult}.
36
+ *
37
+ * @param instanceId - Unique identifier for this spawn instance.
38
+ * @param taskId - CLEO task ID associated with the run.
39
+ * @param providerId - Identifier of the provider that performed the run (e.g. `'openai-sdk'`).
40
+ * @param startTime - ISO timestamp captured just before the run was started.
41
+ * @param outcome - Normalised run outcome from the SDK provider.
42
+ * @returns A fully-populated {@link SpawnResult} ready for return from `spawn()`.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const result = mapSdkRunOutcome('openai-sdk-123', 'T582', 'openai-sdk', start, {
47
+ * finalOutput: 'Done',
48
+ * succeeded: true,
49
+ * });
50
+ * // result.status === 'completed'
51
+ * ```
52
+ */
53
+ export function mapSdkRunOutcome(
54
+ instanceId: string,
55
+ taskId: string,
56
+ providerId: string,
57
+ startTime: string,
58
+ outcome: RawSdkRunOutcome,
59
+ ): SpawnResult {
60
+ const endTime = new Date().toISOString();
61
+
62
+ if (outcome.succeeded) {
63
+ return {
64
+ instanceId,
65
+ taskId,
66
+ providerId,
67
+ status: 'completed',
68
+ output: outcome.finalOutput,
69
+ startTime,
70
+ endTime,
71
+ };
72
+ }
73
+
74
+ return {
75
+ instanceId,
76
+ taskId,
77
+ providerId,
78
+ status: 'failed',
79
+ startTime,
80
+ endTime,
81
+ error: outcome.errorMessage ?? 'SDK run failed without a message',
82
+ };
83
+ }