@smithers-orchestrator/agents 0.16.8 → 0.17.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.8",
3
+ "version": "0.17.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.8",
53
- "@smithers-orchestrator/errors": "0.16.8",
54
- "@smithers-orchestrator/observability": "0.16.8"
53
+ "@smithers-orchestrator/driver": "0.17.0",
54
+ "@smithers-orchestrator/errors": "0.17.0",
55
+ "@smithers-orchestrator/observability": "0.17.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.
@@ -24,5 +25,5 @@ export type AgentLike = {
24
25
  * @param args.outputSchema - Optional Zod schema defining the expected structured output format
25
26
  * @returns A promise resolving to the generated output
26
27
  */
27
- generate: (args: unknown) => Promise<unknown>;
28
+ generate: (args?: AgentGenerateOptions) => Promise<unknown>;
28
29
  };
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
  /**
@@ -3,6 +3,7 @@ import { 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]
@@ -27,10 +28,10 @@ export class AnthropicAgent extends ToolLoopAgent {
27
28
  });
28
29
  }
29
30
  /**
30
- * @param {ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>} args
31
+ * @param {AgentGenerateOptions} [args]
31
32
  * @returns {Promise<GenerateTextResult<TOOLS, never>>}
32
33
  */
33
- generate(args) {
34
+ generate(args = {}) {
34
35
  const promptArgs = "messages" in args
35
36
  ? { messages: args.messages }
36
37
  : { prompt: args.prompt };
@@ -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
@@ -554,7 +532,7 @@ export class BaseCliAgent {
554
532
  this.extraArgs = opts.extraArgs;
555
533
  }
556
534
  /**
557
- * @param {AgentGenerateOptions} [options]
535
+ * @param {AgentGenerateOptions | undefined} options
558
536
  * @param {AgentInvocationOperation} operation
559
537
  * @returns {Effect.Effect<GenerateTextResult<Record<string, never>, unknown>, SmithersError>}
560
538
  */
@@ -598,9 +576,52 @@ export class BaseCliAgent {
598
576
  const recordDurationMetric = () => Effect.sync(() => performance.now() - invocationStart).pipe(Effect.flatMap((durationMs) => Metric.update(taggedMetric(agentDurationMs, metricTags), durationMs)));
599
577
  /**
600
578
  * @param {string} stderr
579
+ * @param {ReadonlyArray<RegExp>} [extraPatterns]
601
580
  * @returns {string}
602
581
  */
603
- function filterBenignStderr(stderr) {
582
+ const agentId = this.id;
583
+ const agentModel = this.model;
584
+ const agentEngine = resolveAgentEngineTag(this);
585
+ /**
586
+ * Detect well-known non-retryable CLI agent configuration errors so the
587
+ * engine surfaces them with a clear, actionable message and stops retrying
588
+ * (these errors are deterministic and will never recover by re-running).
589
+ *
590
+ * @param {string} message
591
+ * @param {string} command
592
+ * @returns {SmithersError | null}
593
+ */
594
+ function classifyNonRetryableAgentError(message, command) {
595
+ if (!message)
596
+ return null;
597
+ const nonRetryablePatterns = [
598
+ { re: /\bLLM not set\b/i, hint: "the agent's model name is not present in the CLI's configured providers" },
599
+ { re: /\bLLM not supported\b/i, hint: "the agent's model is not supported by this CLI build" },
600
+ { re: /\bmodel\s+['"]?[^'"\s]+['"]?\s+not found\b/i, hint: "the requested model is not registered with the CLI" },
601
+ { re: /\bunknown model\b/i, hint: "the requested model is not registered with the CLI" },
602
+ { 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\`)` },
603
+ { 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\`)` },
604
+ { 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\`)` },
605
+ { 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\`)` },
606
+ ];
607
+ for (const { re, hint } of nonRetryablePatterns) {
608
+ if (re.test(message)) {
609
+ const modelLabel = agentModel ?? "<unset>";
610
+ const idLabel = agentId ?? "<anonymous>";
611
+ 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.`;
612
+ return new SmithersError("AGENT_CONFIG_INVALID", summary, {
613
+ failureRetryable: false,
614
+ agentId: idLabel,
615
+ agentEngine,
616
+ agentModel: modelLabel,
617
+ command,
618
+ underlying: message.slice(0, 500),
619
+ });
620
+ }
621
+ }
622
+ return null;
623
+ }
624
+ function filterBenignStderr(stderr, extraPatterns) {
604
625
  const benignPatterns = [
605
626
  /^.*state db missing rollout path.*$/gm,
606
627
  /^.*codex_core::rollout::list.*$/gm,
@@ -612,6 +633,12 @@ export class BaseCliAgent {
612
633
  for (const pattern of benignPatterns) {
613
634
  filtered = filtered.replace(pattern, "");
614
635
  }
636
+ if (extraPatterns?.length) {
637
+ for (const pattern of extraPatterns) {
638
+ const regex = new RegExp(pattern.source, pattern.flags);
639
+ filtered = filtered.replace(regex, "");
640
+ }
641
+ }
615
642
  // Clean up extra blank lines
616
643
  return filtered.replace(/\n{3,}/g, "\n\n").trim();
617
644
  }
@@ -750,11 +777,34 @@ export class BaseCliAgent {
750
777
  }).pipe(Effect.catchAll(() => Effect.succeed(result.stdout)))
751
778
  : result.stdout;
752
779
  if (result.exitCode && result.exitCode !== 0) {
753
- const filteredStderr = filterBenignStderr(result.stderr);
780
+ const filteredStderr = filterBenignStderr(result.stderr, commandSpec.benignStderrPatterns);
754
781
  if (!(commandSpec.command === "codex" && filteredStderr.length === 0)) {
755
782
  const errorText = filteredStderr ||
756
783
  result.stdout.trim() ||
757
784
  `CLI exited with code ${result.exitCode}`;
785
+ const nonRetryable = classifyNonRetryableAgentError(errorText, commandSpec.command);
786
+ if (nonRetryable) {
787
+ return yield* Effect.fail(nonRetryable);
788
+ }
789
+ // Detect kimi session-loss. Kimi crashes mid-stream and prints
790
+ // `To resume this session: kimi -r <uuid>` to stderr (and often
791
+ // also to the merged error text after the benign-stderr filter
792
+ // strips the bare-line variant). The session itself is corrupt
793
+ // — re-running with `--session <same-uuid>` deterministically
794
+ // reproduces the same crash. Surface a typed error that tells
795
+ // the engine retry path to DROP the broken session id and
796
+ // start a fresh one on the next attempt.
797
+ const rawStderr = result.stderr ?? "";
798
+ const sessionLossMatch = rawStderr.match(/kimi -r ([0-9a-f-]{8,})/i)
799
+ || errorText.match(/kimi -r ([0-9a-f-]{8,})/i);
800
+ if (commandSpec.command === "kimi" && sessionLossMatch) {
801
+ 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.`, {
802
+ failureRetryable: true,
803
+ discardResumeSession: true,
804
+ command: "kimi",
805
+ kimiSessionId: sessionLossMatch[1],
806
+ }));
807
+ }
758
808
  return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", errorText));
759
809
  }
760
810
  }
@@ -778,7 +828,9 @@ export class BaseCliAgent {
778
828
  for (const pattern of stdoutErrorPatterns) {
779
829
  const regex = new RegExp(pattern.source, pattern.flags);
780
830
  if (regex.test(rawText)) {
781
- return yield* Effect.fail(new SmithersError("AGENT_CLI_ERROR", `CLI agent error (stdout): ${rawText.slice(0, 500)}`));
831
+ const stdoutErrText = `CLI agent error (stdout): ${rawText.slice(0, 500)}`;
832
+ const nonRetryable = classifyNonRetryableAgentError(rawText, commandSpec.command);
833
+ return yield* Effect.fail(nonRetryable ?? new SmithersError("AGENT_CLI_ERROR", stdoutErrText));
782
834
  }
783
835
  }
784
836
  }
@@ -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
@@ -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
  };
package/src/KimiAgent.js CHANGED
@@ -1,9 +1,190 @@
1
- import { mkdtempSync, cpSync, existsSync, rmSync } from "node:fs";
1
+ import { mkdtempSync, cpSync, existsSync, readFileSync, writeFileSync, readdirSync, rmSync, renameSync } from "node:fs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir, homedir } from "node:os";
5
5
  import { BaseCliAgent, pushFlag, pushList, isRecord, asString, toolKindFromName, createSyntheticIdGenerator, } from "./BaseCliAgent/index.js";
6
6
  import { normalizeCapabilityStringList, } from "./capability-registry/index.js";
7
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
8
+
9
+ /**
10
+ * The kimi CLI's OAuth refresh endpoint, mirroring the Python implementation
11
+ * in `kimi_cli.auth.oauth.refresh_token`. Honour the same env-var override
12
+ * the CLI uses (KIMI_OAUTH_HOST) so test/staging overrides keep working.
13
+ */
14
+ function kimiOAuthHost() {
15
+ return process.env.KIMI_OAUTH_HOST?.replace(/\/+$/, "") || "https://auth.kimi.com";
16
+ }
17
+
18
+ /**
19
+ * Process-level dedup map: if multiple parallel KimiAgent invocations land in
20
+ * the refresh path concurrently, share one in-flight Promise so we issue a
21
+ * single POST instead of racing (kimi rotates refresh tokens — only one
22
+ * refresh wins, the other gets invalid_grant and would be wrongly classified
23
+ * as expired).
24
+ *
25
+ * @type {Map<string, Promise<void>>}
26
+ */
27
+ const inflightRefreshes = new Map();
28
+
29
+ async function refreshKimiTokenIfNeeded(credsDir, fileName) {
30
+ const path = join(credsDir, fileName);
31
+ /** @type {{access_token?: string, refresh_token?: string, expires_at?: number, token_type?: string, scope?: string} | null} */
32
+ let data = null;
33
+ try {
34
+ data = JSON.parse(readFileSync(path, "utf8"));
35
+ }
36
+ catch {
37
+ return { ok: false, reason: "unreadable", expiredAt: null };
38
+ }
39
+ if (!data || typeof data.expires_at !== "number") {
40
+ // Not an OAuth file — leave alone, kimi will handle.
41
+ return { ok: true, refreshed: false };
42
+ }
43
+ // Refresh proactively a bit before expiry to avoid races (matches the
44
+ // CLI's _refresh_threshold behaviour, simplified to a fixed 60s window).
45
+ const nowSec = Date.now() / 1000;
46
+ if (data.expires_at - 60 > nowSec) {
47
+ return { ok: true, refreshed: false };
48
+ }
49
+ if (typeof data.refresh_token !== "string" || data.refresh_token.length === 0) {
50
+ return { ok: false, reason: "no-refresh-token", expiredAt: new Date(data.expires_at * 1000).toISOString() };
51
+ }
52
+ // Dedupe concurrent refreshes per-credential-file within this process.
53
+ const flightKey = path;
54
+ const inflight = inflightRefreshes.get(flightKey);
55
+ if (inflight) {
56
+ try {
57
+ await inflight;
58
+ return { ok: true, refreshed: true, deduped: true };
59
+ }
60
+ catch (err) {
61
+ return { ok: false, reason: err?.message ?? "refresh-failed", expiredAt: new Date(data.expires_at * 1000).toISOString() };
62
+ }
63
+ }
64
+ const refresher = (async () => {
65
+ const tokenUrl = `${kimiOAuthHost()}/api/oauth/token`;
66
+ // client_id matches kimi_cli.auth.oauth.KIMI_CODE_CLIENT_ID. Without it the
67
+ // /api/oauth/token endpoint returns 400 invalid_request.
68
+ const body = new URLSearchParams({
69
+ client_id: "17e5f671-d194-4dfb-9706-5516cb48c098",
70
+ grant_type: "refresh_token",
71
+ refresh_token: data.refresh_token,
72
+ });
73
+ // Mirror kimi-cli's `_common_headers()` so the auth service treats this
74
+ // refresh as coming from a legitimate kimi-cli install. Some of these
75
+ // (notably X-Msh-Device-Id) appear to gate refresh acceptance.
76
+ const headers = {
77
+ "Content-Type": "application/x-www-form-urlencoded",
78
+ "X-Msh-Platform": "kimi_cli",
79
+ "X-Msh-Version": "1.37.0",
80
+ "X-Msh-Device-Name": "smithers-orchestrator",
81
+ "X-Msh-Device-Model": "smithers-orchestrator",
82
+ "X-Msh-Os-Version": process.platform,
83
+ };
84
+ try {
85
+ const deviceIdPath = join(credsDir, "..", "device_id");
86
+ if (existsSync(deviceIdPath)) {
87
+ const deviceId = readFileSync(deviceIdPath, "utf8").trim();
88
+ if (deviceId)
89
+ headers["X-Msh-Device-Id"] = deviceId;
90
+ }
91
+ }
92
+ catch { /* device-id is optional */ }
93
+ const resp = await fetch(tokenUrl, {
94
+ method: "POST",
95
+ headers,
96
+ body,
97
+ });
98
+ if (!resp.ok) {
99
+ const text = await resp.text().catch(() => "");
100
+ const tag = resp.status === 401 ? "invalid_grant" : `http-${resp.status}`;
101
+ throw new Error(`kimi oauth refresh failed (${tag}): ${text.slice(0, 200)}`);
102
+ }
103
+ /** @type {any} */
104
+ const fresh = await resp.json();
105
+ if (typeof fresh?.access_token !== "string") {
106
+ throw new Error("kimi oauth refresh: missing access_token in response");
107
+ }
108
+ const expiresIn = typeof fresh.expires_in === "number"
109
+ ? fresh.expires_in
110
+ : 3600;
111
+ const merged = {
112
+ ...data,
113
+ access_token: fresh.access_token,
114
+ refresh_token: typeof fresh.refresh_token === "string" ? fresh.refresh_token : data.refresh_token,
115
+ token_type: typeof fresh.token_type === "string" ? fresh.token_type : data.token_type,
116
+ scope: typeof fresh.scope === "string" ? fresh.scope : data.scope,
117
+ expires_in: expiresIn,
118
+ expires_at: Math.floor(Date.now() / 1000) + expiresIn,
119
+ };
120
+ // Write atomically so kimi-cli reading concurrently never sees a torn file.
121
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
122
+ writeFileSync(tmp, JSON.stringify(merged, null, 2), { mode: 0o600 });
123
+ renameSync(tmp, path);
124
+ })();
125
+ inflightRefreshes.set(flightKey, refresher);
126
+ try {
127
+ await refresher;
128
+ return { ok: true, refreshed: true };
129
+ }
130
+ catch (err) {
131
+ return { ok: false, reason: err?.message ?? "refresh-failed", expiredAt: new Date(data.expires_at * 1000).toISOString() };
132
+ }
133
+ finally {
134
+ inflightRefreshes.delete(flightKey);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Inspect the kimi CLI's on-disk OAuth credentials and, if any are expired,
140
+ * attempt a non-interactive refresh against `${KIMI_OAUTH_HOST or
141
+ * https://auth.kimi.com}/api/oauth/token` using the stored refresh_token.
142
+ * Only if refresh fails do we surface a clear, non-retryable
143
+ * AGENT_CONFIG_INVALID error.
144
+ *
145
+ * @param {string} shareDir
146
+ * @param {string} agentId
147
+ * @param {string} agentModel
148
+ */
149
+ async function ensureKimiCredentialsUsable(shareDir, agentId, agentModel) {
150
+ const credsDir = join(shareDir, "credentials");
151
+ if (!existsSync(credsDir))
152
+ return; // No creds dir → kimi will print "LLM not set" which the BaseCliAgent classifier handles.
153
+ let entries;
154
+ try {
155
+ entries = readdirSync(credsDir);
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ const tokenFiles = entries.filter((n) => n.endsWith(".json"));
161
+ if (tokenFiles.length === 0)
162
+ return;
163
+ let lastFailure = null;
164
+ let anyUsable = false;
165
+ for (const name of tokenFiles) {
166
+ const result = await refreshKimiTokenIfNeeded(credsDir, name);
167
+ if (result.ok) {
168
+ anyUsable = true;
169
+ }
170
+ else {
171
+ lastFailure = result;
172
+ }
173
+ }
174
+ if (!anyUsable && lastFailure) {
175
+ const reason = lastFailure.reason === "no-refresh-token"
176
+ ? `OAuth token expired at ${lastFailure.expiredAt} and no refresh_token is stored`
177
+ : `OAuth token expired at ${lastFailure.expiredAt}; auto-refresh failed: ${lastFailure.reason}`;
178
+ throw new SmithersError("AGENT_CONFIG_INVALID", `${reason}. Run \`kimi login\` to re-authenticate, then resume the run. (agent="${agentId}", model="${agentModel}", credentials="${credsDir}")`, {
179
+ failureRetryable: false,
180
+ agentId,
181
+ agentEngine: "kimi",
182
+ agentModel,
183
+ command: "kimi",
184
+ underlying: reason,
185
+ });
186
+ }
187
+ }
7
188
  /** @typedef {import("./BaseCliAgent/BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
8
189
  /** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
9
190
  /** @typedef {import("./BaseCliAgent/CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
@@ -165,13 +346,18 @@ export class KimiAgent extends BaseCliAgent {
165
346
  let cleanup;
166
347
  // Isolate kimi metadata per invocation to avoid concurrent writes to
167
348
  // ~/.kimi/kimi.json across parallel tasks. If caller explicitly provides
168
- // KIMI_SHARE_DIR in opts.env, preserve that override.
169
- if (!this.opts.env?.KIMI_SHARE_DIR) {
170
- const defaultShareDir = process.env.KIMI_SHARE_DIR ?? join(homedir(), ".kimi");
349
+ // configDir or KIMI_SHARE_DIR in opts.env, preserve that override.
350
+ const explicitShareDir = this.opts.configDir ?? this.opts.env?.KIMI_SHARE_DIR;
351
+ const sourceShareDir = explicitShareDir ?? process.env.KIMI_SHARE_DIR ?? join(homedir(), ".kimi");
352
+ // Refresh expired OAuth credentials in place using the stored refresh_token,
353
+ // and only fail fast (non-retryable) if the refresh itself fails. This avoids
354
+ // forcing the user to run `kimi login` every time their access_token rotates.
355
+ await ensureKimiCredentialsUsable(sourceShareDir, this.id ?? "<anonymous>", this.opts.model ?? this.model ?? "<unset>");
356
+ if (!explicitShareDir) {
171
357
  const isolatedShareDir = mkdtempSync(join(tmpdir(), "kimi-share-"));
172
- if (existsSync(defaultShareDir)) {
358
+ if (existsSync(sourceShareDir)) {
173
359
  for (const name of ["config.toml", "credentials", "device_id", "latest_version.txt"]) {
174
- const src = join(defaultShareDir, name);
360
+ const src = join(sourceShareDir, name);
175
361
  if (existsSync(src)) {
176
362
  try {
177
363
  cpSync(src, join(isolatedShareDir, name), { recursive: true });
@@ -187,6 +373,11 @@ export class KimiAgent extends BaseCliAgent {
187
373
  rmSync(isolatedShareDir, { recursive: true, force: true });
188
374
  };
189
375
  }
376
+ else if (this.opts.configDir) {
377
+ // configDir takes precedence over any env-var inheritance: spawn the
378
+ // CLI with KIMI_SHARE_DIR pointing at the user-specified path.
379
+ commandEnv = { KIMI_SHARE_DIR: this.opts.configDir };
380
+ }
190
381
  // Print mode is required for non-interactive execution
191
382
  // Note: --print implicitly adds --yolo
192
383
  args.push("--print");
@@ -253,6 +444,20 @@ export class KimiAgent extends BaseCliAgent {
253
444
  /^Interrupted by user$/i,
254
445
  /^Unknown error:/i,
255
446
  /^Error:/i,
447
+ // Auth failures kimi prints when OAuth/api-key is invalid or expired.
448
+ // BaseCliAgent's classifyNonRetryableAgentError treats these as
449
+ // non-retryable so the run does not waste turns on a 401 loop.
450
+ /Error code:\s*401\b[^\n]*/i,
451
+ /\binvalid_authentication_error\b/i,
452
+ /API\s*Key\s+appears to be invalid or may have expired/i,
453
+ ],
454
+ // The kimi CLI emits "To resume this session: kimi -r <id>" to stderr
455
+ // on every non-zero exit (it's a hint for interactive users, not the
456
+ // actual error). Strip it so the real underlying error surfaces — and
457
+ // when it's the only stderr content, our runner will fall back to a
458
+ // useful "exited with code N" message that the engine can retry.
459
+ benignStderrPatterns: [
460
+ /^\s*To resume this session: kimi -r [0-9a-f-]+\s*$/gim,
256
461
  ],
257
462
  errorOnBannerOnly: true,
258
463
  };
@@ -18,4 +18,12 @@ export type KimiAgentOptions = BaseCliAgentOptions & {
18
18
  maxRalphIterations?: number;
19
19
  verbose?: boolean;
20
20
  debug?: boolean;
21
+ /**
22
+ * Path to an isolated Kimi share directory. Sets `KIMI_SHARE_DIR` on the
23
+ * spawned process so this invocation reads/writes credentials at
24
+ * `<configDir>/credentials` (instead of the user's default `~/.kimi/`).
25
+ * Equivalent to passing `env: { KIMI_SHARE_DIR: <path> }` but uniform with
26
+ * the other agents' `configDir` option.
27
+ */
28
+ configDir?: string;
21
29
  };
@@ -3,6 +3,7 @@ import { 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, TOOLS
@@ -27,10 +28,10 @@ export class OpenAIAgent extends ToolLoopAgent {
27
28
  });
28
29
  }
29
30
  /**
30
- * @param {ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>} args
31
+ * @param {AgentGenerateOptions} [args]
31
32
  * @returns {Promise<GenerateTextResult<TOOLS, never>>}
32
33
  */
33
- generate(args) {
34
+ generate(args = {}) {
34
35
  const promptArgs = "messages" in args
35
36
  ? { messages: args.messages }
36
37
  : { prompt: args.prompt };
package/src/PiAgent.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // @smithers-type-exports-end
5
5
 
6
6
  import { Effect } from "effect";
7
- import { BaseCliAgent, buildGenerateResult, combineNonEmpty, extractPrompt, extractTextFromJsonValue, pushFlag, resolveTimeouts, runAgentPromise, runRpcCommandEffect, tryParseJson, asString, truncate, toolKindFromName, } from "./BaseCliAgent/index.js";
7
+ import { BaseCliAgent, buildGenerateResult, combineNonEmpty, extractPrompt, extractTextFromJsonValue, pushFlag, resolveTimeouts, runAgentPromise, runRpcCommandEffect, asString, truncate, toolKindFromName, } from "./BaseCliAgent/index.js";
8
8
  import { normalizeCapabilityStringList, } from "./capability-registry/index.js";
9
9
  import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
10
10
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
@@ -0,0 +1,26 @@
1
+ import type {
2
+ AgentLike,
3
+ AmpAgent,
4
+ AnthropicAgent,
5
+ ClaudeCodeAgent,
6
+ CodexAgent,
7
+ ForgeAgent,
8
+ GeminiAgent,
9
+ KimiAgent,
10
+ OpenAIAgent,
11
+ PiAgent,
12
+ } from "../index.js";
13
+
14
+ type AssertAssignable<T extends AgentLike> = T;
15
+
16
+ type _ConcreteAgentsAreAgentLike = [
17
+ AssertAssignable<AmpAgent>,
18
+ AssertAssignable<AnthropicAgent>,
19
+ AssertAssignable<ClaudeCodeAgent>,
20
+ AssertAssignable<CodexAgent>,
21
+ AssertAssignable<ForgeAgent>,
22
+ AssertAssignable<GeminiAgent>,
23
+ AssertAssignable<KimiAgent>,
24
+ AssertAssignable<OpenAIAgent>,
25
+ AssertAssignable<PiAgent>,
26
+ ];
package/src/index.d.ts CHANGED
@@ -4,7 +4,6 @@ import { ToolLoopAgent, ToolSet, ToolLoopAgentSettings } from 'ai';
4
4
  import { anthropic } from '@ai-sdk/anthropic';
5
5
  import { Effect } from 'effect';
6
6
  import { SmithersError } from '@smithers-orchestrator/errors/SmithersError';
7
- import * as zod from 'zod';
8
7
  import * as zod_v4_core from 'zod/v4/core';
9
8
 
10
9
  type SmithersToolSurface$2 = "raw" | "semantic";
@@ -165,7 +164,7 @@ type AgentLike$1 = {
165
164
  * @param args.outputSchema - Optional Zod schema defining the expected structured output format
166
165
  * @returns A promise resolving to the generated output
167
166
  */
168
- generate: (args: unknown) => Promise<unknown>;
167
+ generate: (args?: AgentGenerateOptions) => Promise<unknown>;
169
168
  };
170
169
 
171
170
  type RunCommandResult = {
@@ -238,11 +237,11 @@ declare class BaseCliAgent {
238
237
  maxOutputBytes: number | undefined;
239
238
  extraArgs: string[] | undefined;
240
239
  /**
241
- * @param {AgentGenerateOptions} [options]
240
+ * @param {AgentGenerateOptions | undefined} options
242
241
  * @param {AgentInvocationOperation} operation
243
242
  * @returns {Effect.Effect<GenerateTextResult<Record<string, never>, unknown>, SmithersError>}
244
243
  */
245
- runGenerateEffect(options?: AgentGenerateOptions, operation: AgentInvocationOperation): Effect.Effect<GenerateTextResult$3<Record<string, never>, unknown>, SmithersError>;
244
+ runGenerateEffect(options: AgentGenerateOptions | undefined, operation: AgentInvocationOperation): Effect.Effect<GenerateTextResult$3<Record<string, never>, unknown>, SmithersError>;
246
245
  /**
247
246
  * @param {AgentGenerateOptions} [options]
248
247
  * @returns {Promise<GenerateTextResult<Record<string, never>, unknown>>}
@@ -304,20 +303,12 @@ declare class AnthropicAgent extends ToolLoopAgent<never, any, never> {
304
303
  constructor(opts: AnthropicAgentOptions$1<CALL_OPTIONS, TOOLS>);
305
304
  hijackEngine: string;
306
305
  /**
307
- * @param {ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>} args
306
+ * @param {AgentGenerateOptions} [args]
308
307
  * @returns {Promise<GenerateTextResult<TOOLS, never>>}
309
308
  */
310
- generate(args: ExtendedGenerateArgs$1<CALL_OPTIONS, TOOLS>): Promise<GenerateTextResult$2<TOOLS, never>>;
309
+ generate(args?: AgentGenerateOptions): Promise<GenerateTextResult$2<TOOLS, never>>;
311
310
  }
312
- type AgentCallParameters$1 = any;
313
311
  type AnthropicAgentOptions$1<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = AnthropicAgentOptions$2<CALL_OPTIONS, TOOLS>;
314
- type ExtendedGenerateArgs$1<CALL_OPTIONS, TOOLS> = AgentCallParameters$1<CALL_OPTIONS, TOOLS> & {
315
- onStdout?: (text: string) => void;
316
- onStderr?: (text: string) => void;
317
- onEvent?: (event: unknown) => Promise<void> | void;
318
- outputSchema?: zod.ZodTypeAny;
319
- resumeSession?: string;
320
- };
321
312
  type GenerateTextResult$2 = ai.GenerateTextResult<any, any>;
322
313
 
323
314
  /** @typedef {import("ai").AgentCallParameters} AgentCallParameters */
@@ -337,19 +328,11 @@ declare class OpenAIAgent extends ToolLoopAgent<never, any, never> {
337
328
  constructor(opts: OpenAIAgentOptions$1<CALL_OPTIONS, TOOLS>);
338
329
  hijackEngine: string;
339
330
  /**
340
- * @param {ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>} args
331
+ * @param {AgentGenerateOptions} [args]
341
332
  * @returns {Promise<GenerateTextResult<TOOLS, never>>}
342
333
  */
343
- generate(args: ExtendedGenerateArgs<CALL_OPTIONS, TOOLS>): Promise<GenerateTextResult$1<TOOLS, never>>;
334
+ generate(args?: AgentGenerateOptions): Promise<GenerateTextResult$1<TOOLS, never>>;
344
335
  }
345
- type AgentCallParameters = any;
346
- type ExtendedGenerateArgs<CALL_OPTIONS, TOOLS> = AgentCallParameters<CALL_OPTIONS, TOOLS> & {
347
- onStdout?: (text: string) => void;
348
- onStderr?: (text: string) => void;
349
- onEvent?: (event: unknown) => Promise<void> | void;
350
- outputSchema?: zod.ZodTypeAny;
351
- resumeSession?: string;
352
- };
353
336
  type GenerateTextResult$1 = ai.GenerateTextResult<any, any>;
354
337
  type OpenAIAgentOptions$1<CALL_OPTIONS = never, TOOLS = ai.ToolSet> = OpenAIAgentOptions$2<CALL_OPTIONS, TOOLS>;
355
338
 
@@ -391,25 +374,7 @@ declare class AmpAgent extends BaseCliAgent {
391
374
  */
392
375
  constructor(opts?: AmpAgentOptions);
393
376
  opts: AmpAgentOptions$1;
394
- capabilities: {
395
- version: number;
396
- engine: string;
397
- runtimeTools: {};
398
- mcp: {
399
- bootstrap: string;
400
- supportsProjectScope: boolean;
401
- supportsUserScope: boolean;
402
- };
403
- skills: {
404
- supportsSkills: boolean;
405
- smithersSkillIds: never[];
406
- };
407
- humanInteraction: {
408
- supportsUiRequests: boolean;
409
- methods: never[];
410
- };
411
- builtIns: string[];
412
- };
377
+ capabilities: AgentCapabilityRegistry$3;
413
378
  cliEngine: string;
414
379
  /**
415
380
  * @returns {CliOutputInterpreter}
@@ -526,6 +491,21 @@ type CodexAgentOptions$1 = BaseCliAgentOptions$1 & {
526
491
  color?: "always" | "never" | "auto";
527
492
  json?: boolean;
528
493
  outputLastMessage?: string;
494
+ /**
495
+ * Path to an isolated Codex CLI config directory. Sets `CODEX_HOME` on the
496
+ * spawned process so this invocation uses the credentials stored at
497
+ * `<configDir>/auth.json` (instead of the user's default `~/.codex/`).
498
+ *
499
+ * Use this to run multiple Codex / ChatGPT subscriptions side-by-side. Set
500
+ * up the directory by running `CODEX_HOME=<path> codex login` once.
501
+ */
502
+ configDir?: string;
503
+ /**
504
+ * OpenAI API key for billing this invocation against the API instead of a
505
+ * ChatGPT Plus/Pro subscription. Sets `OPENAI_API_KEY` on the spawned
506
+ * process.
507
+ */
508
+ apiKey?: string;
529
509
  };
530
510
 
531
511
  declare class CodexAgent extends BaseCliAgent {
@@ -758,25 +738,7 @@ declare class ForgeAgent extends BaseCliAgent {
758
738
  */
759
739
  constructor(opts?: ForgeAgentOptions);
760
740
  opts: ForgeAgentOptions$1;
761
- capabilities: {
762
- version: number;
763
- engine: string;
764
- runtimeTools: {};
765
- mcp: {
766
- bootstrap: string;
767
- supportsProjectScope: boolean;
768
- supportsUserScope: boolean;
769
- };
770
- skills: {
771
- supportsSkills: boolean;
772
- smithersSkillIds: never[];
773
- };
774
- humanInteraction: {
775
- supportsUiRequests: boolean;
776
- methods: never[];
777
- };
778
- builtIns: string[];
779
- };
741
+ capabilities: AgentCapabilityRegistry$3;
780
742
  cliEngine: string;
781
743
  issuedConversationId: any;
782
744
  /**
@@ -869,4 +831,4 @@ type SmithersAgentToolCategory = SmithersAgentToolCategory$1;
869
831
  type SmithersListedTool = SmithersListedTool$2;
870
832
  type SmithersToolSurface = SmithersToolSurface$2;
871
833
 
872
- export { type AgentCapabilityRegistry, type AgentLike, type AgentToolDescriptor, AmpAgent, AnthropicAgent, type AnthropicAgentOptions, BaseCliAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, KimiAgent, OpenAIAgent, type OpenAIAgentOptions, PiAgent, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type SmithersAgentContract, type SmithersAgentContractTool, type SmithersAgentToolCategory, type SmithersListedTool, type SmithersToolSurface, createSmithersAgentContract, hashCapabilityRegistry, renderSmithersAgentPromptGuidance, sanitizeForOpenAI, zodToOpenAISchema };
834
+ export { type AgentCapabilityRegistry, type AgentGenerateOptions, type AgentLike, type AgentToolDescriptor, AmpAgent, AnthropicAgent, type AnthropicAgentOptions, BaseCliAgent, ClaudeCodeAgent, CodexAgent, ForgeAgent, GeminiAgent, KimiAgent, OpenAIAgent, type OpenAIAgentOptions, PiAgent, type PiAgentOptions, type PiExtensionUiRequest, type PiExtensionUiResponse, type SmithersAgentContract, type SmithersAgentContractTool, type SmithersAgentToolCategory, type SmithersListedTool, type SmithersToolSurface, createSmithersAgentContract, hashCapabilityRegistry, renderSmithersAgentPromptGuidance, sanitizeForOpenAI, zodToOpenAISchema };
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @smithers-type-exports-begin
2
2
  /** @typedef {import("./capability-registry/AgentCapabilityRegistry.ts").AgentCapabilityRegistry} AgentCapabilityRegistry */
3
+ /** @typedef {import("./BaseCliAgent/AgentGenerateOptions.ts").AgentGenerateOptions} AgentGenerateOptions */
3
4
  /** @typedef {import("./AgentLike.ts").AgentLike} AgentLike */
4
5
  /** @typedef {import("./capability-registry/AgentToolDescriptor.ts").AgentToolDescriptor} AgentToolDescriptor */
5
6
  /**