426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
312 lines
9.0 KiB
TypeScript
312 lines
9.0 KiB
TypeScript
/**
|
|
* CLI auth mode: `generateViaCli` / `streamViaCli` shell out to `claude -p` via
|
|
* `Bun.spawn` when `ANTHROPIC_AUTH_MODE=claude-cli`, parsing the CLI's JSON
|
|
* result into text plus token usage and emitting a usage event. Bun-only (fails
|
|
* on Node runtimes); does not support `messages`, `tools`, `toolChoice`, or
|
|
* `responseSchema`.
|
|
*/
|
|
import type { IAgentRuntime, ModelTypeName, TextStreamResult } from "@elizaos/core";
|
|
import { buildCanonicalSystemPrompt, logger } from "@elizaos/core";
|
|
import { emitModelUsageEvent } from "./events";
|
|
|
|
interface ClaudeCliModelUsage {
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
}
|
|
|
|
interface ClaudeCliResult {
|
|
result: string;
|
|
duration_ms: number;
|
|
duration_api_ms: number;
|
|
modelUsage: Record<string, ClaudeCliModelUsage>;
|
|
}
|
|
|
|
interface CliGenerateResult {
|
|
text: string;
|
|
usage: {
|
|
promptTokens: number;
|
|
completionTokens: number;
|
|
totalTokens: number;
|
|
} | null;
|
|
}
|
|
|
|
type ClaudeStreamEvent =
|
|
| {
|
|
type: "stream_event";
|
|
event?: {
|
|
delta?: {
|
|
type?: string;
|
|
text?: string;
|
|
};
|
|
};
|
|
}
|
|
| {
|
|
type: "result";
|
|
modelUsage?: Record<string, ClaudeCliModelUsage>;
|
|
stop_reason?: string;
|
|
};
|
|
|
|
function isClaudeStreamEvent(value: unknown): value is ClaudeStreamEvent {
|
|
if (typeof value !== "object" || value === null) return false;
|
|
const type = (value as { type?: unknown }).type;
|
|
return type === "stream_event" || type === "result";
|
|
}
|
|
|
|
function buildCliArgs(
|
|
prompt: string,
|
|
modelName: string,
|
|
systemPrompt: string | undefined,
|
|
maxTokens: number | undefined,
|
|
streaming: boolean
|
|
): string[] {
|
|
const args = [
|
|
"claude",
|
|
"-p",
|
|
prompt,
|
|
"--model",
|
|
modelName,
|
|
"--output-format",
|
|
streaming ? "stream-json" : "json",
|
|
];
|
|
if (streaming) args.push("--verbose", "--include-partial-messages");
|
|
if (maxTokens != null) args.push("--max-tokens", String(maxTokens));
|
|
if (systemPrompt) args.push("--system-prompt", systemPrompt);
|
|
return args;
|
|
}
|
|
|
|
function parseUsage(
|
|
modelUsage: Record<string, ClaudeCliModelUsage> | undefined
|
|
): CliGenerateResult["usage"] {
|
|
const entry = modelUsage ? Object.values(modelUsage)[0] : undefined;
|
|
if (!entry) return null;
|
|
return {
|
|
promptTokens: entry.inputTokens,
|
|
completionTokens: entry.outputTokens,
|
|
totalTokens: entry.inputTokens + entry.outputTokens,
|
|
};
|
|
}
|
|
|
|
function getBunRuntime() {
|
|
const bunRuntime = (
|
|
globalThis as typeof globalThis & {
|
|
Bun?: {
|
|
spawn(
|
|
args: string[],
|
|
options: { stdout: "pipe"; stderr: "pipe" }
|
|
): {
|
|
stdout: ReadableStream<Uint8Array>;
|
|
stderr: ReadableStream<Uint8Array>;
|
|
exited: Promise<number>;
|
|
};
|
|
};
|
|
}
|
|
).Bun;
|
|
|
|
if (!bunRuntime) {
|
|
throw new Error("[Anthropic CLI] Bun runtime is required for CLI mode");
|
|
}
|
|
|
|
return bunRuntime;
|
|
}
|
|
|
|
/**
|
|
* Run a prompt through `claude -p` (non-streaming).
|
|
*/
|
|
export async function generateViaCli(
|
|
runtime: IAgentRuntime,
|
|
prompt: string,
|
|
modelName: string,
|
|
modelType: ModelTypeName,
|
|
maxTokens?: number,
|
|
systemPrompt?: string
|
|
): Promise<CliGenerateResult> {
|
|
const args = buildCliArgs(
|
|
prompt,
|
|
modelName,
|
|
systemPrompt ?? buildCanonicalSystemPrompt({ character: runtime.character }),
|
|
maxTokens,
|
|
false
|
|
);
|
|
logger.debug(`[Anthropic CLI] ${modelType} → ${modelName}`);
|
|
|
|
const proc = getBunRuntime().spawn(args, { stdout: "pipe", stderr: "pipe" });
|
|
const [output, stderr] = await Promise.all([
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
]);
|
|
const exitCode = await proc.exited;
|
|
|
|
if (exitCode !== 0) {
|
|
throw new Error(`[Anthropic CLI] claude -p failed (exit ${exitCode}): ${stderr.slice(0, 500)}`);
|
|
}
|
|
|
|
let data: ClaudeCliResult;
|
|
try {
|
|
data = JSON.parse(output) as ClaudeCliResult;
|
|
} catch (error) {
|
|
// error-policy:J2 context-adding rethrow — surface the raw CLI output that
|
|
// failed to parse, with the parse error as cause.
|
|
throw new Error(`[Anthropic CLI] Failed to parse JSON. Raw: ${output.slice(0, 500)}`, {
|
|
cause: error,
|
|
});
|
|
}
|
|
|
|
logger.debug(
|
|
`[Anthropic CLI] ${modelType} done in ${data.duration_ms}ms (API: ${data.duration_api_ms}ms)`
|
|
);
|
|
|
|
const usage = parseUsage(data.modelUsage);
|
|
if (usage) {
|
|
emitModelUsageEvent(
|
|
runtime,
|
|
modelType,
|
|
prompt,
|
|
{
|
|
promptTokens: usage.promptTokens,
|
|
completionTokens: usage.completionTokens,
|
|
totalTokens: usage.totalTokens,
|
|
},
|
|
modelName
|
|
);
|
|
}
|
|
|
|
return { text: data.result, usage };
|
|
}
|
|
|
|
/**
|
|
* Run a prompt through `claude -p` with real-time streaming.
|
|
* Spawns with --output-format stream-json --verbose --include-partial-messages
|
|
* and yields text_delta events as they arrive from the CLI.
|
|
*/
|
|
export function streamViaCli(
|
|
runtime: IAgentRuntime,
|
|
prompt: string,
|
|
modelName: string,
|
|
modelType: ModelTypeName,
|
|
maxTokens?: number,
|
|
systemPrompt?: string
|
|
): TextStreamResult {
|
|
const args = buildCliArgs(
|
|
prompt,
|
|
modelName,
|
|
systemPrompt ?? buildCanonicalSystemPrompt({ character: runtime.character }),
|
|
maxTokens,
|
|
true
|
|
);
|
|
logger.debug(`[Anthropic CLI] streaming ${modelType} → ${modelName}`);
|
|
|
|
const proc = getBunRuntime().spawn(args, { stdout: "pipe", stderr: "pipe" });
|
|
|
|
let fullText = "";
|
|
let usageResolved = false;
|
|
let finishResolved = false;
|
|
let resolveText!: (v: string) => void;
|
|
let resolveUsage!: (
|
|
v: { promptTokens: number; completionTokens: number; totalTokens: number } | undefined
|
|
) => void;
|
|
let resolveFinish!: (v: string | undefined) => void;
|
|
|
|
const textPromise = new Promise<string>((r) => {
|
|
resolveText = r;
|
|
});
|
|
const usagePromise = new Promise<
|
|
{ promptTokens: number; completionTokens: number; totalTokens: number } | undefined
|
|
>((r) => {
|
|
resolveUsage = r;
|
|
});
|
|
const finishPromise = new Promise<string | undefined>((r) => {
|
|
resolveFinish = r;
|
|
});
|
|
|
|
async function* createTextStream(): AsyncGenerator<string> {
|
|
const reader = proc.stdout.getReader();
|
|
const decoder = new TextDecoder();
|
|
let lineBuf = "";
|
|
let streamFailed = false;
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
lineBuf += decoder.decode(value, { stream: true });
|
|
const lines = lineBuf.split("\n");
|
|
lineBuf = lines.pop() ?? "";
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(line);
|
|
} catch {
|
|
// error-policy:J3 untrusted-input sanitizing — `--verbose` CLI
|
|
// output interleaves non-JSON lines with the stream-json protocol;
|
|
// skipping a non-parsing line is the expected filter, and a wholly
|
|
// broken stream still surfaces via the CLI's non-zero exit.
|
|
continue;
|
|
}
|
|
if (!isClaudeStreamEvent(parsed)) continue;
|
|
const event: ClaudeStreamEvent = parsed;
|
|
|
|
if (event.type === "stream_event" && event.event?.delta?.type === "text_delta") {
|
|
const chunk = event.event.delta.text;
|
|
if (typeof chunk === "string") {
|
|
fullText += chunk;
|
|
yield chunk;
|
|
}
|
|
}
|
|
|
|
if (event.type === "result") {
|
|
const usage = parseUsage(event.modelUsage);
|
|
if (usage) {
|
|
emitModelUsageEvent(
|
|
runtime,
|
|
modelType,
|
|
prompt,
|
|
{
|
|
promptTokens: usage.promptTokens,
|
|
completionTokens: usage.completionTokens,
|
|
totalTokens: usage.totalTokens,
|
|
},
|
|
modelName
|
|
);
|
|
resolveUsage(usage);
|
|
} else {
|
|
resolveUsage(undefined);
|
|
}
|
|
usageResolved = true;
|
|
resolveFinish(event.stop_reason ?? "end_turn");
|
|
finishResolved = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The CLI signals failure via its exit code. A stream that ended after a
|
|
// non-zero exit is a provider failure, not an empty completion — throw so
|
|
// the consumer sees the real error instead of a fabricated "end_turn"
|
|
// with zero chunks (#9324: throw, never fabricate).
|
|
const exitCode = await proc.exited;
|
|
if (exitCode !== 0) {
|
|
streamFailed = true;
|
|
// error-policy:J6 best-effort diagnostics on an already-failed process —
|
|
// the typed failure below throws regardless of stderr readability.
|
|
const stderrText = await new Response(proc.stderr).text().catch(() => "");
|
|
throw new Error(
|
|
`[Anthropic CLI] claude -p stream failed (exit ${exitCode}): ${stderrText.slice(0, 500)}`
|
|
);
|
|
}
|
|
} finally {
|
|
resolveText(fullText);
|
|
if (!usageResolved) resolveUsage(undefined);
|
|
if (!finishResolved) resolveFinish(streamFailed ? "error" : "end_turn");
|
|
}
|
|
}
|
|
|
|
return {
|
|
textStream: createTextStream(),
|
|
text: textPromise,
|
|
usage: usagePromise,
|
|
finishReason: finishPromise,
|
|
} as TextStreamResult;
|
|
}
|