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 () => {
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import {
Parameters,
attachmentTable,
define,
describe as describeTools,
formatValue,
@@ -87,10 +88,13 @@ describe("code mode execute", () => {
expect(description).toContain("This is the COMPLETE list")
expect(description).toContain("- github (2 tools)")
expect(description).toContain("- linear (1 tool)")
// Tools are previewed inline as compact, directly-callable input signatures.
expect(description).toContain("tools.github.create_issue(input: { title: string; body?: string })")
expect(description).toContain("tools.linear.search(input: object)")
// ...but not the full Promise return — that comes from tools.$rune.describe.
// Tools are previewed inline as directly-callable signatures that now include the
// awaited return type (Result<T>) so the model sees the result shape up front.
expect(description).toContain("tools.github.create_issue(input: { title: string; body?: string }): Result<unknown>")
expect(description).toContain("tools.linear.search(input: object): Result<unknown>")
// The Result<T> envelope alias is defined once in the prose.
expect(description).toContain("type Result<T> = { result: T; attachments?: Attachment[] }")
// ...but the preview drops the uniform Promise<…> wrapper — that full form comes from describe.
expect(description).not.toContain("): Promise<")
})
@@ -143,7 +147,7 @@ describe("code mode execute", () => {
const desc = JSON.parse(described.output)
expect(desc.path).toBe("github.create_issue")
expect(desc.signature).toBe(
"tools.github.create_issue(input: { title: string; body?: string }): Promise<{ result: unknown; attachments?: Attachment[] }>",
"tools.github.create_issue(input: { title: string; body?: string }): Promise<Result<unknown>>",
)
expect(described.metadata.toolCalls).toEqual([
{ tool: "$rune.describe", status: "completed", input: { path: "github.create_issue" } },
@@ -357,29 +361,43 @@ describe("code mode execute", () => {
expect(snapshots).toContainEqual({ toolCalls: [{ tool: "bad.tool", status: "error", input: { reason: "test" } }] })
})
test("unit: toEnvelope wraps result and extracts media as attachments", () => {
expect(toEnvelope({ structuredContent: { x: 1 }, content: [] })).toEqual({ result: { x: 1 } })
expect(toEnvelope({ content: [{ type: "text", text: "hi" }] })).toEqual({ result: "hi" })
expect(toEnvelope("raw")).toEqual({ result: "raw" })
test("unit: toEnvelope wraps result and extracts media as opaque attachment handles", () => {
const table = attachmentTable()
expect(toEnvelope({ structuredContent: { x: 1 }, content: [] }, table.seal)).toEqual({ result: { x: 1 } })
expect(toEnvelope({ content: [{ type: "text", text: "hi" }] }, table.seal)).toEqual({ result: "hi" })
expect(toEnvelope("raw", table.seal)).toEqual({ result: "raw" })
// image/audio blocks become data-URL file attachments; text stays in result
expect(
toEnvelope({
// image/audio blocks become OPAQUE handles (mime/bytes, NO url/data); text stays in result
const withImage = toEnvelope(
{
content: [
{ type: "text", text: "see image" },
{ type: "image", data: "AAAA", mimeType: "image/png" },
],
}),
).toEqual({
result: "see image",
attachments: [{ type: "file", mime: "image/png", url: "data:image/png;base64,AAAA" }],
},
table.seal,
)
expect(withImage.result).toBe("see image")
expect(withImage.attachments).toEqual([{ type: "file", id: "att_1", mime: "image/png", bytes: 3 }])
// The handle exposes no bytes, but resolves back to the real attachment host-side.
expect((withImage.attachments![0] as any).url).toBeUndefined()
expect(table.resolve(withImage.attachments![0])).toEqual({
type: "file",
mime: "image/png",
url: "data:image/png;base64,AAAA",
})
// media-only result has an undefined result but still surfaces the attachment
expect(toEnvelope({ content: [{ type: "image", data: "BBBB", mimeType: "image/jpeg" }] })).toEqual({
result: undefined,
attachments: [{ type: "file", mime: "image/jpeg", url: "data:image/jpeg;base64,BBBB" }],
})
// media-only result: undefined result, still surfaces the handle
const mediaOnly = toEnvelope({ content: [{ type: "image", data: "BBBB", mimeType: "image/jpeg" }] }, table.seal)
expect(mediaOnly.result).toBeUndefined()
expect(mediaOnly.attachments).toEqual([{ type: "file", id: "att_2", mime: "image/jpeg", bytes: 3 }])
})
test("unit: attachmentTable resolve drops fabricated or stale handles", () => {
const table = attachmentTable()
expect(table.resolve({ type: "file", id: "att_999", mime: "image/png" })).toBeUndefined()
expect(table.resolve({ type: "file" })).toBeUndefined()
expect(table.resolve("nope")).toBeUndefined()
})
test("unit: formatValue", () => {
@@ -414,7 +432,7 @@ describe("code mode execute", () => {
const described = await Effect.runPromise(tool.execute({ code: "return await tools.$rune.describe('weather.current')" }, ctx))
const desc = JSON.parse(described.output)
expect(desc.signature).toBe(
"tools.weather.current(input: { city?: string }): Promise<{ result: { tempC: number; summary?: string }; attachments?: Attachment[] }>",
"tools.weather.current(input: { city?: string }): Promise<Result<{ tempC: number; summary?: string }>>",
)
// describe now returns the return shape as pretty TypeScript, not raw JSON Schema.
expect(desc.output).toBe("{\n tempC: number\n summary?: string\n}")
@@ -665,6 +683,37 @@ describe("renderType", () => {
)
})
test("emits JSDoc tags for schema constraints TypeScript can't express", () => {
const schema = {
type: "object",
properties: {
when: { type: "string", format: "date-time", default: "now", description: "start time" },
legacy: { type: "boolean", deprecated: true },
tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 5 },
},
required: ["when"],
} as any
expect(renderType(schema, { pretty: true })).toBe(
[
"{",
" /**",
" * start time",
' * @default "now"',
" * @format date-time",
" */",
" when: string",
" /** @deprecated */",
" legacy?: boolean",
" /**",
" * @minItems 1",
" * @maxItems 5",
" */",
" tags?: string[]",
"}",
].join("\n"),
)
})
test("neutralizes a comment terminator inside a JSDoc description", () => {
const schema = { type: "object", properties: { x: { type: "string", description: "danger */ oops" } } } as any
const out = renderType(schema, { pretty: true })