feat(opencode): capture console output and surface it to the model

- console.log/warn/error/info/debug are now a real Rune interpreter builtin:
  a seeded global that formats its args (strings verbatim, objects/arrays as
  JSON, space-joined) and appends a line to a per-run LogCollector. It is not a
  tool call (spends no tool-call budget), returns undefined, and any other
  member throws. Formatting is charged to maxOperations and total captured
  output is bounded by maxAuditBytes so a logging loop can't exhaust memory.
- The collector is shared by reference with parallel interpreter forks (like the
  operation budget) and lives in execute()'s outer scope, so logs are surfaced
  on every ExecuteResult path — success, thrown error, and timeout.
- ExecuteResult (and its schema) gain a logs field; code mode appends captured
  logs to the model-facing output as a trailing '[level] message' section on
  both the success and error paths (withLogs). Logs go to the model only.
- Documents console in rune.md; drops the now-satisfied 'not done yet' entry.
This commit is contained in:
Aiden Cline
2026-07-01 11:36:37 -05:00
parent 2d9015c30d
commit 064c34b25a
5 changed files with 187 additions and 20 deletions
@@ -9,6 +9,7 @@ import {
rankTools,
renderType,
toEnvelope,
withLogs,
type SearchEntry,
} from "@/session/code-mode"
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
@@ -406,6 +407,17 @@ describe("code mode execute", () => {
expect(formatValue(undefined)).toBe("undefined")
})
test("unit: withLogs", () => {
// No logs: output is returned untouched.
expect(withLogs("result", [])).toBe("result")
// Logs are appended as a trailing section, one `[level] message` line each.
expect(withLogs("result", [{ level: "log", message: "a" }, { level: "warn", message: "b" }])).toBe(
"result\n\nLogs:\n[log] a\n[warn] b",
)
// Empty output still gets the section (no leading blank lines).
expect(withLogs("", [{ level: "error", message: "boom" }])).toBe("Logs:\n[error] boom")
})
test("terminates a runaway loop via the operation limit instead of hanging", async () => {
const tool = await build({})
const output = await Effect.runPromise(tool.execute({ code: "while (true) {}" }, ctx))