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
35 }),35 }),
36 );36 );
37 37
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
38 it.effect("recovers prose-wrapped fenced async arrow input", () =>119 it.effect("recovers prose-wrapped fenced async arrow input", () =>
39 Effect.gen(function* () {120 Effect.gen(function* () {
40 const result = yield* executor.execute(121 const result = yield* executor.execute(
packages/kernel/runtime-quickjs/src/index.ts+39−0
@@ -2,6 +2,7 @@ import { · 1 unmodified line
2 recoverExecutionBody,2 recoverExecutionBody,
3 stripTypeScript,3 stripTypeScript,
4 type CodeExecutor,4 type CodeExecutor,
5 type ExecuteOutputItem,
5 type ExecuteResult,6 type ExecuteResult,
6 type SandboxToolInvoker,7 type SandboxToolInvoker,
7} from "@executor-js/codemode-core";8} from "@executor-js/codemode-core";
@@ -137,6 +138,36 @@ const buildExecutionSource = (code: string): string => { · 129 unmodified lines
137 " }",138 " }",
138 "};",139 "};",
139 "const __formatLogLine = (args) => args.map(__formatLogArg).join(' ');",140 "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 "};",
140 "const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {",171 "const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {",
141 " get(_target, prop) {",172 " get(_target, prop) {",
142 " if (prop === 'then' || typeof prop === 'symbol') {",173 " if (prop === 'then' || typeof prop === 'symbol') {",
@@ -178,6 +209,11 @@ const readPropDump = (context: QuickJSContext, handle: QuickJSHandle, key: strin · 35 unmodified lines
178 }209 }
179};210};
180 211
212const 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
181const readResultState = (217const readResultState = (
182 context: QuickJSContext,218 context: QuickJSContext,
183 handle: QuickJSHandle,219 handle: QuickJSHandle,
@@ -385,6 +421,7 @@ const evaluateInQuickJs = async ( · 201 unmodified lines
385 return {421 return {
386 result: null,422 result: null,
387 error: timeoutMessage(timeoutMs),423 error: timeoutMessage(timeoutMs),
424 output: readOutputItems(context),
388 logs,425 logs,
389 } satisfies ExecuteResult;426 } satisfies ExecuteResult;
390 }427 }
@@ -393,12 +430,14 @@ const evaluateInQuickJs = async ( · 2 unmodified lines
393 return {430 return {
394 result: null,431 result: null,
395 error: normalizeExecutionError(state.error, deadlineMs, timeoutMs),432 error: normalizeExecutionError(state.error, deadlineMs, timeoutMs),
433 output: readOutputItems(context),
396 logs,434 logs,
397 } satisfies ExecuteResult;435 } satisfies ExecuteResult;
398 }436 }
399 437
400 return {438 return {
401 result: state.value,439 result: state.value,
440 output: readOutputItems(context),
402 logs,441 logs,
403 } satisfies ExecuteResult;442 } satisfies ExecuteResult;
404 } finally {443 } finally {