@smithers-orchestrator/agents 0.16.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +65 -0
  3. package/src/AgentLike.ts +28 -0
  4. package/src/AmpAgent.js +232 -0
  5. package/src/AmpAgentOptions.ts +26 -0
  6. package/src/AnthropicAgent.js +54 -0
  7. package/src/AnthropicAgentOptions.ts +8 -0
  8. package/src/BaseCliAgent/AgentCliActionKind.ts +10 -0
  9. package/src/BaseCliAgent/AgentCliEvent.ts +44 -0
  10. package/src/BaseCliAgent/BaseCliAgent.js +874 -0
  11. package/src/BaseCliAgent/BaseCliAgentOptions.ts +13 -0
  12. package/src/BaseCliAgent/CliOutputInterpreter.ts +8 -0
  13. package/src/BaseCliAgent/CliUsageInfo.ts +7 -0
  14. package/src/BaseCliAgent/CodexConfigOverrides.ts +3 -0
  15. package/src/BaseCliAgent/PiExtensionUiRequest.ts +10 -0
  16. package/src/BaseCliAgent/PiExtensionUiResponse.ts +7 -0
  17. package/src/BaseCliAgent/RunCommandResult.ts +5 -0
  18. package/src/BaseCliAgent/buildGenerateResult.js +57 -0
  19. package/src/BaseCliAgent/combineNonEmpty.js +8 -0
  20. package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +198 -0
  21. package/src/BaseCliAgent/extractPrompt.js +88 -0
  22. package/src/BaseCliAgent/extractTextFromJsonValue.js +46 -0
  23. package/src/BaseCliAgent/index.js +32 -0
  24. package/src/BaseCliAgent/normalizeCodexConfig.js +22 -0
  25. package/src/BaseCliAgent/parseHelpers.js +111 -0
  26. package/src/BaseCliAgent/pushFlag.js +18 -0
  27. package/src/BaseCliAgent/pushList.js +10 -0
  28. package/src/BaseCliAgent/resolveTimeouts.js +24 -0
  29. package/src/BaseCliAgent/runCommandEffect.js +32 -0
  30. package/src/BaseCliAgent/runRpcCommandEffect.js +365 -0
  31. package/src/BaseCliAgent/truncateToBytes.js +13 -0
  32. package/src/BaseCliAgent/tryParseJson.js +18 -0
  33. package/src/ClaudeCodeAgent.js +455 -0
  34. package/src/ClaudeCodeAgentOptions.ts +52 -0
  35. package/src/CodexAgent.js +593 -0
  36. package/src/CodexAgentOptions.ts +23 -0
  37. package/src/ForgeAgent.js +128 -0
  38. package/src/ForgeAgentOptions.ts +14 -0
  39. package/src/GeminiAgent.js +273 -0
  40. package/src/GeminiAgentOptions.ts +20 -0
  41. package/src/KimiAgent.js +260 -0
  42. package/src/KimiAgentOptions.ts +21 -0
  43. package/src/OpenAIAgent.js +54 -0
  44. package/src/OpenAIAgentOptions.ts +8 -0
  45. package/src/PiAgent.js +468 -0
  46. package/src/PiAgentOptions.ts +40 -0
  47. package/src/SdkAgentOptions.ts +16 -0
  48. package/src/agent-contract/SmithersAgentContract.ts +10 -0
  49. package/src/agent-contract/SmithersAgentContractTool.ts +8 -0
  50. package/src/agent-contract/SmithersAgentToolCategory.ts +6 -0
  51. package/src/agent-contract/SmithersListedTool.ts +4 -0
  52. package/src/agent-contract/SmithersToolSurface.ts +1 -0
  53. package/src/agent-contract/createSmithersAgentContract.js +188 -0
  54. package/src/agent-contract/index.js +10 -0
  55. package/src/agent-contract/renderSmithersAgentPromptGuidance.js +81 -0
  56. package/src/capability-registry/AgentCapabilityRegistry.ts +22 -0
  57. package/src/capability-registry/AgentToolDescriptor.ts +4 -0
  58. package/src/capability-registry/hashCapabilityRegistry.js +43 -0
  59. package/src/capability-registry/index.js +8 -0
  60. package/src/capability-registry/normalizeCapabilityRegistry.js +52 -0
  61. package/src/capability-registry/normalizeCapabilityStringList.js +9 -0
  62. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +6 -0
  63. package/src/cli-capabilities/CliAgentCapabilityDoctorReport.ts +18 -0
  64. package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +9 -0
  65. package/src/cli-capabilities/formatCliAgentCapabilityDoctorReport.js +24 -0
  66. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +92 -0
  67. package/src/cli-capabilities/getCliAgentCapabilityReport.js +52 -0
  68. package/src/cli-capabilities/index.js +11 -0
  69. package/src/diagnostics/DiagnosticCheck.ts +11 -0
  70. package/src/diagnostics/DiagnosticCheckId.ts +4 -0
  71. package/src/diagnostics/DiagnosticContext.ts +4 -0
  72. package/src/diagnostics/DiagnosticReport.ts +9 -0
  73. package/src/diagnostics/enrichReportWithErrorAnalysis.js +34 -0
  74. package/src/diagnostics/formatDiagnosticSummary.js +17 -0
  75. package/src/diagnostics/getDiagnosticStrategy.js +503 -0
  76. package/src/diagnostics/index.js +13 -0
  77. package/src/diagnostics/launchDiagnostics.js +16 -0
  78. package/src/diagnostics/runDiagnostics.js +52 -0
  79. package/src/index.d.ts +872 -0
  80. package/src/index.js +39 -0
  81. package/src/resolveSdkModel.js +9 -0
  82. package/src/sanitizeForOpenAI.js +47 -0
  83. package/src/streamResultToGenerateResult.js +70 -0
  84. package/src/zodToOpenAISchema.js +16 -0
