feat(aircoding): AirCoding V2 baseline — deterministic multi-agent architecture

Forked from OpenCode v1.17.4 with multi-agent system:
- 5 agents: aircoding, scheduler, worker, architect, reviewer
- Deterministic DAG scheduling engine (coordinator_tick)
- Tool whitelists as hard enforcement
- AirCoding validation plugin
- V1 requirements: C4 docs, ADR, AGENTS.md, debug-log.md
- Design documents in docs/
This commit is contained in:
airlongdian
2026-06-13 21:53:43 +08:00
commit deaf7eb806
5757 changed files with 1169969 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import { describe, expect, test } from "bun:test"
import { errorData, errorFormat, errorMessage } from "../../src/util/error"
describe("util.error", () => {
test("formats native Error instances", () => {
const err = new Error("boom")
expect(errorMessage(err)).toBe("boom")
expect(errorFormat(err)).toContain("boom")
const data = errorData(err)
expect(data.type).toBe("Error")
expect(data.message).toBe("boom")
expect(String(data.formatted)).toContain("boom")
})
test("extracts message from record-like values", () => {
const err = { message: "bad input", code: "E_BAD" }
expect(errorMessage(err)).toBe("bad input")
const data = errorData(err)
expect(data.message).toBe("bad input")
expect(data.code).toBe("E_BAD")
})
test("never returns bare {} for opaque object errors", () => {
expect(errorFormat({})).not.toBe("{}")
expect(errorFormat({})).toContain("no message")
class OpaqueError {}
const opaque = new OpaqueError()
Object.defineProperty(opaque, "secret", { value: "hidden", enumerable: false })
expect(errorFormat(opaque)).not.toBe("{}")
expect(errorFormat(opaque)).toContain("OpaqueError")
})
test("handles opaque throwables with custom toString", () => {
const err = {
toString() {
return "ResolveMessage: Cannot resolve module"
},
}
expect(errorMessage(err)).toBe("ResolveMessage: Cannot resolve module")
const data = errorData(err)
expect(data.message).toBe("ResolveMessage: Cannot resolve module")
expect(String(data.formatted)).toContain("ResolveMessage")
})
})

View File

@@ -0,0 +1,16 @@
import { describe, expect, test } from "bun:test"
import { filetype } from "../../src/util/filetype"
describe("util.filetype", () => {
test("maps filenames to presentation languages", () => {
expect(filetype("component.tsx")).toBe("typescript")
expect(filetype("script.js")).toBe("typescript")
expect(filetype("main.py")).toBe("python")
expect(filetype("README.unknown")).toBeUndefined()
})
test("uses none for missing filenames", () => {
expect(filetype()).toBe("none")
expect(filetype("")).toBe("none")
})
})

View File

@@ -0,0 +1,59 @@
import { describe, expect, test } from "bun:test"
import { formatDuration } from "../../src/util/format"
describe("util.format", () => {
describe("formatDuration", () => {
test("returns empty string for zero or negative values", () => {
expect(formatDuration(0)).toBe("")
expect(formatDuration(-1)).toBe("")
expect(formatDuration(-100)).toBe("")
})
test("formats seconds under a minute", () => {
expect(formatDuration(1)).toBe("1s")
expect(formatDuration(30)).toBe("30s")
expect(formatDuration(59)).toBe("59s")
})
test("formats minutes under an hour", () => {
expect(formatDuration(60)).toBe("1m")
expect(formatDuration(61)).toBe("1m 1s")
expect(formatDuration(90)).toBe("1m 30s")
expect(formatDuration(120)).toBe("2m")
expect(formatDuration(330)).toBe("5m 30s")
expect(formatDuration(3599)).toBe("59m 59s")
})
test("formats hours under a day", () => {
expect(formatDuration(3600)).toBe("1h")
expect(formatDuration(3660)).toBe("1h 1m")
expect(formatDuration(7200)).toBe("2h")
expect(formatDuration(8100)).toBe("2h 15m")
expect(formatDuration(86399)).toBe("23h 59m")
})
test("formats days under a week", () => {
expect(formatDuration(86400)).toBe("~1 day")
expect(formatDuration(172800)).toBe("~2 days")
expect(formatDuration(259200)).toBe("~3 days")
expect(formatDuration(604799)).toBe("~6 days")
})
test("formats weeks", () => {
expect(formatDuration(604800)).toBe("~1 week")
expect(formatDuration(1209600)).toBe("~2 weeks")
expect(formatDuration(1609200)).toBe("~2 weeks")
})
test("handles boundary values correctly", () => {
expect(formatDuration(59)).toBe("59s")
expect(formatDuration(60)).toBe("1m")
expect(formatDuration(3599)).toBe("59m 59s")
expect(formatDuration(3600)).toBe("1h")
expect(formatDuration(86399)).toBe("23h 59m")
expect(formatDuration(86400)).toBe("~1 day")
expect(formatDuration(604799)).toBe("~6 days")
expect(formatDuration(604800)).toBe("~1 week")
})
})
})

