feat(opencode): JSDoc tags, Result<T> return hints, and opaque attachments

- describe/preview now surface each tool's return type as Result<T> (alias
  defined once in the prompt); the inline preview shows it too, so the model
  sees a tool's result shape without a describe round-trip. T is the declared
  outputSchema else unknown, with prose telling the model to inspect an unknown
  result before assuming fields.
- renderType pretty mode emits JSDoc tags a TS type can't express: @default,
  @format, @deprecated, @minItems/@maxItems (multi-line descriptions preserved).
- Attachments are now opaque handles: a tool's media becomes
  { type:'file', id, mime, filename?, bytes } with no inline bytes. Real bytes
  stay host-side in a per-execution attachmentTable; the program propagates or
  drops a handle (return it to surface the image to the model+user) but cannot
  read or leak the base64. Documents the divergence from prior art in rune.md.
- Records the throw/catch (not errors-as-values) decision in rune.md.
This commit is contained in:
Aiden Cline
2026-07-01 10:54:42 -05:00
parent 5085a13b0e
commit c16bba8bc0
4 changed files with 274 additions and 88 deletions
@@ -118,7 +118,7 @@ describe("code mode integration (real MCP server)", () => {
const desc = JSON.parse(out.output)
expect(desc.path).toBe("fixtures.add")
expect(desc.signature).toBe(
"tools.fixtures.add(input: { a: number; b: number }): Promise<{ result: { sum: number }; attachments?: Attachment[] }>",
"tools.fixtures.add(input: { a: number; b: number }): Promise<Result<{ sum: number }>>",
)
// describe returns TypeScript for the input/output types, not raw JSON Schema.
expect(desc.input).toBe("{\n a: number\n b: number\n}")
@@ -129,7 +129,7 @@ describe("code mode integration (real MCP server)", () => {
test("describe falls back to result: unknown when no outputSchema is declared", async () => {
const out = await run("return await tools.$rune.describe('fixtures.get_text')")
const desc = JSON.parse(out.output)
expect(desc.signature).toContain("Promise<{ result: unknown; attachments?: Attachment[] }>")
expect(desc.signature).toContain("Promise<Result<unknown>>")
})
test("search finds a tool by keyword", async () => {
@@ -169,20 +169,24 @@ describe("code mode integration (real MCP server)", () => {
expect(out.attachments).toEqual([{ type: "file", mime: "image/png", url: `data:image/png;base64,${PNG}` }])
})
test("an attachment's bytes are readable and routable in code, not opaque", async () => {
// The data: URL carrying the base64 payload is an ordinary string in the
// sandbox: the program can inspect it (and thus route it into another tool).
test("an attachment is an opaque handle: metadata only, no readable bytes", async () => {
// The program sees mime/bytes but NOT the data — a stray return can't leak base64.
const out = await run(`
const shot = await tools.fixtures.screenshot({})
const url = shot.attachments[0].url
return { result: { mime: shot.attachments[0].mime, isDataUrl: url.startsWith('data:'), bytes: url.length } }
const a = shot.attachments[0]
return { result: { mime: a.mime, hasUrl: 'url' in a, hasData: 'data' in a, bytes: a.bytes, keys: Object.keys(a).sort() } }
`)
expect(JSON.parse(out.output)).toEqual({
mime: "image/png",
isDataUrl: true,
bytes: `data:image/png;base64,${PNG}`.length,
hasUrl: false,
hasData: false,
bytes: Buffer.from(PNG, "base64").byteLength,
keys: ["bytes", "id", "mime", "type"],
})
// Returning the handle inside `.result` (not as an attachment) surfaces no media
// and — crucially — carries no base64, so nothing large re-enters the conversation.
expect(out.attachments).toBeUndefined()
expect(out.output).not.toContain(PNG)
})
test("drops media when only .result is returned", async () => {