diff --git a/src/commands/agent-exec.test.ts b/src/commands/agent-exec.test.ts index 946141eddb1b..65c4bb7cff02 100644 --- a/src/commands/agent-exec.test.ts +++ b/src/commands/agent-exec.test.ts @@ -1,7 +1,9 @@ +import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Readable } from "node:stream"; +import { promisify } from "node:util"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ensureAuthProfileStore, @@ -13,6 +15,7 @@ import type { RuntimeEnv } from "../runtime.js"; import { agentExecCommand, classifyAgentExecResult, resolveAgentExecPrompt } from "./agent-exec.js"; const tempRoots: string[] = []; +const execFileAsync = promisify(execFile); async function makeTempRoot(prefix: string): Promise { const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); @@ -167,6 +170,44 @@ describe("agent exec strict result classification", () => { }); describe("agent exec command composition", () => { + it("writes plain final text to stdout when diagnostics are routed to stderr", async () => { + const source = ` + import { agentExecCommand } from "./src/commands/agent-exec.ts"; + import { enableConsoleCapture, routeLogsToStderr } from "./src/logging.ts"; + import { defaultRuntime } from "./src/runtime.ts"; + + routeLogsToStderr(); + enableConsoleCapture(); + const result = await agentExecCommand("inspect", {}, defaultRuntime, { + runAgent: async () => ({ + payloads: [{ text: "india" }], + meta: { + durationMs: 1, + agentMeta: { + sessionId: "session-result", + provider: "openai", + model: "gpt-5.6-sol", + }, + }, + }), + }); + process.exitCode = result.exitCode; + `; + + const { stdout, stderr } = await execFileAsync( + process.execPath, + ["--import", "tsx", "--input-type=module", "--eval", source], + { + cwd: path.resolve(import.meta.dirname, "../.."), + encoding: "utf8", + env: { ...process.env, OPENCLAW_TEST_RUNTIME_LOG: "1" }, + }, + ); + + expect(stdout).toBe("india\n"); + expect(stderr).not.toContain("india"); + }); + it("treats invalid timeout syntax as an ordinary usage error", async () => { const { runtime } = createRuntime(); diff --git a/src/commands/agent-exec.ts b/src/commands/agent-exec.ts index 1e32140d3abd..ec015282c801 100644 --- a/src/commands/agent-exec.ts +++ b/src/commands/agent-exec.ts @@ -9,7 +9,7 @@ import type { EmbeddedAgentRunMeta } from "../agents/embedded-agent.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; -import { writeRuntimeJson, type RuntimeEnv } from "../runtime.js"; +import { writeRuntimeJson, writeRuntimeStdout, type RuntimeEnv } from "../runtime.js"; const AGENT_EXEC_MESSAGE_MAX_BYTES = 4 * 1024 * 1024; const AGENT_EXEC_DEFAULT_TIMEOUT_SECONDS = 600; @@ -365,7 +365,7 @@ function writeAgentExecOutput( if (json) { writeRuntimeJson(runtime, envelope); } else if (envelope.final) { - runtime.log(envelope.final); + writeRuntimeStdout(runtime, envelope.final); } if (!envelope.ok && envelope.error) { runtime.error(envelope.error.message); diff --git a/src/runtime.test.ts b/src/runtime.test.ts index 01d3f15fddfa..c5234160e4ef 100644 --- a/src/runtime.test.ts +++ b/src/runtime.test.ts @@ -10,7 +10,12 @@ vi.mock("../packages/terminal-core/src/restore.js", () => ({ restoreTerminalState: vi.fn(), })); -import { createNonExitingRuntime, ExitError, writeRuntimeJson } from "./runtime.js"; +import { + createNonExitingRuntime, + ExitError, + writeRuntimeJson, + writeRuntimeStdout, +} from "./runtime.js"; describe("createNonExitingRuntime", () => { it("returns runtime with exit function", () => { @@ -85,3 +90,32 @@ describe("writeRuntimeJson", () => { expect(runtime.log).toHaveBeenCalledWith('{"key":"value"}'); }); }); + +describe("writeRuntimeStdout", () => { + it("uses the direct stdout writer when available", () => { + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + writeStdout: vi.fn(), + writeJson: vi.fn(), + }; + + writeRuntimeStdout(runtime, "plain output"); + + expect(runtime.writeStdout).toHaveBeenCalledWith("plain output"); + expect(runtime.log).not.toHaveBeenCalled(); + }); + + it("falls back to the runtime logger when no stdout writer is available", () => { + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + writeRuntimeStdout(runtime, "plain output"); + + expect(runtime.log).toHaveBeenCalledWith("plain output"); + }); +}); diff --git a/src/runtime.ts b/src/runtime.ts index 2fe5ef61abd9..814be842b550 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -139,3 +139,11 @@ export function writeRuntimeJson( } runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined)); } + +export function writeRuntimeStdout(runtime: RuntimeEnv | OutputRuntimeEnv, value: string): void { + if (hasRuntimeOutputWriter(runtime)) { + runtime.writeStdout(value); + return; + } + runtime.log(value); +}