View File

@@ -0,0 +1,9 @@
import { describe, expect, test } from "bun:test"
import { parse } from "../../src/util/model"
describe("util.model", () => {
test("splits provider from a nested model identifier", () => {
expect(parse("provider/org/model")).toEqual({ providerID: "provider", modelID: "org/model" })
expect(parse("invalid")).toEqual({ providerID: "invalid", modelID: "" })
})
})

View File

@@ -0,0 +1,8 @@
import { expect, test } from "bun:test"
import { sessionEpilogue } from "../../src/util/presentation"
test("formats session continuation summary", () => {
const epilogue = sessionEpilogue({ title: "A session", sessionID: "ses_123" })
expect(epilogue).toContain("A session")
expect(epilogue).toContain("opencode -s ses_123")
})

View File

@@ -0,0 +1,30 @@
import { expect, test } from "bun:test"
import { destroyRenderer } from "../../src/util/renderer"
test("clears the terminal title before destroying the renderer", () => {
const calls: string[] = []
destroyRenderer({
isDestroyed: false,
setTerminalTitle(title) {
calls.push(`title:${title}`)
},
destroy() {
calls.push("destroy")
},
})
expect(calls).toEqual(["title:", "destroy"])
})
test("still clears the title after renderer destruction", () => {
const calls: string[] = []
destroyRenderer({
isDestroyed: true,
setTerminalTitle(title) {
calls.push(`title:${title}`)
},
destroy() {
calls.push("destroy")
},
})
expect(calls).toEqual(["title:"])
})

View File

@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test"
import { getRevertDiffFiles } from "../../src/util/revert-diff"
describe("revert diff", () => {
test("prefers the actual file path over /dev/null for added and deleted files", () => {
const files = getRevertDiffFiles(`diff --git a/new.txt b/new.txt
new file mode 100644
index 0000000..3b18e51
--- /dev/null
+++ b/new.txt
@@ -0,0 +1 @@
+new content
diff --git a/old.txt b/old.txt
deleted file mode 100644
index 3b18e51..0000000
--- a/old.txt
+++ /dev/null
@@ -1 +0,0 @@
-old content
`)
expect(files).toEqual([
{
filename: "new.txt",
additions: 1,
deletions: 0,
},
{
filename: "old.txt",
additions: 0,
deletions: 1,
},
])
})
})

View File

@@ -0,0 +1,10 @@
import { describe, expect, test } from "bun:test"
import { isDefaultTitle } from "../../src/util/session"
describe("util.session", () => {
test("recognizes generated parent and child titles", () => {
expect(isDefaultTitle("New session - 2026-06-06T12:34:56.789Z")).toBeTrue()
expect(isDefaultTitle("Child session - 2026-06-06T12:34:56.789Z")).toBeTrue()
expect(isDefaultTitle("New session - custom")).toBeFalse()
})
})

View File

@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test"
import { toolDisplayMetadata, webSearchProviderLabel } from "../../src/util/tool-display"
describe("webSearchProviderLabel", () => {
test("labels known providers", () => {
expect(webSearchProviderLabel("parallel")).toBe("Parallel Web Search")
expect(webSearchProviderLabel("exa")).toBe("Exa Web Search")
})
for (const [name, provider] of [
["undefined", undefined],
["null", null],
["an object", {}],
["an array", []],
["a number", 1],
["an unexpected string", "other"],
] as const) {
test(`uses the generic label for ${name}`, () => {
expect(webSearchProviderLabel(provider)).toBe("Web Search")
})
}
})
describe("toolDisplayMetadata", () => {
test("returns structured metadata for non-pending states", () => {
const structured = { provider: "parallel", numResults: 3 }
expect(toolDisplayMetadata({ status: "running", structured })).toBe(structured)
expect(toolDisplayMetadata({ status: "completed", structured })).toBe(structured)
expect(toolDisplayMetadata({ status: "error", structured })).toBe(structured)
})
test("does not expose pending or malformed metadata", () => {
expect(toolDisplayMetadata({ status: "pending", structured: { provider: "exa" } })).toEqual({})
expect(toolDisplayMetadata({ status: "completed" })).toEqual({})
expect(toolDisplayMetadata({ status: "completed", structured: null })).toEqual({})
expect(toolDisplayMetadata({ status: "completed", structured: [] })).toEqual({})
expect(toolDisplayMetadata(undefined)).toEqual({})
})
})

