@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.16.9",
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/driver": "0.16.9",
53
- "@smithers-orchestrator/errors": "0.16.9",
54
- "@smithers-orchestrator/observability": "0.16.9"
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: unknown) => Promise<unknown>;
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
  /**
@@ -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 {ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>} args
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, toolOutputTruncatedTotal, } from "@smithers-orchestrator/observability/metrics";
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
- const text = extractTextFromJsonValue(parsed);
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} [options]
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
- function filterBenignStderr(stderr) {
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 errorText = filteredStderr ||
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
- return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", `CLI agent error (stdout): ${rawText.slice(0, 500)}`));
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);
@@ -1,4 +1,3 @@
1
- import { extractTextFromJsonValue } from "./extractTextFromJsonValue.js";
2
1
  /** @typedef {import("ai").ModelMessage} ModelMessage */
3
2
  /**
4
3
  * @typedef {{ prompt: string; systemFromMessages?: string; }} PromptParts
@@ -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 */
@@ -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)"
@@ -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;
@@ -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
  };