feat: add codex cli backend
This commit is contained in:
@@ -47,6 +47,38 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
||||
serialize: true,
|
||||
};
|
||||
|
||||
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
||||
command: "codex",
|
||||
args: [
|
||||
"exec",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
resumeArgs: [
|
||||
"exec",
|
||||
"resume",
|
||||
"{sessionId}",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
output: "jsonl",
|
||||
resumeOutput: "text",
|
||||
input: "arg",
|
||||
modelArg: "--model",
|
||||
sessionIdFields: ["thread_id"],
|
||||
sessionMode: "existing",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
serialize: true,
|
||||
};
|
||||
|
||||
function normalizeBackendKey(key: string): string {
|
||||
return normalizeProviderId(key);
|
||||
}
|
||||
@@ -76,11 +108,16 @@ function mergeBackendConfig(
|
||||
new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])]),
|
||||
),
|
||||
sessionIdFields: override.sessionIdFields ?? base.sessionIdFields,
|
||||
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
||||
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCliBackendIds(cfg?: ClawdbotConfig): Set<string> {
|
||||
const ids = new Set<string>([normalizeBackendKey("claude-cli")]);
|
||||
const ids = new Set<string>([
|
||||
normalizeBackendKey("claude-cli"),
|
||||
normalizeBackendKey("codex-cli"),
|
||||
]);
|
||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
for (const key of Object.keys(configured)) {
|
||||
ids.add(normalizeBackendKey(key));
|
||||
@@ -102,6 +139,12 @@ export function resolveCliBackendConfig(
|
||||
if (!command) return null;
|
||||
return { id: normalized, config: { ...merged, command } };
|
||||
}
|
||||
if (normalized === "codex-cli") {
|
||||
const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override);
|
||||
const command = merged.command?.trim();
|
||||
if (!command) return null;
|
||||
return { id: normalized, config: { ...merged, command } };
|
||||
}
|
||||
|
||||
if (!override) return null;
|
||||
const command = override.command?.trim();
|
||||
|
||||
@@ -178,7 +178,10 @@ function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
||||
: undefined;
|
||||
const input = pick("input_tokens") ?? pick("inputTokens");
|
||||
const output = pick("output_tokens") ?? pick("outputTokens");
|
||||
const cacheRead = pick("cache_read_input_tokens") ?? pick("cacheRead");
|
||||
const cacheRead =
|
||||
pick("cache_read_input_tokens") ??
|
||||
pick("cached_input_tokens") ??
|
||||
pick("cacheRead");
|
||||
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
||||
const total = pick("total_tokens") ?? pick("total");
|
||||
if (!input && !output && !cacheRead && !cacheWrite && !total)
|
||||
@@ -246,6 +249,47 @@ function parseCliJson(
|
||||
return { text: text.trim(), sessionId, usage };
|
||||
}
|
||||
|
||||
function parseCliJsonl(
|
||||
raw: string,
|
||||
backend: CliBackendConfig,
|
||||
): CliOutput | null {
|
||||
const lines = raw
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (lines.length === 0) return null;
|
||||
let sessionId: string | undefined;
|
||||
let usage: CliUsage | undefined;
|
||||
const texts: string[] = [];
|
||||
for (const line of lines) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(parsed)) continue;
|
||||
if (!sessionId) sessionId = pickSessionId(parsed, backend);
|
||||
if (!sessionId && typeof parsed.thread_id === "string") {
|
||||
sessionId = parsed.thread_id.trim();
|
||||
}
|
||||
if (isRecord(parsed.usage)) {
|
||||
usage = toUsage(parsed.usage) ?? usage;
|
||||
}
|
||||
const item = isRecord(parsed.item) ? parsed.item : null;
|
||||
if (item && typeof item.text === "string") {
|
||||
const type =
|
||||
typeof item.type === "string" ? item.type.toLowerCase() : "";
|
||||
if (!type || type.includes("message")) {
|
||||
texts.push(item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
const text = texts.join("\n").trim();
|
||||
if (!text) return null;
|
||||
return { text, sessionId, usage };
|
||||
}
|
||||
|
||||
function resolveSystemPromptUsage(params: {
|
||||
backend: CliBackendConfig;
|
||||
isNewSession: boolean;
|
||||
@@ -328,21 +372,33 @@ async function writeCliImages(
|
||||
|
||||
function buildCliArgs(params: {
|
||||
backend: CliBackendConfig;
|
||||
baseArgs: string[];
|
||||
modelId: string;
|
||||
sessionId?: string;
|
||||
systemPrompt?: string | null;
|
||||
imagePaths?: string[];
|
||||
promptArg?: string;
|
||||
useResume: boolean;
|
||||
}): string[] {
|
||||
const args: string[] = [...(params.backend.args ?? [])];
|
||||
if (params.backend.modelArg && params.modelId) {
|
||||
const args: string[] = [...params.baseArgs];
|
||||
if (!params.useResume && params.backend.modelArg && params.modelId) {
|
||||
args.push(params.backend.modelArg, params.modelId);
|
||||
}
|
||||
if (params.systemPrompt && params.backend.systemPromptArg) {
|
||||
if (
|
||||
!params.useResume &&
|
||||
params.systemPrompt &&
|
||||
params.backend.systemPromptArg
|
||||
) {
|
||||
args.push(params.backend.systemPromptArg, params.systemPrompt);
|
||||
}
|
||||
if (params.sessionId && params.backend.sessionArg) {
|
||||
args.push(params.backend.sessionArg, params.sessionId);
|
||||
if (!params.useResume && params.sessionId) {
|
||||
if (params.backend.sessionArgs && params.backend.sessionArgs.length > 0) {
|
||||
for (const entry of params.backend.sessionArgs) {
|
||||
args.push(entry.replaceAll("{sessionId}", params.sessionId));
|
||||
}
|
||||
} else if (params.backend.sessionArg) {
|
||||
args.push(params.backend.sessionArg, params.sessionId);
|
||||
}
|
||||
}
|
||||
if (params.imagePaths && params.imagePaths.length > 0) {
|
||||
const mode = params.backend.imageMode ?? "repeat";
|
||||
@@ -434,8 +490,19 @@ export async function runCliAgent(params: {
|
||||
backend,
|
||||
cliSessionId: params.cliSessionId,
|
||||
});
|
||||
const sessionIdSent =
|
||||
backend.sessionArg && cliSessionIdToSend ? cliSessionIdToSend : undefined;
|
||||
const useResume = Boolean(
|
||||
params.cliSessionId &&
|
||||
cliSessionIdToSend &&
|
||||
backend.resumeArgs &&
|
||||
backend.resumeArgs.length > 0,
|
||||
);
|
||||
const sessionIdSent = cliSessionIdToSend
|
||||
? useResume ||
|
||||
Boolean(backend.sessionArg) ||
|
||||
Boolean(backend.sessionArgs?.length)
|
||||
? cliSessionIdToSend
|
||||
: undefined
|
||||
: undefined;
|
||||
const systemPromptArg = resolveSystemPromptUsage({
|
||||
backend,
|
||||
isNewSession: isNew,
|
||||
@@ -459,13 +526,23 @@ export async function runCliAgent(params: {
|
||||
prompt,
|
||||
});
|
||||
const stdinPayload = stdin ?? "";
|
||||
const baseArgs = useResume
|
||||
? (backend.resumeArgs ?? backend.args ?? [])
|
||||
: (backend.args ?? []);
|
||||
const resolvedArgs = useResume
|
||||
? baseArgs.map((entry) =>
|
||||
entry.replaceAll("{sessionId}", cliSessionIdToSend ?? ""),
|
||||
)
|
||||
: baseArgs;
|
||||
const args = buildCliArgs({
|
||||
backend,
|
||||
baseArgs: resolvedArgs,
|
||||
modelId: normalizedModel,
|
||||
sessionId: cliSessionIdToSend,
|
||||
systemPrompt: systemPromptArg,
|
||||
imagePaths,
|
||||
promptArg: argsPrompt,
|
||||
useResume,
|
||||
});
|
||||
|
||||
const serialize = backend.serialize ?? true;
|
||||
@@ -556,9 +633,16 @@ export async function runCliAgent(params: {
|
||||
});
|
||||
}
|
||||
|
||||
if (backend.output === "text") {
|
||||
const outputMode =
|
||||
useResume ? backend.resumeOutput ?? backend.output : backend.output;
|
||||
|
||||
if (outputMode === "text") {
|
||||
return { text: stdout, sessionId: undefined };
|
||||
}
|
||||
if (outputMode === "jsonl") {
|
||||
const parsed = parseCliJsonl(stdout, backend);
|
||||
return parsed ?? { text: stdout };
|
||||
}
|
||||
|
||||
const parsed = parseCliJson(stdout, backend);
|
||||
return parsed ?? { text: stdout };
|
||||
@@ -572,7 +656,7 @@ export async function runCliAgent(params: {
|
||||
meta: {
|
||||
durationMs: Date.now() - started,
|
||||
agentMeta: {
|
||||
sessionId: output.sessionId ?? sessionIdSent ?? params.sessionId,
|
||||
sessionId: output.sessionId ?? sessionIdSent,
|
||||
provider: params.provider,
|
||||
model: modelId,
|
||||
usage: output.usage,
|
||||
|
||||
@@ -31,6 +31,7 @@ export function normalizeProviderId(provider: string): string {
|
||||
export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (normalized === "claude-cli") return true;
|
||||
if (normalized === "codex-cli") return true;
|
||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
return Object.keys(backends).some(
|
||||
(key) => normalizeProviderId(key) === normalized,
|
||||
|
||||
Reference in New Issue
Block a user