View File

@@ -0,0 +1,421 @@
import { describe, expect, test } from "bun:test"
import { formatAssistantHeader, formatMessage, formatPart, formatTranscript } from "../../src/util/transcript"
import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
const providers: Provider[] = [
{
id: "anthropic",
name: "Anthropic",
source: "api",
env: [],
options: {},
models: {
"claude-sonnet-4-20250514": {
id: "claude-sonnet-4-20250514",
providerID: "anthropic",
api: {
id: "claude-sonnet-4-20250514",
url: "https://example.com/claude-sonnet-4-20250514",
npm: "@ai-sdk/anthropic",
},
name: "Claude Sonnet 4",
capabilities: {
temperature: true,
reasoning: true,
attachment: true,
toolcall: true,
input: {
text: true,
audio: false,
image: true,
video: false,
pdf: true,
},
output: {
text: true,
audio: false,
image: false,
video: false,
pdf: false,
},
interleaved: false,
},
cost: {
input: 0,
output: 0,
cache: {
read: 0,
write: 0,
},
},
limit: {
context: 200_000,
output: 8_192,
},
status: "active",
options: {},
headers: {},
release_date: "2025-05-14",
},
},
},
]
describe("transcript", () => {
describe("formatAssistantHeader", () => {
const baseMsg: AssistantMessage = {
id: "msg_123",
sessionID: "ses_123",
role: "assistant",
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
mode: "",
parentID: "msg_parent",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1000000, completed: 1005400 },
}
test("includes metadata when enabled", () => {
const result = formatAssistantHeader(baseMsg, true)
expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)\n\n")
})
test("uses model display name when available", () => {
const result = formatAssistantHeader(baseMsg, true, providers)
expect(result).toBe("## Assistant (Build · Claude Sonnet 4 · 5.4s)\n\n")
})
test("excludes metadata when disabled", () => {
const result = formatAssistantHeader(baseMsg, false)
expect(result).toBe("## Assistant\n\n")
})
test("handles missing completed time", () => {
const msg = { ...baseMsg, time: { created: 1000000 } }
const result = formatAssistantHeader(msg as AssistantMessage, true)
expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514)\n\n")
})
test("titlecases agent name", () => {
const msg = { ...baseMsg, agent: "plan" }
const result = formatAssistantHeader(msg, true)
expect(result).toContain("Plan")
})
})
describe("formatPart", () => {
const options = { thinking: true, toolDetails: true, assistantMetadata: true }
test("formats text part", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "text",
text: "Hello world",
}
const result = formatPart(part, options)
expect(result).toBe("Hello world\n\n")
})
test("skips synthetic text parts", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "text",
text: "Synthetic content",
synthetic: true,
}
const result = formatPart(part, options)
expect(result).toBe("")
})
test("formats reasoning when thinking enabled", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "reasoning",
text: "Let me think...",
time: { start: 1000 },
}
const result = formatPart(part, options)
expect(result).toBe("_Thinking:_\n\nLet me think...\n\n")
})
test("skips reasoning when thinking disabled", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "reasoning",
text: "Let me think...",
time: { start: 1000 },
}
const result = formatPart(part, { ...options, thinking: false })
expect(result).toBe("")
})
test("formats tool part with details", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "tool",
callID: "call_1",
tool: "bash",
state: {
status: "completed",
input: { command: "ls" },
output: "file1.txt\nfile2.txt",
title: "List files",
metadata: {},
time: { start: 1000, end: 1100 },
},
}
const result = formatPart(part, options)
expect(result).toContain("**Tool: bash**")
expect(result).toContain("**Input:**")
expect(result).toContain('"command": "ls"')
expect(result).toContain("**Output:**")
expect(result).toContain("file1.txt")
})
test("formats tool output containing triple backticks without breaking markdown", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "tool",
callID: "call_1",
tool: "bash",
state: {
status: "completed",
input: { command: "echo '```hello```'" },
output: "```hello```",
title: "Echo backticks",
metadata: {},
time: { start: 1000, end: 1100 },
},
}
const result = formatPart(part, options)
// The tool header should not be inside a code block
expect(result).toStartWith("**Tool: bash**\n")
// Input and output should each be in their own code blocks
expect(result).toContain("**Input:**\n```json")
expect(result).toContain("**Output:**\n```\n```hello```\n```")
})
test("formats tool part without details when disabled", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "tool",
callID: "call_1",
tool: "bash",
state: {
status: "completed",
input: { command: "ls" },
output: "file1.txt",
title: "List files",
metadata: {},
time: { start: 1000, end: 1100 },
},
}
const result = formatPart(part, { ...options, toolDetails: false })
expect(result).toContain("**Tool: bash**")
expect(result).not.toContain("**Input:**")
expect(result).not.toContain("**Output:**")
})
test("formats tool error", () => {
const part: Part = {
id: "part_1",
sessionID: "ses_123",
messageID: "msg_123",
type: "tool",
callID: "call_1",
tool: "bash",
state: {
status: "error",
input: { command: "invalid" },
error: "Command failed",
time: { start: 1000, end: 1100 },
},
}
const result = formatPart(part, options)
expect(result).toContain("**Error:**")
expect(result).toContain("Command failed")
})
})
describe("formatMessage", () => {
const options = { thinking: true, toolDetails: true, assistantMetadata: true, providers }
test("formats user message", () => {
const msg: UserMessage = {
id: "msg_123",
sessionID: "ses_123",
role: "user",
agent: "build",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
time: { created: 1000000 },
}
const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hello" }]
const result = formatMessage(msg, parts, options)
expect(result).toContain("## User")
expect(result).toContain("Hello")
})
test("formats assistant message with metadata", () => {
const msg: AssistantMessage = {
id: "msg_123",
sessionID: "ses_123",
role: "assistant",
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
mode: "",
parentID: "msg_parent",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1000000, completed: 1005400 },
}
const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
const result = formatMessage(msg, parts, options)
expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 5.4s)")
expect(result).toContain("Hi there")
})
})
describe("formatTranscript", () => {
test("formats complete transcript", () => {
const session = {
id: "ses_abc123",
title: "Test Session",
time: { created: 1000000000000, updated: 1000000001000 },
}
const messages = [
{
info: {
id: "msg_1",
sessionID: "ses_abc123",
role: "user" as const,
agent: "build",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
time: { created: 1000000000000 },
},
parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Hello" }],
},
{
info: {
id: "msg_2",
sessionID: "ses_abc123",
role: "assistant" as const,
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
mode: "",
parentID: "msg_1",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1000000000100, completed: 1000000000600 },
},
parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
},
]
const options = {
thinking: false,
toolDetails: false,
assistantMetadata: true,
providers,
}
const result = formatTranscript(session, messages, options)
expect(result).toContain("# Test Session")
expect(result).toContain("**Session ID:** ses_abc123")
expect(result).toContain("## User")
expect(result).toContain("Hello")
expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 0.5s)")
expect(result).toContain("Hi!")
expect(result).toContain("---")
})
test("falls back to raw model id when provider data is missing", () => {
const session = {
id: "ses_abc123",
title: "Test Session",
time: { created: 1000000000000, updated: 1000000001000 },
}
const messages = [
{
info: {
id: "msg_1",
sessionID: "ses_abc123",
role: "assistant" as const,
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
mode: "",
parentID: "msg_0",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1000000000100, completed: 1000000000600 },
},
parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
},
]
const result = formatTranscript(session, messages, {
thinking: false,
toolDetails: false,
assistantMetadata: true,
})
expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
})
test("formats transcript without assistant metadata", () => {
const session = {
id: "ses_abc123",
title: "Test Session",
time: { created: 1000000000000, updated: 1000000001000 },
}
const messages = [
{
info: {
id: "msg_1",
sessionID: "ses_abc123",
role: "assistant" as const,
agent: "build",
modelID: "claude-sonnet-4-20250514",
providerID: "anthropic",
mode: "",
parentID: "msg_0",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1000000000100, completed: 1000000000600 },
},
parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
},
]
const options = { thinking: false, toolDetails: false, assistantMetadata: false }
const result = formatTranscript(session, messages, options)
expect(result).toContain("## Assistant\n\n")
expect(result).not.toContain("Build")
expect(result).not.toContain("claude-sonnet-4-20250514")
})
})
})