feat: add CLI backend fallback

This commit is contained in:
Peter Steinberger
2026-01-10 23:31:25 +00:00
parent 07be761779
commit d8f1124d59
18 changed files with 1448 additions and 472 deletions

View File

@@ -112,6 +112,7 @@ const FIELD_LABELS: Record<string, string> = {
"agents.defaults.humanDelay.mode": "Human Delay Mode",
"agents.defaults.humanDelay.minMs": "Human Delay Min (ms)",
"agents.defaults.humanDelay.maxMs": "Human Delay Max (ms)",
"agents.defaults.cliBackends": "CLI Backends",
"commands.native": "Native Commands",
"commands.text": "Text Commands",
"commands.restart": "Allow Restart",
@@ -191,6 +192,8 @@ const FIELD_HELP: Record<string, string> = {
"Optional image model (provider/model) used when the primary model lacks image input.",
"agents.defaults.imageModel.fallbacks":
"Ordered fallback image models (provider/model).",
"agents.defaults.cliBackends":
"Optional CLI backends for text-only fallback (claude-cli, etc.).",
"agents.defaults.humanDelay.mode":
'Delay style for block replies ("off", "natural", "custom").',
"agents.defaults.humanDelay.minMs":

View File

@@ -113,6 +113,7 @@ export type SessionEntry = {
model?: string;
contextTokens?: number;
compactionCount?: number;
cliSessionIds?: Record<string, string>;
claudeCliSessionId?: string;
label?: string;
displayName?: string;

View File

@@ -1355,6 +1355,45 @@ export type AgentContextPruningConfig = {
};
};
export type CliBackendConfig = {
/** CLI command to execute (absolute path or on PATH). */
command: string;
/** Base args applied to every invocation. */
args?: string[];
/** Output parsing mode (default: json). */
output?: "json" | "text";
/** Prompt input mode (default: arg). */
input?: "arg" | "stdin";
/** Max prompt length for arg mode (if exceeded, stdin is used). */
maxPromptArgChars?: number;
/** Extra env vars injected for this CLI. */
env?: Record<string, string>;
/** Env vars to remove before launching this CLI. */
clearEnv?: string[];
/** Flag used to pass model id (e.g. --model). */
modelArg?: string;
/** Model aliases mapping (config model id → CLI model id). */
modelAliases?: Record<string, string>;
/** Flag used to pass session id (e.g. --session-id). */
sessionArg?: string;
/** When to pass session ids. */
sessionMode?: "always" | "existing" | "none";
/** JSON fields to read session id from (in order). */
sessionIdFields?: string[];
/** Flag used to pass system prompt. */
systemPromptArg?: string;
/** System prompt behavior (append vs replace). */
systemPromptMode?: "append" | "replace";
/** When to send system prompt. */
systemPromptWhen?: "first" | "always" | "never";
/** Flag used to pass image paths. */
imageArg?: string;
/** How to pass multiple images. */
imageMode?: "repeat" | "list";
/** Serialize runs for this CLI. */
serialize?: boolean;
};
export type AgentDefaultsConfig = {
/** Primary model and fallbacks (provider/model). */
model?: AgentModelListConfig;
@@ -1370,6 +1409,8 @@ export type AgentDefaultsConfig = {
userTimezone?: string;
/** Optional display-only context window override (used for % in status UIs). */
contextTokens?: number;
/** Optional CLI backends for text-only fallback (claude-cli, etc.). */
cliBackends?: Record<string, CliBackendConfig>;
/** Opt-in: prune old tool results from the LLM context to reduce token usage. */
contextPruning?: AgentContextPruningConfig;
/** Default thinking level when no /think directive is present. */

View File

@@ -124,6 +124,33 @@ const HumanDelaySchema = z.object({
maxMs: z.number().int().nonnegative().optional(),
});
const CliBackendSchema = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
output: z.union([z.literal("json"), z.literal("text")]).optional(),
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
maxPromptArgChars: z.number().int().positive().optional(),
env: z.record(z.string(), z.string()).optional(),
clearEnv: z.array(z.string()).optional(),
modelArg: z.string().optional(),
modelAliases: z.record(z.string(), z.string()).optional(),
sessionArg: z.string().optional(),
sessionMode: z
.union([z.literal("always"), z.literal("existing"), z.literal("none")])
.optional(),
sessionIdFields: z.array(z.string()).optional(),
systemPromptArg: z.string().optional(),
systemPromptMode: z
.union([z.literal("append"), z.literal("replace")])
.optional(),
systemPromptWhen: z
.union([z.literal("first"), z.literal("always"), z.literal("never")])
.optional(),
imageArg: z.string().optional(),
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
serialize: z.boolean().optional(),
});
const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
@@ -1037,6 +1064,7 @@ const AgentDefaultsSchema = z
skipBootstrap: z.boolean().optional(),
userTimezone: z.string().optional(),
contextTokens: z.number().int().positive().optional(),
cliBackends: z.record(z.string(), CliBackendSchema).optional(),
contextPruning: z
.object({
mode: z