History

Use emit for code mode outputs

Rhys Sullivan committed 4 months ago

Commit 5d4f1121 parent 5cd7a7715 files · +817 −222Browse files

Showing the changes to packages/kernel/runtime-quickjs/src (2 of 15 files). Show all

Unified · Split

2 files
  1. packages/kernel/runtime-quickjs/src/index.test.ts +81 −0
  2. packages/kernel/runtime-quickjs/src/index.ts +39 −0
packages/kernel/runtime-quickjs/src/index.test.ts+81−0
@@ -35,6 +35,87 @@ describe("quickjs executor", () => { · 34 unmodified lines
3535 }),
3636 );
3737
38+ it.effect("accumulates helper output separately from returned data", () =>
39+ Effect.gen(function* () {
40+ const result = yield* executor.execute(
41+ `
42+ const attachment = {
43+ _tag: "ToolFile",
44+ name: "photo.png",
45+ mimeType: "image/png",
46+ encoding: "base64",
47+ data: "iVBORw0KGgo=",
48+ byteLength: 8,
49+ };
50+ const values = [
51+ emit("hello"),
52+ emit(attachment),
53+ emit({ type: "text", text: "mcp hello" }),
54+ emit({ type: "image", data: "Zm9v", mimeType: "image/png" }),
55+ emit({ type: "audio", data: "SUQz", mimeType: "audio/mpeg" }),
56+ emit({
57+ type: "resource",
58+ resource: { uri: "executor-file:///report.pdf", mimeType: "application/pdf", blob: "JVBERg==" },
59+ }),
60+ emit({
61+ type: "resource_link",
62+ uri: "executor-file:///remote.pdf",
63+ name: "remote.pdf",
64+ mimeType: "application/pdf",
65+ }),
66+ emit({ arbitrary: true }),
67+ ];
68+ return { values: values.map((value) => value === undefined), keptReturn: true };
69+ `,
70+ makeTestInvoker({}),
71+ );
72+
73+ expect(result.error).toBeUndefined();
74+ expect(result.result).toEqual({
75+ values: [true, true, true, true, true, true, true, true],
76+ keptReturn: true,
77+ });
78+ expect(result.output).toEqual([
79+ { type: "content", content: { type: "text", text: "hello" } },
80+ {
81+ type: "file",
82+ file: {
83+ _tag: "ToolFile",
84+ name: "photo.png",
85+ mimeType: "image/png",
86+ encoding: "base64",
87+ data: "iVBORw0KGgo=",
88+ byteLength: 8,
89+ },
90+ },
91+ { type: "content", content: { type: "text", text: "mcp hello" } },
92+ { type: "content", content: { type: "image", data: "Zm9v", mimeType: "image/png" } },
93+ { type: "content", content: { type: "audio", data: "SUQz", mimeType: "audio/mpeg" } },
94+ {
95+ type: "content",
96+ content: {
97+ type: "resource",
98+ resource: {
99+ uri: "executor-file:///report.pdf",
100+ mimeType: "application/pdf",
101+ blob: "JVBERg==",
102+ },
103+ },
104+ },
105+ {
106+ type: "content",
107+ content: {
108+ type: "resource_link",
109+ uri: "executor-file:///remote.pdf",
110+ name: "remote.pdf",
111+ mimeType: "application/pdf",
112+ },
113+ },
114+ { type: "content", content: { type: "text", text: '{"arbitrary":true}' } },
115+ ]);
116+ }),
117+ );
118+
38119 it.effect("recovers prose-wrapped fenced async arrow input", () =>
39120 Effect.gen(function* () {
40121 const result = yield* executor.execute(
packages/kernel/runtime-quickjs/src/index.ts+39−0
@@ -2,6 +2,7 @@ import { · 1 unmodified line
22 recoverExecutionBody,
33 stripTypeScript,
44 type CodeExecutor,
5+ type ExecuteOutputItem,
56 type ExecuteResult,
67 type SandboxToolInvoker,
78 } from "@executor-js/codemode-core";
@@ -137,6 +138,36 @@ const buildExecutionSource = (code: string): string => { · 129 unmodified lines
137138 " }",
138139 "};",
139140 "const __formatLogLine = (args) => args.map(__formatLogArg).join(' ');",
141+ "const __outputs = [];",
142+ "globalThis.__executor_outputs = __outputs;",
143+ "const __formatOutputText = (value) => {",
144+ " if (typeof value === 'undefined') return 'undefined';",
145+ " if (value === null) return 'null';",
146+ " if (typeof value === 'string') return value;",
147+ " try {",
148+ " return JSON.stringify(value);",
149+ " } catch {",
150+ " return String(value);",
151+ " }",
152+ "};",
153+ "const __isToolFile = (value) => value && typeof value === 'object' && value._tag === 'ToolFile' && typeof value.mimeType === 'string' && value.encoding === 'base64' && typeof value.data === 'string' && typeof value.byteLength === 'number';",
154+ "const __isMcpTextContentBlock = (value) => value && typeof value === 'object' && value.type === 'text' && typeof value.text === 'string';",
155+ "const __isMcpImageContentBlock = (value) => value && typeof value === 'object' && value.type === 'image' && typeof value.data === 'string' && typeof value.mimeType === 'string';",
156+ "const __isMcpAudioContentBlock = (value) => value && typeof value === 'object' && value.type === 'audio' && typeof value.data === 'string' && typeof value.mimeType === 'string';",
157+ "const __isMcpResourceContentBlock = (value) => value && typeof value === 'object' && value.type === 'resource' && value.resource && typeof value.resource === 'object' && typeof value.resource.uri === 'string' && (typeof value.resource.text === 'string' || typeof value.resource.blob === 'string');",
158+ "const __isMcpResourceLinkContentBlock = (value) => value && typeof value === 'object' && value.type === 'resource_link' && typeof value.uri === 'string' && typeof value.name === 'string';",
159+ "const __isMcpContentBlock = (value) => __isMcpTextContentBlock(value) || __isMcpImageContentBlock(value) || __isMcpAudioContentBlock(value) || __isMcpResourceContentBlock(value) || __isMcpResourceLinkContentBlock(value);",
160+ "const emit = (value) => {",
161+ " if (__isToolFile(value)) {",
162+ " __outputs.push({ type: 'file', file: value });",
163+ " return;",
164+ " }",
165+ " if (__isMcpContentBlock(value)) {",
166+ " __outputs.push({ type: 'content', content: value });",
167+ " return;",
168+ " }",
169+ " __outputs.push({ type: 'content', content: { type: 'text', text: __formatOutputText(value) } });",
170+ "};",
140171 "const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {",
141172 " get(_target, prop) {",
142173 " if (prop === 'then' || typeof prop === 'symbol') {",
@@ -178,6 +209,11 @@ const readPropDump = (context: QuickJSContext, handle: QuickJSHandle, key: strin · 35 unmodified lines
178209 }
179210 };
180211
212+const readOutputItems = (context: QuickJSContext): ExecuteOutputItem[] | undefined => {
213+ const output = readPropDump(context, context.global, "__executor_outputs");
214+ return Array.isArray(output) && output.length > 0 ? (output as ExecuteOutputItem[]) : undefined;
215+};
216+
181217 const readResultState = (
182218 context: QuickJSContext,
183219 handle: QuickJSHandle,
@@ -385,6 +421,7 @@ const evaluateInQuickJs = async ( · 201 unmodified lines
385421 return {
386422 result: null,
387423 error: timeoutMessage(timeoutMs),
424+ output: readOutputItems(context),
388425 logs,
389426 } satisfies ExecuteResult;
390427 }
@@ -393,12 +430,14 @@ const evaluateInQuickJs = async ( · 2 unmodified lines
393430 return {
394431 result: null,
395432 error: normalizeExecutionError(state.error, deadlineMs, timeoutMs),
433+ output: readOutputItems(context),
396434 logs,
397435 } satisfies ExecuteResult;
398436 }
399437
400438 return {
401439 result: state.value,
440+ output: readOutputItems(context),
402441 logs,
403442 } satisfies ExecuteResult;
404443 } finally {