@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.
- package/LICENSE +21 -0
- package/package.json +65 -0
- package/src/AgentLike.ts +28 -0
- package/src/AmpAgent.js +232 -0
- package/src/AmpAgentOptions.ts +26 -0
- package/src/AnthropicAgent.js +54 -0
- package/src/AnthropicAgentOptions.ts +8 -0
- package/src/BaseCliAgent/AgentCliActionKind.ts +10 -0
- package/src/BaseCliAgent/AgentCliEvent.ts +44 -0
- package/src/BaseCliAgent/BaseCliAgent.js +874 -0
- package/src/BaseCliAgent/BaseCliAgentOptions.ts +13 -0
- package/src/BaseCliAgent/CliOutputInterpreter.ts +8 -0
- package/src/BaseCliAgent/CliUsageInfo.ts +7 -0
- package/src/BaseCliAgent/CodexConfigOverrides.ts +3 -0
- package/src/BaseCliAgent/PiExtensionUiRequest.ts +10 -0
- package/src/BaseCliAgent/PiExtensionUiResponse.ts +7 -0
- package/src/BaseCliAgent/RunCommandResult.ts +5 -0
- package/src/BaseCliAgent/buildGenerateResult.js +57 -0
- package/src/BaseCliAgent/combineNonEmpty.js +8 -0
- package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +198 -0
- package/src/BaseCliAgent/extractPrompt.js +88 -0
- package/src/BaseCliAgent/extractTextFromJsonValue.js +46 -0
- package/src/BaseCliAgent/index.js +32 -0
- package/src/BaseCliAgent/normalizeCodexConfig.js +22 -0
- package/src/BaseCliAgent/parseHelpers.js +111 -0
- package/src/BaseCliAgent/pushFlag.js +18 -0
- package/src/BaseCliAgent/pushList.js +10 -0
- package/src/BaseCliAgent/resolveTimeouts.js +24 -0
- package/src/BaseCliAgent/runCommandEffect.js +32 -0
- package/src/BaseCliAgent/runRpcCommandEffect.js +365 -0
- package/src/BaseCliAgent/truncateToBytes.js +13 -0
- package/src/BaseCliAgent/tryParseJson.js +18 -0
- package/src/ClaudeCodeAgent.js +455 -0
- package/src/ClaudeCodeAgentOptions.ts +52 -0
- package/src/CodexAgent.js +593 -0
- package/src/CodexAgentOptions.ts +23 -0
- package/src/ForgeAgent.js +128 -0
- package/src/ForgeAgentOptions.ts +14 -0
- package/src/GeminiAgent.js +273 -0
- package/src/GeminiAgentOptions.ts +20 -0
- package/src/KimiAgent.js +260 -0
- package/src/KimiAgentOptions.ts +21 -0
- package/src/OpenAIAgent.js +54 -0
- package/src/OpenAIAgentOptions.ts +8 -0
- package/src/PiAgent.js +468 -0
- package/src/PiAgentOptions.ts +40 -0
- package/src/SdkAgentOptions.ts +16 -0
- package/src/agent-contract/SmithersAgentContract.ts +10 -0
- package/src/agent-contract/SmithersAgentContractTool.ts +8 -0
- package/src/agent-contract/SmithersAgentToolCategory.ts +6 -0
- package/src/agent-contract/SmithersListedTool.ts +4 -0
- package/src/agent-contract/SmithersToolSurface.ts +1 -0
- package/src/agent-contract/createSmithersAgentContract.js +188 -0
- package/src/agent-contract/index.js +10 -0
- package/src/agent-contract/renderSmithersAgentPromptGuidance.js +81 -0
- package/src/capability-registry/AgentCapabilityRegistry.ts +22 -0
- package/src/capability-registry/AgentToolDescriptor.ts +4 -0
- package/src/capability-registry/hashCapabilityRegistry.js +43 -0
- package/src/capability-registry/index.js +8 -0
- package/src/capability-registry/normalizeCapabilityRegistry.js +52 -0
- package/src/capability-registry/normalizeCapabilityStringList.js +9 -0
- package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +6 -0
- package/src/cli-capabilities/CliAgentCapabilityDoctorReport.ts +18 -0
- package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +9 -0
- package/src/cli-capabilities/formatCliAgentCapabilityDoctorReport.js +24 -0
- package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +92 -0
- package/src/cli-capabilities/getCliAgentCapabilityReport.js +52 -0
- package/src/cli-capabilities/index.js +11 -0
- package/src/diagnostics/DiagnosticCheck.ts +11 -0
- package/src/diagnostics/DiagnosticCheckId.ts +4 -0
- package/src/diagnostics/DiagnosticContext.ts +4 -0
- package/src/diagnostics/DiagnosticReport.ts +9 -0
- package/src/diagnostics/enrichReportWithErrorAnalysis.js +34 -0
- package/src/diagnostics/formatDiagnosticSummary.js +17 -0
- package/src/diagnostics/getDiagnosticStrategy.js +503 -0
- package/src/diagnostics/index.js +13 -0
- package/src/diagnostics/launchDiagnostics.js +16 -0
- package/src/diagnostics/runDiagnostics.js +52 -0
- package/src/index.d.ts +872 -0
- package/src/index.js +39 -0
- package/src/resolveSdkModel.js +9 -0
- package/src/sanitizeForOpenAI.js +47 -0
- package/src/streamResultToGenerateResult.js +70 -0
- 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
|
+
}
|