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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user