@@ -0,0 +1,874 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import { Cause, Effect, Exit, Metric } from "effect";
4
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
5
+ import { logDebug, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
6
+ import { agentDurationMs, agentErrorsTotal, agentInvocationsTotal, agentRetriesTotal, agentTokensTotal, toolOutputTruncatedTotal, } from "@smithers-orchestrator/observability/metrics";
7
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
8
+ import { launchDiagnostics, enrichReportWithErrorAnalysis, formatDiagnosticSummary } from "../diagnostics/index.js";
9
+ import { extractPrompt } from "./extractPrompt.js";
10
+ import { resolveTimeouts } from "./resolveTimeouts.js";
11
+ import { combineNonEmpty } from "./combineNonEmpty.js";
12
+ import { tryParseJson } from "./tryParseJson.js";
13
+ import { extractTextFromJsonValue } from "./extractTextFromJsonValue.js";
14
+ import { createAgentStdoutTextEmitter } from "./createAgentStdoutTextEmitter.js";
15
+ import { buildGenerateResult } from "./buildGenerateResult.js";
16
+ import { runCommandEffect } from "./runCommandEffect.js";
17
+ /** @typedef {import("./AgentCliEvent.ts").AgentCliEvent} AgentCliEvent */
18
+
19
+ /** @typedef {import("./BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
20
+ /** @typedef {import("./CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
21
+ /** @typedef {import("./CliUsageInfo.ts").CliUsageInfo} CliUsageInfo */
22
+ /** @typedef {import("ai").GenerateTextResult} GenerateTextResult */
23
+ /** @typedef {import("ai").StreamTextResult} StreamTextResult */
24
+ /** @typedef {import("ai").LanguageModelUsage} LanguageModelUsage */
25
+ /**
26
+ * @typedef {"generate" | "stream"} AgentInvocationOperation
27
+ */
28
+ /**
29
+ * @typedef {Record<string, string | undefined>} AgentInvocationTags
30
+ */
31
+ /**
32
+ * @typedef {{
33
+ * inputTokens?: number;
34
+ * outputTokens?: number;
35
+ * cacheReadTokens?: number;
36
+ * cacheWriteTokens?: number;
37
+ * reasoningTokens?: number;
38
+ * totalTokens?: number;
39
+ * }} AgentTokenTotals
40
+ */
41
+ /**
42
+ * Loosely-typed generation options. The AI SDK passes a dynamic shape here
43
+ * (GenerateTextOptions / StreamTextOptions and provider-specific extensions)
44
+ * so we keep this permissive but avoid raw `any`.
45
+ * @typedef {{
46
+ * prompt?: unknown;
47
+ * messages?: unknown;
48
+ * timeout?: unknown;
49
+ * abortSignal?: AbortSignal;
50
+ * rootDir?: string;
51
+ * resumeSession?: string;
52
+ * maxOutputBytes?: number;
53
+ * onStdout?: (text: string) => void;
54
+ * onStderr?: (text: string) => void;
55
+ * onEvent?: (event: AgentCliEvent) => unknown;
56
+ * retry?: unknown;
57
+ * isRetry?: unknown;
58
+ * retryAttempt?: unknown;
59
+ * schemaRetry?: unknown;
60
+ * [key: string]: unknown;
61
+ * }} AgentGenerateOptions
62
+ */
63
+
64
+ /**
65
+ * @template A
66
+ * @param {Effect.Effect<A, SmithersError, never>} effect
67
+ * @returns {Promise<A>}
68
+ */
69
+ export async function runAgentPromise(effect) {
70
+ const exit = await Effect.runPromiseExit(effect);
71
+ if (Exit.isSuccess(exit)) {
72
+ return exit.value;
73
+ }
74
+ const failure = Cause.failureOption(exit.cause);
75
+ if (failure._tag === "Some") {
76
+ throw failure.value;
77
+ }
78
+ throw Cause.squash(exit.cause);
79
+ }
80
+ /**
81
+ * @param {unknown} value
82
+ * @returns {string | undefined}
83
+ */
84
+ function normalizeMetricTag(value) {
85
+ if (typeof value !== "string")
86
+ return undefined;
87
+ const trimmed = value.trim();
88
+ return trimmed.length > 0 ? trimmed : undefined;
89
+ }
90
+ /**
91
+ * @template A
92
+ * @param {A} metric
93
+ * @param {Record<string, string | undefined>} tags
94
+ * @returns {A}
95
+ */
96
+ function taggedMetric(metric, tags) {
97
+ let tagged = metric;
98
+ for (const [key, value] of Object.entries(tags)) {
99
+ if (!value)
100
+ continue;
101
+ tagged = Metric.tagged(tagged, key, value);
102
+ }
103
+ return tagged;
104
+ }
105
+ /**
106
+ * @param {BaseCliAgent} agent
107
+ * @param {string} [fallbackCommand]
108
+ * @returns {string}
109
+ */
110
+ function resolveAgentEngineTag(agent, fallbackCommand) {
111
+ return normalizeMetricTag(agent.cliEngine)
112
+ ?? normalizeMetricTag(agent.model)
113
+ ?? normalizeMetricTag(fallbackCommand)
114
+ ?? normalizeMetricTag(agent.constructor?.name)
115
+ ?? "unknown";
116
+ }
117
+ /**
118
+ * @param {unknown} value
119
+ * @returns {number | undefined}
120
+ */
121
+ function asFiniteTokenCount(value) {
122
+ return typeof value === "number" && Number.isFinite(value) && value > 0
123
+ ? value
124
+ : undefined;
125
+ }
126
+ /**
127
+ * @param {unknown} usage
128
+ * @returns {AgentTokenTotals}
129
+ */
130
+ function extractAgentTokenTotals(usage) {
131
+ if (!usage || typeof usage !== "object") {
132
+ return {};
133
+ }
134
+ const u = /** @type {Record<string, unknown>} */ (usage);
135
+ const inputDetails = /** @type {Record<string, unknown> | undefined} */ (
136
+ u.inputTokenDetails && typeof u.inputTokenDetails === "object" ? u.inputTokenDetails : undefined
137
+ );
138
+ const outputDetails = /** @type {Record<string, unknown> | undefined} */ (
139
+ u.outputTokenDetails && typeof u.outputTokenDetails === "object" ? u.outputTokenDetails : undefined
140
+ );
141
+ const inputTokens = asFiniteTokenCount(u.inputTokens)
142
+ ?? asFiniteTokenCount(u.input_tokens)
143
+ ?? asFiniteTokenCount(u.prompt_tokens);
144
+ const outputTokens = asFiniteTokenCount(u.outputTokens)
145
+ ?? asFiniteTokenCount(u.output_tokens)
146
+ ?? asFiniteTokenCount(u.completion_tokens);
147
+ const cacheReadTokens = asFiniteTokenCount(u.cacheReadTokens)
148
+ ?? asFiniteTokenCount(u.cached_input_tokens)
149
+ ?? asFiniteTokenCount(u.cache_read_input_tokens)
150
+ ?? asFiniteTokenCount(inputDetails?.cacheReadTokens);
151
+ const cacheWriteTokens = asFiniteTokenCount(u.cacheWriteTokens)
152
+ ?? asFiniteTokenCount(u.cache_creation_input_tokens)
153
+ ?? asFiniteTokenCount(inputDetails?.cacheWriteTokens);
154
+ const reasoningTokens = asFiniteTokenCount(u.reasoningTokens)
155
+ ?? asFiniteTokenCount(u.reasoning_tokens)
156
+ ?? asFiniteTokenCount(outputDetails?.reasoningTokens);
157
+ const totalTokens = asFiniteTokenCount(u.totalTokens)
158
+ ?? asFiniteTokenCount((inputTokens ?? 0)
159
+ + (outputTokens ?? 0)
160
+ + (cacheReadTokens ?? 0)
161
+ + (cacheWriteTokens ?? 0)
162
+ + (reasoningTokens ?? 0));
163
+ return {
164
+ inputTokens,
165
+ outputTokens,
166
+ cacheReadTokens,
167
+ cacheWriteTokens,
168
+ reasoningTokens,
169
+ totalTokens,
170
+ };
171
+ }
172
+ /**
173
+ * @param {AgentInvocationTags} tags
174
+ * @param {AgentTokenTotals} totals
175
+ * @returns {Effect.Effect<void>}
176
+ */
177
+ function recordAgentTokenMetrics(tags, totals) {
178
+ const effects = [];
179
+ /**
180
+ * @param {string} kind
181
+ * @param {number | undefined} value
182
+ */
183
+ const pushMetric = (kind, value) => {
184
+ if (!value || value <= 0)
185
+ return;
186
+ effects.push(Metric.incrementBy(taggedMetric(agentTokensTotal, {
187
+ ...tags,
188
+ kind,
189
+ }), value));
190
+ };
191
+ pushMetric("input", totals.inputTokens);
192
+ pushMetric("output", totals.outputTokens);
193
+ pushMetric("cache_read", totals.cacheReadTokens);
194
+ pushMetric("cache_write", totals.cacheWriteTokens);
195
+ pushMetric("reasoning", totals.reasoningTokens);
196
+ pushMetric("total", totals.totalTokens);
197
+ return effects.length > 0 ? Effect.all(effects, { discard: true }) : Effect.void;
198
+ }
199
+ /**
200
+ * @param {unknown} options
201
+ * @returns {{ isRetry: boolean; reason?: string }}
202
+ */
203
+ function resolveRetryHint(options) {
204
+ if (!options || typeof options !== "object") return { isRetry: false };
205
+ const o = /** @type {Record<string, unknown>} */ (options);
206
+ if (o.retry === true)
207
+ return { isRetry: true, reason: "retry" };
208
+ if (o.isRetry === true)
209
+ return { isRetry: true, reason: "is_retry" };
210
+ if (typeof o.retryAttempt === "number" && o.retryAttempt > 0) {
211
+ return { isRetry: true, reason: "retry_attempt" };
212
+ }
213
+ if (typeof o.schemaRetry === "number" && o.schemaRetry > 0) {
214
+ return { isRetry: true, reason: "schema_retry" };
215
+ }
216
+ return { isRetry: false };
217
+ }
218
+ /**
219
+ * @param {AgentCliEvent} event
220
+ * @param {Record<string, unknown>} annotations
221
+ * @param {string} span
222
+ */
223
+ function logAgentCliEvent(event, annotations, span) {
224
+ switch (event.type) {
225
+ case "started":
226
+ logInfo("agent session started", {
227
+ ...annotations,
228
+ eventType: event.type,
229
+ eventEngine: event.engine,
230
+ title: event.title,
231
+ resume: event.resume ?? null,
232
+ }, span);
233
+ return;
234
+ case "action":
235
+ logDebug("agent action event", {
236
+ ...annotations,
237
+ eventType: event.type,
238
+ eventEngine: event.engine,
239
+ phase: event.phase,
240
+ actionId: event.action.id,
241
+ actionKind: event.action.kind,
242
+ actionTitle: event.action.title,
243
+ entryType: event.entryType ?? null,
244
+ level: event.level ?? null,
245
+ ok: event.ok ?? null,
246
+ }, span);
247
+ return;
248
+ case "completed":
249
+ (event.ok ? logInfo : logWarning)(event.ok ? "agent session completed" : "agent session failed", {
250
+ ...annotations,
251
+ eventType: event.type,
252
+ eventEngine: event.engine,
253
+ ok: event.ok,
254
+ resume: event.resume ?? null,
255
+ error: event.error ?? null,
256
+ hasUsage: Boolean(event.usage),
257
+ }, span);
258
+ return;
259
+ }
260
+ }
261
+ /**
262
+ * @param {string} raw
263
+ * @returns {string | undefined}
264
+ */
265
+ function extractTextFromJsonPayload(raw) {
266
+ const trimmed = raw.trim();
267
+ if (!trimmed)
268
+ return undefined;
269
+ try {
270
+ const parsed = JSON.parse(trimmed);
271
+ return extractTextFromJsonValue(parsed);
272
+ }
273
+ catch {
274
+ // Possibly JSONL
275
+ }
276
+ const lines = trimmed.split(/\r?\n/).filter(Boolean);
277
+ const parsedLines = [];
278
+ for (const line of lines) {
279
+ try {
280
+ const parsed = JSON.parse(line);
281
+ parsedLines.push(parsed);
282
+ }
283
+ catch {
284
+ continue;
285
+ }
286
+ }
287
+ for (let i = parsedLines.length - 1; i >= 0; i--) {
288
+ const parsed = parsedLines[i];
289
+ const type = typeof parsed?.type === "string" ? parsed.type : "";
290
+ if ((type === "turn_end" || type === "message_end") &&
291
+ parsed?.message?.role === "assistant") {
292
+ const text = extractTextFromJsonValue(parsed.message);
293
+ if (text)
294
+ return text;
295
+ }
296
+ if (type === "agent_end" && Array.isArray(parsed?.messages)) {
297
+ for (let j = parsed.messages.length - 1; j >= 0; j--) {
298
+ const message = parsed.messages[j];
299
+ if (message?.role !== "assistant")
300
+ continue;
301
+ const text = extractTextFromJsonValue(message);
302
+ if (text)
303
+ return text;
304
+ }
305
+ }
306
+ }
307
+ const chunks = [];
308
+ for (const parsed of parsedLines) {
309
+ const text = extractTextFromJsonValue(parsed);
310
+ if (text)
311
+ chunks.push(text);
312
+ }
313
+ return chunks.length ? chunks.join("") : undefined;
314
+ }
315
+ /**
316
+ * @param {string[]} args
317
+ * @returns {string | undefined}
318
+ */
319
+ function inferOutputFormatFromArgs(args) {
320
+ for (let i = 0; i < args.length; i++) {
321
+ const arg = args[i];
322
+ if (arg === "--output-format" || arg === "--mode") {
323
+ return args[i + 1];
324
+ }
325
+ }
326
+ return undefined;
327
+ }
328
+ function emptyUsage() {
329
+ return {
330
+ inputTokens: undefined,
331
+ inputTokenDetails: {
332
+ noCacheTokens: undefined,
333
+ cacheReadTokens: undefined,
334
+ cacheWriteTokens: undefined,
335
+ },
336
+ outputTokens: undefined,
337
+ outputTokenDetails: {
338
+ textTokens: undefined,
339
+ reasoningTokens: undefined,
340
+ },
341
+ totalTokens: undefined,
342
+ };
343
+ }
344
+ /**
345
+ * @template T
346
+ * @param {AsyncIterable<T>} iterable
347
+ * @returns {ReadableStream<T> & AsyncIterable<T>}
348
+ */
349
+ function asyncIterableToStream(iterable) {
350
+ const stream = new ReadableStream({
351
+ async start(controller) {
352
+ try {
353
+ for await (const item of iterable) {
354
+ controller.enqueue(item);
355
+ }
356
+ }
357
+ catch (err) {
358
+ controller.error(err);
359
+ return;
360
+ }
361
+ controller.close();
362
+ },
363
+ });
364
+ stream[Symbol.asyncIterator] =
365
+ iterable[Symbol.asyncIterator].bind(iterable);
366
+ return stream;
367
+ }
368
+ /**
369
+ * @param {GenerateTextResult<Record<string, never>, unknown>} result
370
+ * @returns {StreamTextResult<Record<string, never>, unknown>}
371
+ */
372
+ function buildStreamResult(result) {
373
+ const text = result.text ?? "";
374
+ const content = result.content ?? [];
375
+ const steps = result.steps ?? [];
376
+ const usage = result.usage ?? emptyUsage();
377
+ const totalUsage = result.totalUsage ?? usage;
378
+ const response = result.response ?? {
379
+ id: randomUUID(),
380
+ timestamp: new Date(),
381
+ modelId: "unknown",
382
+ messages: [],
383
+ };
384
+ const request = result.request ?? {};
385
+ const textStream = asyncIterableToStream((async function* () {
386
+ if (text)
387
+ yield text;
388
+ })());
389
+ const fullStream = asyncIterableToStream((async function* () {
390
+ const id = randomUUID();
391
+ yield { type: "text-start", id };
392
+ if (text) {
393
+ yield { type: "text-delta", id, text };
394
+ }
395
+ yield { type: "text-end", id };
396
+ })());
397
+ return {
398
+ content: Promise.resolve(content),
399
+ text: Promise.resolve(text),
400
+ reasoning: Promise.resolve(result.reasoning ?? []),
401
+ reasoningText: Promise.resolve(result.reasoningText),
402
+ files: Promise.resolve(result.files ?? []),
403
+ sources: Promise.resolve(result.sources ?? []),
404
+ toolCalls: Promise.resolve(result.toolCalls ?? []),
405
+ staticToolCalls: Promise.resolve(result.staticToolCalls ?? []),
406
+ dynamicToolCalls: Promise.resolve(result.dynamicToolCalls ?? []),
407
+ staticToolResults: Promise.resolve(result.staticToolResults ?? []),
408
+ dynamicToolResults: Promise.resolve(result.dynamicToolResults ?? []),
409
+ toolResults: Promise.resolve(result.toolResults ?? []),
410
+ finishReason: Promise.resolve(result.finishReason ?? "stop"),
411
+ rawFinishReason: Promise.resolve(result.rawFinishReason),
412
+ usage: Promise.resolve(usage),
413
+ totalUsage: Promise.resolve(totalUsage),
414
+ warnings: Promise.resolve(result.warnings),
415
+ steps: Promise.resolve(steps),
416
+ request: Promise.resolve(request),
417
+ response: Promise.resolve(response),
418
+ providerMetadata: Promise.resolve(result.providerMetadata),
419
+ textStream: textStream,
420
+ fullStream: fullStream,
421
+ };
422
+ }
423
+ /**
424
+ * @param {string} raw
425
+ * @returns {CliUsageInfo | undefined}
426
+ */
427
+ export function extractUsageFromOutput(raw) {
428
+ const lines = raw.split(/\r?\n/).filter(Boolean);
429
+ const usage = {};
430
+ let found = false;
431
+ for (const line of lines) {
432
+ let parsed;
433
+ try {
434
+ parsed = JSON.parse(line);
435
+ }
436
+ catch {
437
+ continue;
438
+ }
439
+ if (!parsed || typeof parsed !== "object")
440
+ continue;
441
+ if (parsed.type === "message_start" && parsed.message?.usage) {
442
+ const u = parsed.message.usage;
443
+ usage.inputTokens = (usage.inputTokens ?? 0) + (u.input_tokens ?? 0);
444
+ if (u.cache_read_input_tokens) {
445
+ usage.cacheReadTokens =
446
+ (usage.cacheReadTokens ?? 0) + u.cache_read_input_tokens;
447
+ }
448
+ if (u.cache_creation_input_tokens) {
449
+ usage.cacheWriteTokens =
450
+ (usage.cacheWriteTokens ?? 0) + u.cache_creation_input_tokens;
451
+ }
452
+ found = true;
453
+ continue;
454
+ }
455
+ if (parsed.type === "message_delta" && parsed.usage) {
456
+ if (parsed.usage.output_tokens) {
457
+ usage.outputTokens =
458
+ (usage.outputTokens ?? 0) + parsed.usage.output_tokens;
459
+ }
460
+ found = true;
461
+ continue;
462
+ }
463
+ if (parsed.type === "turn.completed" && parsed.usage) {
464
+ const u = parsed.usage;
465
+ if (u.input_tokens) {
466
+ usage.inputTokens = (usage.inputTokens ?? 0) + u.input_tokens;
467
+ }
468
+ if (u.output_tokens) {
469
+ usage.outputTokens = (usage.outputTokens ?? 0) + u.output_tokens;
470
+ }
471
+ if (u.cached_input_tokens) {
472
+ usage.cacheReadTokens =
473
+ (usage.cacheReadTokens ?? 0) + u.cached_input_tokens;
474
+ }
475
+ found = true;
476
+ continue;
477
+ }
478
+ if (parsed.usage && typeof parsed.usage === "object") {
479
+ const u = parsed.usage;
480
+ const inTok = u.input_tokens ?? u.inputTokens ?? u.prompt_tokens ?? 0;
481
+ const outTok = u.output_tokens ?? u.outputTokens ?? u.completion_tokens ?? 0;
482
+ if (inTok > 0 || outTok > 0) {
483
+ usage.inputTokens = (usage.inputTokens ?? 0) + inTok;
484
+ usage.outputTokens = (usage.outputTokens ?? 0) + outTok;
485
+ if (u.cache_read_input_tokens ||
486
+ u.cacheReadTokens ||
487
+ u.cached_input_tokens) {
488
+ usage.cacheReadTokens =
489
+ (usage.cacheReadTokens ?? 0) +
490
+ (u.cache_read_input_tokens ??
491
+ u.cacheReadTokens ??
492
+ u.cached_input_tokens ??
493
+ 0);
494
+ }
495
+ if (u.reasoning_tokens ?? u.reasoningTokens) {
496
+ usage.reasoningTokens =
497
+ (usage.reasoningTokens ?? 0) +
498
+ (u.reasoning_tokens ?? u.reasoningTokens ?? 0);
499
+ }
500
+ found = true;
501
+ continue;
502
+ }
503
+ }
504
+ }
505
+ if (!found) {
506
+ try {
507
+ const parsed = JSON.parse(raw.trim());
508
+ if (parsed?.stats?.models && typeof parsed.stats.models === "object") {
509
+ for (const data of Object.values(parsed.stats.models)) {
510
+ if (data?.tokens) {
511
+ usage.inputTokens =
512
+ (usage.inputTokens ?? 0) +
513
+ (data.tokens.input ?? data.tokens.prompt ?? 0);
514
+ usage.outputTokens =
515
+ (usage.outputTokens ?? 0) + (data.tokens.output ?? 0);
516
+ found = true;
517
+ }
518
+ }
519
+ }
520
+ }
521
+ catch {
522
+ // not single JSON
523
+ }
524
+ }
525
+ return found ? usage : undefined;
526
+ }
527
+ export class BaseCliAgent {
528
+ version = "agent-v1";
529
+ tools = {};
530
+ capabilities;
531
+ id;
532
+ model;
533
+ systemPrompt;
534
+ cwd;
535
+ env;
536
+ yolo;
537
+ timeoutMs;
538
+ idleTimeoutMs;
539
+ maxOutputBytes;
540
+ extraArgs;
541
+ /**
542
+ * @param {BaseCliAgentOptions} opts
543
+ */
544
+ constructor(opts) {
545
+ this.id = opts.id ?? randomUUID();
546
+ this.model = opts.model;
547
+ this.systemPrompt = opts.systemPrompt ?? opts.instructions;
548
+ this.cwd = opts.cwd;
549
+ this.env = opts.env;
550
+ this.yolo = opts.yolo ?? true;
551
+ this.timeoutMs = opts.timeoutMs;
552
+ this.idleTimeoutMs = opts.idleTimeoutMs;
553
+ this.maxOutputBytes = opts.maxOutputBytes;
554
+ this.extraArgs = opts.extraArgs;
555
+ }
556
+ /**
557
+ * @param {AgentGenerateOptions} [options]
558
+ * @param {AgentInvocationOperation} operation
559
+ * @returns {Effect.Effect<GenerateTextResult<Record<string, never>, unknown>, SmithersError>}
560
+ */
561
+ runGenerateEffect(options, operation) {
562
+ const invocationStart = performance.now();
563
+ const { prompt, systemFromMessages } = extractPrompt(options);
564
+ const callTimeouts = resolveTimeouts(options?.timeout, {
565
+ totalMs: this.timeoutMs,
566
+ idleMs: this.idleTimeoutMs,
567
+ });
568
+ const cwd = this.cwd ?? options?.rootDir ?? process.cwd();
569
+ const env = { ...process.env, ...this.env };
570
+ const combinedSystem = combineNonEmpty([
571
+ this.systemPrompt,
572
+ systemFromMessages,
573
+ ]);
574
+ const retryHint = resolveRetryHint(options);
575
+ const span = `agent.${operation}`;
576
+ let metricTags = {
577
+ source: "adapter",
578
+ engine: resolveAgentEngineTag(this),
579
+ operation,
580
+ model: normalizeMetricTag(this.model),
581
+ };
582
+ const spanAnnotations = {
583
+ agentEngine: metricTags.engine,
584
+ agentOperation: operation,
585
+ agentModel: metricTags.model ?? "unknown",
586
+ cwd,
587
+ timeoutMs: callTimeouts.totalMs ?? null,
588
+ idleTimeoutMs: callTimeouts.idleMs ?? null,
589
+ hasMessages: Array.isArray(options?.messages),
590
+ hasResumeSession: typeof options?.resumeSession === "string",
591
+ promptBytes: Buffer.byteLength(prompt, "utf8"),
592
+ systemPromptBytes: combinedSystem ? Buffer.byteLength(combinedSystem, "utf8") : 0,
593
+ };
594
+ let diagnosticsPromise;
595
+ let stdoutEmitter;
596
+ let cleanup;
597
+ let commandLogAnnotations = {};
598
+ const recordDurationMetric = () => Effect.sync(() => performance.now() - invocationStart).pipe(Effect.flatMap((durationMs) => Metric.update(taggedMetric(agentDurationMs, metricTags), durationMs)));
599
+ /**
600
+ * @param {string} stderr
601
+ * @returns {string}
602
+ */
603
+ function filterBenignStderr(stderr) {
604
+ const benignPatterns = [
605
+ /^.*state db missing rollout path.*$/gm,
606
+ /^.*codex_core::rollout::list.*$/gm,
607
+ /^.*failed to record rollout items: failed to queue rollout items: channel closed.*$/gim,
608
+ /^.*Failed to shutdown rollout recorder.*$/gm,
609
+ /^.*failed to renew cache TTL: Operation not permitted.*$/gim,
610
+ ];
611
+ let filtered = stderr;
612
+ for (const pattern of benignPatterns) {
613
+ filtered = filtered.replace(pattern, "");
614
+ }
615
+ // Clean up extra blank lines
616
+ return filtered.replace(/\n{3,}/g, "\n\n").trim();
617
+ }
618
+ const program = Effect.all([
619
+ Metric.increment(taggedMetric(agentInvocationsTotal, metricTags)),
620
+ ...(retryHint.isRetry
621
+ ? [
622
+ Metric.increment(taggedMetric(agentRetriesTotal, {
623
+ ...metricTags,
624
+ reason: retryHint.reason ?? "explicit",
625
+ })),
626
+ ]
627
+ : []),
628
+ Effect.logDebug("agent invocation started").pipe(Effect.annotateLogs({
629
+ ...spanAnnotations,
630
+ retryReason: retryHint.reason ?? null,
631
+ })),
632
+ ], { discard: true }).pipe(Effect.andThen(Effect.tryPromise({
633
+ try: () => this.buildCommand({
634
+ prompt,
635
+ systemPrompt: combinedSystem,
636
+ cwd,
637
+ options,
638
+ }),
639
+ catch: (cause) => toSmithersError(cause, "build agent command"),
640
+ })), Effect.flatMap((commandSpec) => {
641
+ cleanup = commandSpec.cleanup;
642
+ metricTags = {
643
+ ...metricTags,
644
+ engine: resolveAgentEngineTag(this, commandSpec.command),
645
+ model: normalizeMetricTag(this.model ?? commandSpec.command),
646
+ };
647
+ const outputFormat = commandSpec.outputFormat ?? inferOutputFormatFromArgs(commandSpec.args);
648
+ commandLogAnnotations = {
649
+ ...spanAnnotations,
650
+ agentEngine: metricTags.engine,
651
+ agentModel: metricTags.model ?? "unknown",
652
+ agentCommand: commandSpec.command,
653
+ agentArgs: commandSpec.args.join(" "),
654
+ outputFormat: outputFormat ?? "text",
655
+ };
656
+ const commandEnv = commandSpec.env
657
+ ? { ...env, ...commandSpec.env }
658
+ : env;
659
+ stdoutEmitter = createAgentStdoutTextEmitter({
660
+ outputFormat,
661
+ onText: options?.onStdout,
662
+ });
663
+ const interpreter = this.createOutputInterpreter();
664
+ let stdoutBuffer = "";
665
+ let stderrBuffer = "";
666
+ /**
667
+ * @param {AgentCliEvent[] | AgentCliEvent | null | undefined} eventPayload
668
+ */
669
+ const emitEvents = (eventPayload) => {
670
+ if (!eventPayload)
671
+ return;
672
+ const events = Array.isArray(eventPayload) ? eventPayload : [eventPayload];
673
+ for (const event of events) {
674
+ logAgentCliEvent(event, commandLogAnnotations, span);
675
+ if (!options?.onEvent)
676
+ continue;
677
+ void Promise.resolve(options.onEvent(event)).catch(() => undefined);
678
+ }
679
+ };
680
+ /**
681
+ * @param {"stdout" | "stderr"} stream
682
+ * @param {boolean} includePartial
683
+ */
684
+ const flushBufferedLines = (stream, includePartial) => {
685
+ if (!interpreter)
686
+ return;
687
+ let buffer = stream === "stdout" ? stdoutBuffer : stderrBuffer;
688
+ const lines = buffer.split("\n");
689
+ if (!includePartial) {
690
+ buffer = lines.pop() ?? "";
691
+ }
692
+ else {
693
+ buffer = "";
694
+ }
695
+ for (const line of lines) {
696
+ if (!line)
697
+ continue;
698
+ emitEvents(stream === "stdout"
699
+ ? interpreter.onStdoutLine?.(line)
700
+ : interpreter.onStderrLine?.(line));
701
+ }
702
+ if (stream === "stdout") {
703
+ stdoutBuffer = buffer;
704
+ }
705
+ else {
706
+ stderrBuffer = buffer;
707
+ }
708
+ };
709
+ /**
710
+ * @param {"stdout" | "stderr"} stream
711
+ * @param {string} chunk
712
+ */
713
+ const handleInterpreterChunk = (stream, chunk) => {
714
+ if (!interpreter || !chunk)
715
+ return;
716
+ if (stream === "stdout") {
717
+ stdoutBuffer += chunk;
718
+ }
719
+ else {
720
+ stderrBuffer += chunk;
721
+ }
722
+ flushBufferedLines(stream, false);
723
+ };
724
+ diagnosticsPromise = launchDiagnostics(commandSpec.command, commandEnv, cwd);
725
+ return Effect.gen(this, function* () {
726
+ const result = yield* runCommandEffect(commandSpec.command, commandSpec.args, {
727
+ cwd,
728
+ env: commandEnv,
729
+ input: commandSpec.stdin,
730
+ timeoutMs: callTimeouts.totalMs,
731
+ idleTimeoutMs: callTimeouts.idleMs,
732
+ signal: options?.abortSignal,
733
+ maxOutputBytes: this.maxOutputBytes ?? options?.maxOutputBytes,
734
+ onStdout: (chunk) => {
735
+ stdoutEmitter?.push(chunk);
736
+ handleInterpreterChunk("stdout", chunk);
737
+ },
738
+ onStderr: (chunk) => {
739
+ options?.onStderr?.(chunk);
740
+ handleInterpreterChunk("stderr", chunk);
741
+ },
742
+ });
743
+ flushBufferedLines("stdout", true);
744
+ flushBufferedLines("stderr", true);
745
+ emitEvents(interpreter?.onExit?.(result));
746
+ const stdout = commandSpec.outputFile
747
+ ? yield* Effect.tryPromise({
748
+ try: () => fs.readFile(commandSpec.outputFile, "utf8"),
749
+ catch: (cause) => toSmithersError(cause, "read output file"),
750
+ }).pipe(Effect.catchAll(() => Effect.succeed(result.stdout)))
751
+ : result.stdout;
752
+ if (result.exitCode && result.exitCode !== 0) {
753
+ const filteredStderr = filterBenignStderr(result.stderr);
754
+ if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
755
+ const errorText = filteredStderr ||
756
+ result.stdout.trim() ||
757
+ `CLI exited with code ${result.exitCode}`;
758
+ return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", errorText));
759
+ }
760
+ }
761
+ // Some CLIs may print extra banners to stdout. Allow individual agents
762
+ // to provide patterns so this logic stays opt-in and agent-specific.
763
+ const stdoutBannerPatterns = commandSpec.stdoutBannerPatterns ?? [];
764
+ let cleanedStdout = stdout;
765
+ for (const pattern of stdoutBannerPatterns) {
766
+ const regex = new RegExp(pattern.source, pattern.flags);
767
+ cleanedStdout = cleanedStdout.replace(regex, "");
768
+ }
769
+ const rawText = cleanedStdout.trim();
770
+ // Optionally treat "banner-only" output as an error when requested.
771
+ if (commandSpec.errorOnBannerOnly && !rawText && stdout.trim()) {
772
+ return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", "CLI agent error (stdout): output was only a banner with no model response"));
773
+ }
774
+ // Some CLIs report failures on stdout even with exit code 0. Keep
775
+ // detection patterns opt-in so normal model text is not misclassified.
776
+ const stdoutErrorPatterns = commandSpec.stdoutErrorPatterns ?? [];
777
+ if (rawText && !rawText.startsWith("{") && !rawText.startsWith("[")) {
778
+ for (const pattern of stdoutErrorPatterns) {
779
+ const regex = new RegExp(pattern.source, pattern.flags);
780
+ if (regex.test(rawText)) {
781
+ return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", `CLI agent error (stdout): ${rawText.slice(0, 500)}`));
782
+ }
783
+ }
784
+ }
785
+ const extractedText = outputFormat === "json" || outputFormat === "stream-json"
786
+ ? (extractTextFromJsonPayload(rawText) ?? rawText)
787
+ : rawText;
788
+ const output = tryParseJson(extractedText);
789
+ // Extract token usage from raw stdout before text extraction strips it.
790
+ // Each CLI harness embeds usage differently (NDJSON events, JSON stats, etc.)
791
+ const cliUsage = extractUsageFromOutput(stdout);
792
+ const usage = cliUsage ? {
793
+ inputTokens: cliUsage.inputTokens,
794
+ inputTokenDetails: {
795
+ noCacheTokens: undefined,
796
+ cacheReadTokens: cliUsage.cacheReadTokens,
797
+ cacheWriteTokens: cliUsage.cacheWriteTokens,
798
+ },
799
+ outputTokens: cliUsage.outputTokens,
800
+ outputTokenDetails: {
801
+ textTokens: undefined,
802
+ reasoningTokens: cliUsage.reasoningTokens,
803
+ },
804
+ totalTokens: (cliUsage.inputTokens ?? 0) + (cliUsage.outputTokens ?? 0) || undefined,
805
+ } : undefined;
806
+ const tokenTotals = extractAgentTokenTotals(usage);
807
+ stdoutEmitter?.flush(extractedText);
808
+ yield* recordAgentTokenMetrics(metricTags, tokenTotals);
809
+ const durationMs = performance.now() - invocationStart;
810
+ yield* Effect.logDebug("agent invocation completed").pipe(Effect.annotateLogs({
811
+ ...commandLogAnnotations,
812
+ durationMs,
813
+ textBytes: Buffer.byteLength(extractedText, "utf8"),
814
+ stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
815
+ inputTokens: tokenTotals.inputTokens ?? 0,
816
+ outputTokens: tokenTotals.outputTokens ?? 0,
817
+ totalTokens: tokenTotals.totalTokens ?? 0,
818
+ }));
819
+ return buildGenerateResult(extractedText, output, this.model ?? commandSpec.command, usage);
820
+ });
821
+ })).pipe(Effect.tapError((err) => Effect.all([
822
+ Metric.increment(taggedMetric(agentErrorsTotal, metricTags)),
823
+ Effect.logWarning("agent invocation failed").pipe(Effect.annotateLogs({
824
+ ...commandLogAnnotations,
825
+ ...spanAnnotations,
826
+ error: err.message,
827
+ durationMs: performance.now() - invocationStart,
828
+ })),
829
+ Effect.tryPromise({
830
+ try: async () => {
831
+ if (!diagnosticsPromise)
832
+ return;
833
+ const report = await diagnosticsPromise.catch(() => null);
834
+ if (report && err instanceof SmithersError) {
835
+ enrichReportWithErrorAnalysis(report, err.message);
836
+ err.details = { ...err.details, diagnostics: report };
837
+ logWarning(formatDiagnosticSummary(report), {}, span);
838
+ }
839
+ },
840
+ catch: (cause) => toSmithersError(cause, "enrich diagnostics"),
841
+ }).pipe(Effect.ignore),
842
+ ], { discard: true })), Effect.ensuring(Effect.sync(() => { stdoutEmitter?.flush(); })), Effect.ensuring(Effect.suspend(() => {
843
+ const cleanupFn = cleanup;
844
+ return cleanupFn
845
+ ? Effect.tryPromise({
846
+ try: () => cleanupFn(),
847
+ catch: (cause) => toSmithersError(cause, "agent cleanup"),
848
+ }).pipe(Effect.ignore)
849
+ : Effect.void;
850
+ })), Effect.ensuring(recordDurationMetric()), Effect.annotateLogs(spanAnnotations), Effect.withLogSpan(span));
851
+ return program;
852
+ }
853
+ /**
854
+ * @param {AgentGenerateOptions} [options]
855
+ * @returns {Promise<GenerateTextResult<Record<string, never>, unknown>>}
856
+ */
857
+ async generate(options) {
858
+ return runAgentPromise(this.runGenerateEffect(options, "generate"));
859
+ }
860
+ /**
861
+ * @param {AgentGenerateOptions} [options]
862
+ * @returns {Promise<StreamTextResult<Record<string, never>, unknown>>}
863
+ */
864
+ async stream(options) {
865
+ const result = await runAgentPromise(this.runGenerateEffect(options, "stream").pipe(Effect.map((generateResult) => buildStreamResult(generateResult))));
866
+ return result;
867
+ }
868
+ /**
869
+ * @returns {CliOutputInterpreter | undefined}
870
+ */
871
+ createOutputInterpreter() {
872
+ return undefined;
873
+ }
874
+ }