@smithers-orchestrator/agents 0.16.9 → 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 +6 -4
- package/src/AgentLike.ts +2 -1
- package/src/AmpAgent.js +2 -0
- package/src/AnthropicAgent.js +3 -2
- package/src/BaseCliAgent/AgentGenerateOptions.ts +24 -0
- package/src/BaseCliAgent/BaseCliAgent.js +80 -28
- package/src/BaseCliAgent/extractPrompt.js +0 -1
- 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 +3 -2
- package/src/PiAgent.js +1 -1
- package/src/__type-tests__/AgentLike.assignability.test-d.ts +26 -0
- package/src/index.d.ts +25 -63
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
53
|
-
"@smithers-orchestrator/errors": "0.
|
|
54
|
-
"@smithers-orchestrator/observability": "0.
|
|
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
|
|
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
|
/**
|
package/src/AnthropicAgent.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
};
|
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
|
-
|
|
170
|
-
|
|
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(
|
|
358
|
+
if (existsSync(sourceShareDir)) {
|
|
173
359
|
for (const name of ["config.toml", "credentials", "device_id", "latest_version.txt"]) {
|
|
174
|
-
const src = join(
|
|
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
|
};
|
package/src/KimiAgentOptions.ts
CHANGED
|
@@ -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
|
};
|
package/src/OpenAIAgent.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
|
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}
|
|
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
|
|
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 {
|
|
306
|
+
* @param {AgentGenerateOptions} [args]
|
|
308
307
|
* @returns {Promise<GenerateTextResult<TOOLS, never>>}
|
|
309
308
|
*/
|
|
310
|
-
generate(args
|
|
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 {
|
|
331
|
+
* @param {AgentGenerateOptions} [args]
|
|
341
332
|
* @returns {Promise<GenerateTextResult<TOOLS, never>>}
|
|
342
333
|
*/
|
|
343
|
-
generate(args
|
|
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
|
/**
|