Showing the changes to packages/kernel/runtime-quickjs/src (2 of 15 files). Show all
Unified · Split
| @@ -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 | 119 | it.effect("recovers prose-wrapped fenced async arrow input", () => |
| 39 | 120 | Effect.gen(function* () { |
| 40 | 121 | const result = yield* executor.execute( |
| @@ -2,6 +2,7 @@ import { · 1 unmodified line | ||
| 2 | 2 | recoverExecutionBody, |
| 3 | 3 | stripTypeScript, |
| 4 | 4 | type CodeExecutor, |
| 5 | + type ExecuteOutputItem, | |
| 5 | 6 | type ExecuteResult, |
| 6 | 7 | type SandboxToolInvoker, |
| 7 | 8 | } from "@executor-js/codemode-core"; |
| @@ -137,6 +138,36 @@ const buildExecutionSource = (code: string): string => { · 129 unmodified lines | ||
| 137 | 138 | " }", |
| 138 | 139 | "};", |
| 139 | 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 | 171 | "const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {", |
| 141 | 172 | " get(_target, prop) {", |
| 142 | 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 | |
| 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 | + | |
| 181 | 217 | const readResultState = ( |
| 182 | 218 | context: QuickJSContext, |
| 183 | 219 | handle: QuickJSHandle, |
| @@ -385,6 +421,7 @@ const evaluateInQuickJs = async ( · 201 unmodified lines | ||
| 385 | 421 | return { |
| 386 | 422 | result: null, |
| 387 | 423 | error: timeoutMessage(timeoutMs), |
| 424 | + output: readOutputItems(context), | |
| 388 | 425 | logs, |
| 389 | 426 | } satisfies ExecuteResult; |
| 390 | 427 | } |
| @@ -393,12 +430,14 @@ const evaluateInQuickJs = async ( · 2 unmodified lines | ||
| 393 | 430 | return { |
| 394 | 431 | result: null, |
| 395 | 432 | error: normalizeExecutionError(state.error, deadlineMs, timeoutMs), |
| 433 | + output: readOutputItems(context), | |
| 396 | 434 | logs, |
| 397 | 435 | } satisfies ExecuteResult; |
| 398 | 436 | } |
| 399 | 437 | |
| 400 | 438 | return { |
| 401 | 439 | result: state.value, |
| 440 | + output: readOutputItems(context), | |
| 402 | 441 | logs, |
| 403 | 442 | } satisfies ExecuteResult; |
| 404 | 443 | } finally { |