@smithers-orchestrator/agents 0.16.9 → 0.18.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/package.json +6 -4
- package/src/AgentLike.ts +4 -1
- package/src/AmpAgent.js +2 -0
- package/src/AnthropicAgent.js +10 -3
- package/src/BaseCliAgent/AgentGenerateOptions.ts +24 -0
- package/src/BaseCliAgent/BaseCliAgent.js +166 -32
- package/src/BaseCliAgent/extractPrompt.js +0 -1
- package/src/BaseCliAgent/extractTextFromJsonValue.js +2 -0
- package/src/BaseCliAgent/index.js +1 -0
- package/src/ClaudeCodeAgent.js +10 -3
- package/src/ClaudeCodeAgentOptions.ts +16 -0
- package/src/CodexAgent.js +6 -0
- package/src/CodexAgentOptions.ts +15 -0
- package/src/ForgeAgent.js +2 -0
- package/src/GeminiAgent.js +6 -2
- package/src/GeminiAgentOptions.ts +12 -0
- package/src/KimiAgent.js +211 -6
- package/src/KimiAgentOptions.ts +8 -0
- package/src/OpenAIAgent.js +10 -3
- package/src/OpenCodeAgent.js +495 -0
- package/src/OpenCodeAgent.ts +43 -0
- package/src/PiAgent.js +1 -1
- package/src/__type-tests__/AgentLike.assignability.test-d.ts +31 -0
- package/src/capability-registry/AgentCapabilityRegistry.ts +1 -1
- package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +1 -0
- package/src/cli-capabilities/getCliAgentCapabilityReport.js +6 -0
- package/src/index.d.ts +65 -64
- package/src/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "AI SDK and CLI agent adapters for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -48,13 +48,15 @@
|
|
|
48
48
|
"@ai-sdk/anthropic": "^3.0.71",
|
|
49
49
|
"@ai-sdk/openai": "^3.0.53",
|
|
50
50
|
"ai": "^6.0.168",
|
|
51
|
+
"effect": "^3.21.1",
|
|
51
52
|
"zod": "^4.3.6",
|
|
52
|
-
"@smithers-orchestrator/
|
|
53
|
-
"@smithers-orchestrator/
|
|
54
|
-
"@smithers-orchestrator/
|
|
53
|
+
"@smithers-orchestrator/errors": "0.18.0",
|
|
54
|
+
"@smithers-orchestrator/observability": "0.18.0",
|
|
55
|
+
"@smithers-orchestrator/driver": "0.18.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/bun": "latest",
|
|
59
|
+
"react": "^19.2.5",
|
|
58
60
|
"typescript": "~5.9.3"
|
|
59
61
|
},
|
|
60
62
|
"scripts": {
|
package/src/AgentLike.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentCapabilityRegistry } from "./capability-registry";
|
|
2
|
+
import type { AgentGenerateOptions } from "./BaseCliAgent/AgentGenerateOptions";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Represents an entity capable of generating responses or actions based on prompts.
|
|
@@ -11,6 +12,8 @@ export type AgentLike = {
|
|
|
11
12
|
tools?: Record<string, unknown>;
|
|
12
13
|
/** Optional structured capability registry for cache and diagnostics */
|
|
13
14
|
capabilities?: AgentCapabilityRegistry;
|
|
15
|
+
/** True when the agent consumes outputSchema through a native structured-output API. */
|
|
16
|
+
supportsNativeStructuredOutput?: boolean;
|
|
14
17
|
/**
|
|
15
18
|
* Generates a response or action based on the provided arguments.
|
|
16
19
|
*
|
|
@@ -24,5 +27,5 @@ export type AgentLike = {
|
|
|
24
27
|
* @param args.outputSchema - Optional Zod schema defining the expected structured output format
|
|
25
28
|
* @returns A promise resolving to the generated output
|
|
26
29
|
*/
|
|
27
|
-
generate: (args
|
|
30
|
+
generate: (args?: AgentGenerateOptions) => Promise<unknown>;
|
|
28
31
|
};
|
package/src/AmpAgent.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @smithers-type-exports-end
|
|
4
4
|
|
|
5
5
|
import { BaseCliAgent, pushFlag, isRecord, asString, toolKindFromName, createSyntheticIdGenerator, } from "./BaseCliAgent/index.js";
|
|
6
|
+
/** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
|
|
6
7
|
/** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -11,6 +12,7 @@ import { BaseCliAgent, pushFlag, isRecord, asString, toolKindFromName, createSyn
|
|
|
11
12
|
*/
|
|
12
13
|
export class AmpAgent extends BaseCliAgent {
|
|
13
14
|
opts;
|
|
15
|
+
/** @type {AgentCapabilityRegistry} */
|
|
14
16
|
capabilities;
|
|
15
17
|
cliEngine = "amp";
|
|
16
18
|
/**
|
package/src/AnthropicAgent.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import { ToolLoopAgent, } from "ai";
|
|
2
|
+
import { Output, ToolLoopAgent, } from "ai";
|
|
3
3
|
import { resolveSdkModel } from "./resolveSdkModel.js";
|
|
4
4
|
import { streamResultToGenerateResult } from "./streamResultToGenerateResult.js";
|
|
5
5
|
/** @typedef {import("ai").AgentCallParameters} AgentCallParameters */
|
|
6
|
+
/** @typedef {import("./BaseCliAgent/AgentGenerateOptions.ts").AgentGenerateOptions} AgentGenerateOptions */
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @template [CALL_OPTIONS=never], [TOOLS=import("ai").ToolSet]
|
|
@@ -16,6 +17,7 @@ import { streamResultToGenerateResult } from "./streamResultToGenerateResult.js"
|
|
|
16
17
|
|
|
17
18
|
export class AnthropicAgent extends ToolLoopAgent {
|
|
18
19
|
hijackEngine = "anthropic-sdk";
|
|
20
|
+
supportsNativeStructuredOutput = true;
|
|
19
21
|
/**
|
|
20
22
|
* @param {AnthropicAgentOptions<CALL_OPTIONS, TOOLS>} opts
|
|
21
23
|
*/
|
|
@@ -27,18 +29,22 @@ export class AnthropicAgent extends ToolLoopAgent {
|
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
|
-
* @param {
|
|
32
|
+
* @param {AgentGenerateOptions} [args]
|
|
31
33
|
* @returns {Promise<GenerateTextResult<TOOLS, never>>}
|
|
32
34
|
*/
|
|
33
|
-
generate(args) {
|
|
35
|
+
generate(args = {}) {
|
|
34
36
|
const promptArgs = "messages" in args
|
|
35
37
|
? { messages: args.messages }
|
|
36
38
|
: { prompt: args.prompt };
|
|
39
|
+
const outputArgs = args.outputSchema
|
|
40
|
+
? { output: Output.object({ schema: args.outputSchema }) }
|
|
41
|
+
: {};
|
|
37
42
|
if (!args.onStdout) {
|
|
38
43
|
return super.generate({
|
|
39
44
|
options: args.options,
|
|
40
45
|
abortSignal: args.abortSignal,
|
|
41
46
|
...promptArgs,
|
|
47
|
+
...outputArgs,
|
|
42
48
|
timeout: args.timeout,
|
|
43
49
|
onStepFinish: args.onStepFinish,
|
|
44
50
|
});
|
|
@@ -47,6 +53,7 @@ export class AnthropicAgent extends ToolLoopAgent {
|
|
|
47
53
|
options: args.options,
|
|
48
54
|
abortSignal: args.abortSignal,
|
|
49
55
|
...promptArgs,
|
|
56
|
+
...outputArgs,
|
|
50
57
|
timeout: args.timeout,
|
|
51
58
|
onStepFinish: args.onStepFinish,
|
|
52
59
|
}).then((stream) => streamResultToGenerateResult(stream, args.onStdout));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentCliEvent } from "./AgentCliEvent";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loosely-typed generation options. The AI SDK passes a dynamic shape here
|
|
5
|
+
* (GenerateTextOptions / StreamTextOptions and provider-specific extensions)
|
|
6
|
+
* so we keep this permissive but avoid raw `any`.
|
|
7
|
+
*/
|
|
8
|
+
export type AgentGenerateOptions = {
|
|
9
|
+
prompt?: unknown;
|
|
10
|
+
messages?: unknown;
|
|
11
|
+
timeout?: unknown;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
rootDir?: string;
|
|
14
|
+
resumeSession?: string;
|
|
15
|
+
maxOutputBytes?: number;
|
|
16
|
+
onStdout?: (text: string) => void;
|
|
17
|
+
onStderr?: (text: string) => void;
|
|
18
|
+
onEvent?: (event: AgentCliEvent) => unknown;
|
|
19
|
+
retry?: unknown;
|
|
20
|
+
isRetry?: unknown;
|
|
21
|
+
retryAttempt?: unknown;
|
|
22
|
+
schemaRetry?: unknown;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
@@ -3,7 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import { Cause, Effect, Exit, Metric } from "effect";
|
|
4
4
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
5
5
|
import { logDebug, logInfo, logWarning } from "@smithers-orchestrator/observability/logging";
|
|
6
|
-
import { agentDurationMs, agentErrorsTotal, agentInvocationsTotal, agentRetriesTotal, agentTokensTotal,
|
|
6
|
+
import { agentDurationMs, agentErrorsTotal, agentInvocationsTotal, agentRetriesTotal, agentTokensTotal, } from "@smithers-orchestrator/observability/metrics";
|
|
7
7
|
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
8
8
|
import { launchDiagnostics, enrichReportWithErrorAnalysis, formatDiagnosticSummary } from "../diagnostics/index.js";
|
|
9
9
|
import { extractPrompt } from "./extractPrompt.js";
|
|
@@ -16,6 +16,7 @@ import { buildGenerateResult } from "./buildGenerateResult.js";
|
|
|
16
16
|
import { runCommandEffect } from "./runCommandEffect.js";
|
|
17
17
|
/** @typedef {import("./AgentCliEvent.ts").AgentCliEvent} AgentCliEvent */
|
|
18
18
|
|
|
19
|
+
/** @typedef {import("./AgentGenerateOptions.ts").AgentGenerateOptions} AgentGenerateOptions */
|
|
19
20
|
/** @typedef {import("./BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
|
|
20
21
|
/** @typedef {import("./CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
21
22
|
/** @typedef {import("./CliUsageInfo.ts").CliUsageInfo} CliUsageInfo */
|
|
@@ -38,29 +39,6 @@ import { runCommandEffect } from "./runCommandEffect.js";
|
|
|
38
39
|
* totalTokens?: number;
|
|
39
40
|
* }} AgentTokenTotals
|
|
40
41
|
*/
|
|
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
42
|
/**
|
|
65
43
|
* @template A
|
|
66
44
|
* @param {Effect.Effect<A, SmithersError, never>} effect
|
|
@@ -303,15 +281,67 @@ function extractTextFromJsonPayload(raw) {
|
|
|
303
281
|
return text;
|
|
304
282
|
}
|
|
305
283
|
}
|
|
284
|
+
// OpenCode-style CLIs emit a final "finish" or "done" event with the
|
|
285
|
+
// complete response text directly on the payload. Prefer this over
|
|
286
|
+
// concatenating all text_delta chunks which would duplicate content.
|
|
287
|
+
if (type === "finish" || type === "done") {
|
|
288
|
+
const text = typeof parsed?.text === "string" ? parsed.text : undefined;
|
|
289
|
+
if (text)
|
|
290
|
+
return text;
|
|
291
|
+
}
|
|
292
|
+
// OpenCode nd-JSON format: "text" events carry part.text with finalized
|
|
293
|
+
// text chunks. Accumulate these as a fallback when the interpreter's
|
|
294
|
+
// completed event isn't surfaced properly.
|
|
295
|
+
if (type === "text" && parsed?.part?.text) {
|
|
296
|
+
// Don't return early — accumulate via the chunks path below
|
|
297
|
+
}
|
|
306
298
|
}
|
|
307
299
|
const chunks = [];
|
|
308
300
|
for (const parsed of parsedLines) {
|
|
309
|
-
|
|
301
|
+
let text;
|
|
302
|
+
if (parsed?.type === "text" && typeof parsed?.part?.text === "string") {
|
|
303
|
+
text = parsed.part.text;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
text = extractTextFromJsonValue(parsed);
|
|
307
|
+
}
|
|
310
308
|
if (text)
|
|
311
309
|
chunks.push(text);
|
|
312
310
|
}
|
|
313
311
|
return chunks.length ? chunks.join("") : undefined;
|
|
314
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* @param {string} raw
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
function stripOscSequences(raw) {
|
|
318
|
+
return raw.replace(/\x1b\]0;[^\x07]*\x07/g, "");
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} raw
|
|
322
|
+
* @returns {string | undefined}
|
|
323
|
+
*/
|
|
324
|
+
function extractErrorFromJsonPayload(raw) {
|
|
325
|
+
const trimmed = stripOscSequences(raw).trim();
|
|
326
|
+
if (!trimmed)
|
|
327
|
+
return undefined;
|
|
328
|
+
const lines = trimmed.split(/\r?\n/).filter(Boolean);
|
|
329
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(lines[i]);
|
|
332
|
+
if (parsed?.type !== "error")
|
|
333
|
+
continue;
|
|
334
|
+
const message = parsed?.error?.data?.message ?? parsed?.error?.message ?? parsed?.error?.name;
|
|
335
|
+
if (typeof message === "string" && message.trim()) {
|
|
336
|
+
return message.trim();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
315
345
|
/**
|
|
316
346
|
* @param {string[]} args
|
|
317
347
|
* @returns {string | undefined}
|
|
@@ -425,7 +455,7 @@ function buildStreamResult(result) {
|
|
|
425
455
|
* @returns {CliUsageInfo | undefined}
|
|
426
456
|
*/
|
|
427
457
|
export function extractUsageFromOutput(raw) {
|
|
428
|
-
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
458
|
+
const lines = stripOscSequences(raw).split(/\r?\n/).filter(Boolean);
|
|
429
459
|
const usage = {};
|
|
430
460
|
let found = false;
|
|
431
461
|
for (const line of lines) {
|
|
@@ -475,6 +505,25 @@ export function extractUsageFromOutput(raw) {
|
|
|
475
505
|
found = true;
|
|
476
506
|
continue;
|
|
477
507
|
}
|
|
508
|
+
if (parsed.type === "step_finish" && parsed.part?.tokens && typeof parsed.part.tokens === "object") {
|
|
509
|
+
const tokens = parsed.part.tokens;
|
|
510
|
+
const input = tokens.input ?? 0;
|
|
511
|
+
const output = tokens.output ?? 0;
|
|
512
|
+
const total = tokens.total ?? 0;
|
|
513
|
+
const reasoning = tokens.reasoning ?? 0;
|
|
514
|
+
const cacheRead = tokens.cache?.read ?? 0;
|
|
515
|
+
const cacheWrite = tokens.cache?.write ?? 0;
|
|
516
|
+
if (input > 0 || output > 0 || total > 0 || reasoning > 0 || cacheRead > 0 || cacheWrite > 0) {
|
|
517
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + input;
|
|
518
|
+
usage.outputTokens = (usage.outputTokens ?? 0) + output;
|
|
519
|
+
usage.totalTokens = (usage.totalTokens ?? 0) + total;
|
|
520
|
+
usage.reasoningTokens = (usage.reasoningTokens ?? 0) + reasoning;
|
|
521
|
+
usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cacheRead;
|
|
522
|
+
usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + cacheWrite;
|
|
523
|
+
found = true;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
478
527
|
if (parsed.usage && typeof parsed.usage === "object") {
|
|
479
528
|
const u = parsed.usage;
|
|
480
529
|
const inTok = u.input_tokens ?? u.inputTokens ?? u.prompt_tokens ?? 0;
|
|
@@ -554,7 +603,7 @@ export class BaseCliAgent {
|
|
|
554
603
|
this.extraArgs = opts.extraArgs;
|
|
555
604
|
}
|
|
556
605
|
/**
|
|
557
|
-
* @param {AgentGenerateOptions}
|
|
606
|
+
* @param {AgentGenerateOptions | undefined} options
|
|
558
607
|
* @param {AgentInvocationOperation} operation
|
|
559
608
|
* @returns {Effect.Effect<GenerateTextResult<Record<string, never>, unknown>, SmithersError>}
|
|
560
609
|
*/
|
|
@@ -598,9 +647,52 @@ export class BaseCliAgent {
|
|
|
598
647
|
const recordDurationMetric = () => Effect.sync(() => performance.now() - invocationStart).pipe(Effect.flatMap((durationMs) => Metric.update(taggedMetric(agentDurationMs, metricTags), durationMs)));
|
|
599
648
|
/**
|
|
600
649
|
* @param {string} stderr
|
|
650
|
+
* @param {ReadonlyArray<RegExp>} [extraPatterns]
|
|
601
651
|
* @returns {string}
|
|
602
652
|
*/
|
|
603
|
-
|
|
653
|
+
const agentId = this.id;
|
|
654
|
+
const agentModel = this.model;
|
|
655
|
+
const agentEngine = resolveAgentEngineTag(this);
|
|
656
|
+
/**
|
|
657
|
+
* Detect well-known non-retryable CLI agent configuration errors so the
|
|
658
|
+
* engine surfaces them with a clear, actionable message and stops retrying
|
|
659
|
+
* (these errors are deterministic and will never recover by re-running).
|
|
660
|
+
*
|
|
661
|
+
* @param {string} message
|
|
662
|
+
* @param {string} command
|
|
663
|
+
* @returns {SmithersError | null}
|
|
664
|
+
*/
|
|
665
|
+
function classifyNonRetryableAgentError(message, command) {
|
|
666
|
+
if (!message)
|
|
667
|
+
return null;
|
|
668
|
+
const nonRetryablePatterns = [
|
|
669
|
+
{ re: /\bLLM not set\b/i, hint: "the agent's model name is not present in the CLI's configured providers" },
|
|
670
|
+
{ re: /\bLLM not supported\b/i, hint: "the agent's model is not supported by this CLI build" },
|
|
671
|
+
{ re: /\bmodel\s+['"]?[^'"\s]+['"]?\s+not found\b/i, hint: "the requested model is not registered with the CLI" },
|
|
672
|
+
{ re: /\bunknown model\b/i, hint: "the requested model is not registered with the CLI" },
|
|
673
|
+
{ re: /\b401\b[\s\S]{0,200}?(invalid[_\s-]?authentication|unauthorized|invalid[_\s-]?api[_\s-]?key)/i, hint: `the CLI's stored credentials are invalid or expired — re-authenticate (e.g. for kimi run \`kimi login\`)` },
|
|
674
|
+
{ re: /\bAPI\s*Key\b[\s\S]{0,120}?(invalid|expired|may have expired)/i, hint: `the CLI's stored credentials are invalid or expired — re-authenticate (e.g. for kimi run \`kimi login\`)` },
|
|
675
|
+
{ re: /\b(access|auth(entication)?|oauth|bearer)\s+token\b[\s\S]{0,80}?(expired|invalid|revoked)/i, hint: `the CLI's auth token is no longer valid — re-authenticate (e.g. for kimi run \`kimi login\`)` },
|
|
676
|
+
{ re: /\binvalid[_\s-]?authentication[_\s-]?error\b/i, hint: `the CLI's stored credentials are invalid — re-authenticate (e.g. for kimi run \`kimi login\`)` },
|
|
677
|
+
];
|
|
678
|
+
for (const { re, hint } of nonRetryablePatterns) {
|
|
679
|
+
if (re.test(message)) {
|
|
680
|
+
const modelLabel = agentModel ?? "<unset>";
|
|
681
|
+
const idLabel = agentId ?? "<anonymous>";
|
|
682
|
+
const summary = `Agent "${idLabel}" (${command}, model=${modelLabel}) failed with non-retryable configuration error: ${message.slice(0, 300)}. Hint: ${hint}. Fix the agent's model in .smithers/agents.ts (or the CLI's config) — retrying will not help.`;
|
|
683
|
+
return new SmithersError("AGENT_CONFIG_INVALID", summary, {
|
|
684
|
+
failureRetryable: false,
|
|
685
|
+
agentId: idLabel,
|
|
686
|
+
agentEngine,
|
|
687
|
+
agentModel: modelLabel,
|
|
688
|
+
command,
|
|
689
|
+
underlying: message.slice(0, 500),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
function filterBenignStderr(stderr, extraPatterns) {
|
|
604
696
|
const benignPatterns = [
|
|
605
697
|
/^.*state db missing rollout path.*$/gm,
|
|
606
698
|
/^.*codex_core::rollout::list.*$/gm,
|
|
@@ -612,6 +704,12 @@ export class BaseCliAgent {
|
|
|
612
704
|
for (const pattern of benignPatterns) {
|
|
613
705
|
filtered = filtered.replace(pattern, "");
|
|
614
706
|
}
|
|
707
|
+
if (extraPatterns?.length) {
|
|
708
|
+
for (const pattern of extraPatterns) {
|
|
709
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
710
|
+
filtered = filtered.replace(regex, "");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
615
713
|
// Clean up extra blank lines
|
|
616
714
|
return filtered.replace(/\n{3,}/g, "\n\n").trim();
|
|
617
715
|
}
|
|
@@ -663,6 +761,7 @@ export class BaseCliAgent {
|
|
|
663
761
|
const interpreter = this.createOutputInterpreter();
|
|
664
762
|
let stdoutBuffer = "";
|
|
665
763
|
let stderrBuffer = "";
|
|
764
|
+
let completedEvent = null;
|
|
666
765
|
/**
|
|
667
766
|
* @param {AgentCliEvent[] | AgentCliEvent | null | undefined} eventPayload
|
|
668
767
|
*/
|
|
@@ -671,6 +770,9 @@ export class BaseCliAgent {
|
|
|
671
770
|
return;
|
|
672
771
|
const events = Array.isArray(eventPayload) ? eventPayload : [eventPayload];
|
|
673
772
|
for (const event of events) {
|
|
773
|
+
if (event?.type === "completed") {
|
|
774
|
+
completedEvent = event;
|
|
775
|
+
}
|
|
674
776
|
logAgentCliEvent(event, commandLogAnnotations, span);
|
|
675
777
|
if (!options?.onEvent)
|
|
676
778
|
continue;
|
|
@@ -750,14 +852,44 @@ export class BaseCliAgent {
|
|
|
750
852
|
}).pipe(Effect.catchAll(() => Effect.succeed(result.stdout)))
|
|
751
853
|
: result.stdout;
|
|
752
854
|
if (result.exitCode && result.exitCode !== 0) {
|
|
753
|
-
const filteredStderr = filterBenignStderr(result.stderr);
|
|
855
|
+
const filteredStderr = filterBenignStderr(result.stderr, commandSpec.benignStderrPatterns);
|
|
754
856
|
if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
|
|
755
|
-
const
|
|
857
|
+
const structuredError = (commandSpec.outputFormat === "json" || commandSpec.outputFormat === "stream-json")
|
|
858
|
+
? extractErrorFromJsonPayload(result.stdout)
|
|
859
|
+
: undefined;
|
|
860
|
+
const errorText = structuredError ||
|
|
861
|
+
filteredStderr ||
|
|
756
862
|
result.stdout.trim() ||
|
|
757
863
|
`CLI exited with code ${result.exitCode}`;
|
|
864
|
+
const nonRetryable = classifyNonRetryableAgentError(errorText, commandSpec.command);
|
|
865
|
+
if (nonRetryable) {
|
|
866
|
+
return yield* Effect.fail(nonRetryable);
|
|
867
|
+
}
|
|
868
|
+
// Detect kimi session-loss. Kimi crashes mid-stream and prints
|
|
869
|
+
// `To resume this session: kimi -r <uuid>` to stderr (and often
|
|
870
|
+
// also to the merged error text after the benign-stderr filter
|
|
871
|
+
// strips the bare-line variant). The session itself is corrupt
|
|
872
|
+
// — re-running with `--session <same-uuid>` deterministically
|
|
873
|
+
// reproduces the same crash. Surface a typed error that tells
|
|
874
|
+
// the engine retry path to DROP the broken session id and
|
|
875
|
+
// start a fresh one on the next attempt.
|
|
876
|
+
const rawStderr = result.stderr ?? "";
|
|
877
|
+
const sessionLossMatch = rawStderr.match(/kimi -r ([0-9a-f-]{8,})/i)
|
|
878
|
+
|| errorText.match(/kimi -r ([0-9a-f-]{8,})/i);
|
|
879
|
+
if (commandSpec.command === "kimi" && sessionLossMatch) {
|
|
880
|
+
return yield* Effect.fail(new SmithersError("AGENT_SESSION_LOST", `Kimi session ${sessionLossMatch[1]} is broken; CLI exited ${result.exitCode}. Retry will start a fresh session.`, {
|
|
881
|
+
failureRetryable: true,
|
|
882
|
+
discardResumeSession: true,
|
|
883
|
+
command: "kimi",
|
|
884
|
+
kimiSessionId: sessionLossMatch[1],
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
758
887
|
return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", errorText));
|
|
759
888
|
}
|
|
760
889
|
}
|
|
890
|
+
if (completedEvent?.ok === false) {
|
|
891
|
+
return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", completedEvent.error || "CLI agent reported an error"));
|
|
892
|
+
}
|
|
761
893
|
// Some CLIs may print extra banners to stdout. Allow individual agents
|
|
762
894
|
// to provide patterns so this logic stays opt-in and agent-specific.
|
|
763
895
|
const stdoutBannerPatterns = commandSpec.stdoutBannerPatterns ?? [];
|
|
@@ -778,7 +910,9 @@ export class BaseCliAgent {
|
|
|
778
910
|
for (const pattern of stdoutErrorPatterns) {
|
|
779
911
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
780
912
|
if (regex.test(rawText)) {
|
|
781
|
-
|
|
913
|
+
const stdoutErrText = `CLI agent error (stdout): ${rawText.slice(0, 500)}`;
|
|
914
|
+
const nonRetryable = classifyNonRetryableAgentError(rawText, commandSpec.command);
|
|
915
|
+
return yield* Effect.fail(nonRetryable ?? new SmithersError("AGENT_CLI_ERROR", stdoutErrText));
|
|
782
916
|
}
|
|
783
917
|
}
|
|
784
918
|
}
|
|
@@ -801,7 +935,7 @@ export class BaseCliAgent {
|
|
|
801
935
|
textTokens: undefined,
|
|
802
936
|
reasoningTokens: cliUsage.reasoningTokens,
|
|
803
937
|
},
|
|
804
|
-
totalTokens: (cliUsage.inputTokens ?? 0) + (cliUsage.outputTokens ?? 0) || undefined,
|
|
938
|
+
totalTokens: cliUsage.totalTokens ?? ((cliUsage.inputTokens ?? 0) + (cliUsage.outputTokens ?? 0) || undefined),
|
|
805
939
|
} : undefined;
|
|
806
940
|
const tokenTotals = extractAgentTokenTotals(usage);
|
|
807
941
|
stdoutEmitter?.flush(extractedText);
|
|
@@ -32,6 +32,8 @@ export function extractTextFromJsonValue(value) {
|
|
|
32
32
|
if (parts.trim())
|
|
33
33
|
return parts;
|
|
34
34
|
}
|
|
35
|
+
if (record.type === "text" && record.part)
|
|
36
|
+
return extractTextFromJsonValue(record.part);
|
|
35
37
|
if (record.response)
|
|
36
38
|
return extractTextFromJsonValue(record.response);
|
|
37
39
|
if (record.message)
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
/** @typedef {import("./AgentCliEvent.ts").AgentCliEvent} AgentCliEvent */
|
|
7
7
|
/** @typedef {import("./AgentCliEvent.ts").AgentCliEventLevel} AgentCliEventLevel */
|
|
8
8
|
/** @typedef {import("./AgentCliEvent.ts").AgentCliStartedEvent} AgentCliStartedEvent */
|
|
9
|
+
/** @typedef {import("./AgentGenerateOptions.ts").AgentGenerateOptions} AgentGenerateOptions */
|
|
9
10
|
/** @typedef {import("./BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
|
|
10
11
|
/** @typedef {import("./CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
11
12
|
/** @typedef {import("./CliUsageInfo.ts").CliUsageInfo} CliUsageInfo */
|
package/src/ClaudeCodeAgent.js
CHANGED
|
@@ -97,15 +97,16 @@ export class ClaudeCodeAgent extends BaseCliAgent {
|
|
|
97
97
|
// Clear env vars that cause "Cannot run nested Claude Code instances" errors.
|
|
98
98
|
// CLAUDE_CODE_ENTRYPOINT / CLAUDECODE are set by a parent Claude Code process;
|
|
99
99
|
// child instances refuse to start when they detect these.
|
|
100
|
-
// ANTHROPIC_API_KEY is cleared so Claude Code uses the subscription instead of API billing
|
|
100
|
+
// ANTHROPIC_API_KEY is cleared so Claude Code uses the subscription instead of API billing,
|
|
101
|
+
// unless the caller explicitly opts in by passing `apiKey`.
|
|
101
102
|
const parentEnvOverrides = {};
|
|
102
103
|
if (process.env.CLAUDE_CODE_ENTRYPOINT)
|
|
103
104
|
parentEnvOverrides.CLAUDE_CODE_ENTRYPOINT = "";
|
|
104
105
|
if (process.env.CLAUDECODE)
|
|
105
106
|
parentEnvOverrides.CLAUDECODE = "";
|
|
106
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
107
|
+
if (process.env.ANTHROPIC_API_KEY && !opts.apiKey) {
|
|
107
108
|
logWarning("ClaudeCodeAgent: unsetting ANTHROPIC_API_KEY so Claude Code uses your subscription. " +
|
|
108
|
-
"To use API billing instead, use ToolLoopAgent from 'ai' with anthropic() provider.", {}, "agent.init");
|
|
109
|
+
"To use API billing instead, pass `apiKey` to ClaudeCodeAgent or use ToolLoopAgent from 'ai' with anthropic() provider.", {}, "agent.init");
|
|
109
110
|
parentEnvOverrides.ANTHROPIC_API_KEY = "";
|
|
110
111
|
}
|
|
111
112
|
if (Object.keys(parentEnvOverrides).length > 0) {
|
|
@@ -446,10 +447,16 @@ export class ClaudeCodeAgent extends BaseCliAgent {
|
|
|
446
447
|
args.push(...this.extraArgs);
|
|
447
448
|
if (params.prompt)
|
|
448
449
|
args.push(params.prompt);
|
|
450
|
+
const accountEnv = {};
|
|
451
|
+
if (this.opts.configDir)
|
|
452
|
+
accountEnv.CLAUDE_CONFIG_DIR = this.opts.configDir;
|
|
453
|
+
if (this.opts.apiKey)
|
|
454
|
+
accountEnv.ANTHROPIC_API_KEY = this.opts.apiKey;
|
|
449
455
|
return {
|
|
450
456
|
command: "claude",
|
|
451
457
|
args,
|
|
452
458
|
outputFormat,
|
|
459
|
+
env: Object.keys(accountEnv).length > 0 ? accountEnv : undefined,
|
|
453
460
|
};
|
|
454
461
|
}
|
|
455
462
|
}
|
|
@@ -9,6 +9,22 @@ export type ClaudeCodeAgentOptions = BaseCliAgentOptions & {
|
|
|
9
9
|
allowDangerouslySkipPermissions?: boolean;
|
|
10
10
|
allowedTools?: string[];
|
|
11
11
|
appendSystemPrompt?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Path to an isolated Claude Code config directory. Sets `CLAUDE_CONFIG_DIR`
|
|
14
|
+
* on the spawned process so this invocation uses the credentials stored at
|
|
15
|
+
* `<configDir>/.credentials.json` (instead of the user's default `~/.claude/`).
|
|
16
|
+
*
|
|
17
|
+
* Use this to run multiple Claude Code subscriptions side-by-side. Set up
|
|
18
|
+
* the directory by running `CLAUDE_CONFIG_DIR=<path> claude` once and
|
|
19
|
+
* completing `/login` interactively.
|
|
20
|
+
*/
|
|
21
|
+
configDir?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Anthropic API key for billing this invocation against the API instead of
|
|
24
|
+
* a Claude Pro/Max subscription. When set, ClaudeCodeAgent stops unsetting
|
|
25
|
+
* `ANTHROPIC_API_KEY` (which it normally clears so subscription auth wins).
|
|
26
|
+
*/
|
|
27
|
+
apiKey?: string;
|
|
12
28
|
betas?: string[];
|
|
13
29
|
chrome?: boolean;
|
|
14
30
|
continue?: boolean;
|
package/src/CodexAgent.js
CHANGED
|
@@ -567,12 +567,18 @@ export class CodexAgent extends BaseCliAgent {
|
|
|
567
567
|
: "";
|
|
568
568
|
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}`;
|
|
569
569
|
args.push("-");
|
|
570
|
+
const accountEnv = {};
|
|
571
|
+
if (this.opts.configDir)
|
|
572
|
+
accountEnv.CODEX_HOME = this.opts.configDir;
|
|
573
|
+
if (this.opts.apiKey)
|
|
574
|
+
accountEnv.OPENAI_API_KEY = this.opts.apiKey;
|
|
570
575
|
return {
|
|
571
576
|
command: "codex",
|
|
572
577
|
args,
|
|
573
578
|
stdin: fullPrompt,
|
|
574
579
|
outputFile,
|
|
575
580
|
outputFormat: "stream-json",
|
|
581
|
+
env: Object.keys(accountEnv).length > 0 ? accountEnv : undefined,
|
|
576
582
|
stdoutBannerPatterns: [
|
|
577
583
|
// Codex CLI prints a startup banner like:
|
|
578
584
|
// "OpenAI Codex v0.99.0-alpha.13 (research preview)"
|
package/src/CodexAgentOptions.ts
CHANGED
|
@@ -20,4 +20,19 @@ export type CodexAgentOptions = BaseCliAgentOptions & {
|
|
|
20
20
|
color?: "always" | "never" | "auto";
|
|
21
21
|
json?: boolean;
|
|
22
22
|
outputLastMessage?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Path to an isolated Codex CLI config directory. Sets `CODEX_HOME` on the
|
|
25
|
+
* spawned process so this invocation uses the credentials stored at
|
|
26
|
+
* `<configDir>/auth.json` (instead of the user's default `~/.codex/`).
|
|
27
|
+
*
|
|
28
|
+
* Use this to run multiple Codex / ChatGPT subscriptions side-by-side. Set
|
|
29
|
+
* up the directory by running `CODEX_HOME=<path> codex login` once.
|
|
30
|
+
*/
|
|
31
|
+
configDir?: string;
|
|
32
|
+
/**
|
|
33
|
+
* OpenAI API key for billing this invocation against the API instead of a
|
|
34
|
+
* ChatGPT Plus/Pro subscription. Sets `OPENAI_API_KEY` on the spawned
|
|
35
|
+
* process.
|
|
36
|
+
*/
|
|
37
|
+
apiKey?: string;
|
|
23
38
|
};
|
package/src/ForgeAgent.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { BaseCliAgent, pushFlag, } from "./BaseCliAgent/index.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
/** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
|
|
3
4
|
/** @typedef {import("./BaseCliAgent/BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
|
|
4
5
|
/** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
5
6
|
/** @typedef {import("./ForgeAgentOptions.ts").ForgeAgentOptions} ForgeAgentOptions */
|
|
6
7
|
|
|
7
8
|
export class ForgeAgent extends BaseCliAgent {
|
|
8
9
|
opts;
|
|
10
|
+
/** @type {AgentCapabilityRegistry} */
|
|
9
11
|
capabilities;
|
|
10
12
|
cliEngine = "forge";
|
|
11
13
|
issuedConversationId;
|
package/src/GeminiAgent.js
CHANGED
|
@@ -56,7 +56,6 @@ export class GeminiAgent extends BaseCliAgent {
|
|
|
56
56
|
createOutputInterpreter() {
|
|
57
57
|
let sessionId;
|
|
58
58
|
let finalAnswer = "";
|
|
59
|
-
let emittedStarted = false;
|
|
60
59
|
let didEmitCompleted = false;
|
|
61
60
|
const nextSyntheticId = createSyntheticIdGenerator();
|
|
62
61
|
/**
|
|
@@ -84,7 +83,6 @@ export class GeminiAgent extends BaseCliAgent {
|
|
|
84
83
|
if (resume) {
|
|
85
84
|
sessionId = resume;
|
|
86
85
|
}
|
|
87
|
-
emittedStarted = true;
|
|
88
86
|
return [{
|
|
89
87
|
type: "started",
|
|
90
88
|
engine: this.cliEngine,
|
|
@@ -264,10 +262,16 @@ export class GeminiAgent extends BaseCliAgent {
|
|
|
264
262
|
: "";
|
|
265
263
|
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}${jsonReminder}`;
|
|
266
264
|
args.push("--prompt", fullPrompt);
|
|
265
|
+
const accountEnv = {};
|
|
266
|
+
if (this.opts.configDir)
|
|
267
|
+
accountEnv.GEMINI_DIR = this.opts.configDir;
|
|
268
|
+
if (this.opts.apiKey)
|
|
269
|
+
accountEnv.GEMINI_API_KEY = this.opts.apiKey;
|
|
267
270
|
return {
|
|
268
271
|
command: "gemini",
|
|
269
272
|
args,
|
|
270
273
|
outputFormat,
|
|
274
|
+
env: Object.keys(accountEnv).length > 0 ? accountEnv : undefined,
|
|
271
275
|
};
|
|
272
276
|
}
|
|
273
277
|
}
|
|
@@ -17,4 +17,16 @@ export type GeminiAgentOptions = BaseCliAgentOptions & {
|
|
|
17
17
|
includeDirectories?: string[];
|
|
18
18
|
screenReader?: boolean;
|
|
19
19
|
outputFormat?: "text" | "json" | "stream-json";
|
|
20
|
+
/**
|
|
21
|
+
* Path to an isolated Gemini CLI config directory. Sets `GEMINI_DIR` on the
|
|
22
|
+
* spawned process so this invocation uses the credentials stored at
|
|
23
|
+
* `<configDir>/oauth_creds.json` (instead of the user's default
|
|
24
|
+
* `~/.gemini/`). Use this to run multiple Gemini accounts side-by-side.
|
|
25
|
+
*/
|
|
26
|
+
configDir?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Gemini API key. Sets `GEMINI_API_KEY` on the spawned process for
|
|
29
|
+
* API-billed invocations.
|
|
30
|
+
*/
|
|
31
|
+
apiKey?: string;
|
|
20
32
|
};
|