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 | 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( |
| @@ -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 | ||
| 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 | const readResultState = ( | 217 | const 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 { |