From b35691bc2ff2a35491e0bf97841b2c8a2fe01f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Thu, 4 Jun 2026 09:45:54 +0200 Subject: [PATCH] script: unwrap only __root sentinel in extract Only unwrap the `__root` sentinel injected for array schemas, ensuring single-field object schemas retain their shape. Also update synthesis prompt instructions for modern JS and tool fidelity. --- src/browser/tools.zig | 11 +++++++++++ src/script/Runtime.zig | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/browser/tools.zig b/src/browser/tools.zig index cfa0d1a0..cd866029 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -127,6 +127,17 @@ pub const save_synthesis_prompt = \\JavaScript wherever they fit; fall back to evaluate(...) only for logic the \\builtins can't express. End with an extract(...) for any data the user \\wanted out. + \\Stay faithful to the recorded tool calls: reproduce each call with the same + \\options it actually used. Do NOT add `timeout` or `waitUntil` to goto (or any + \\tool) unless that option was used in the session — default calls stay default. + \\Use evaluate(...) only when no builtin can express the logic. Never stash a + \\result into `lp.*` and read it back, and never append no-op extract(...) probes + \\or trailing `evaluate("return lp....")` lines — the script's output is whatever + \\the final extract(...) (plus any plain-JS aggregation) produces. + \\Write modern, readable JavaScript: `for (const x of xs)` rather than + \\`for (var i = 0; i < xs.length; i++)`, `const`/`let` over `var`, template + \\literals, destructuring. Indent consistently with 2 spaces, including + \\multi-line extract({...}) schema literals. \\The output MUST be valid JavaScript that runs as-is — it is executed as a \\classic script (not a module), so top-level `await` is a syntax error; \\`await` is only legal inside an `async` function. diff --git a/src/script/Runtime.zig b/src/script/Runtime.zig index f609610c..7fcd3199 100644 --- a/src/script/Runtime.zig +++ b/src/script/Runtime.zig @@ -514,6 +514,8 @@ fn objectWith(arena: std.mem.Allocator, key: []const u8, value: std.json.Value) return .{ .object = obj }; } +/// Unwraps only the `__root` sentinel that `normalizeExtractSchemaString` injects +/// for array schemas; a real single-field object schema keeps its shape. fn normalizeExtractReturnJson(_: *Runtime, arena: std.mem.Allocator, value: []const u8) error{OutOfMemory}![]const u8 { if (value.len == 0) return value; @@ -525,7 +527,7 @@ fn normalizeExtractReturnJson(_: *Runtime, arena: std.mem.Allocator, value: []co var it = parsed.object.iterator(); const entry = it.next() orelse return value; - if (entry.value_ptr.* != .array) return value; + if (!std.mem.eql(u8, entry.key_ptr.*, "__root")) return value; return try std.json.Stringify.valueAlloc(arena, entry.value_ptr.*, .{}); } @@ -691,8 +693,8 @@ test "agent script runtime: extract returns a JavaScript object" { \\ } \\ }] \\}); - \\if (!Array.isArray(options)) throw new Error("single array field should return an array"); - \\if (options[0].text !== "Option 1") throw new Error("unexpected unwrapped option text: " + options[0].text); + \\if (typeof options !== "object" || options === null || Array.isArray(options)) throw new Error("single object field should stay an object"); + \\if (options.options[0].text !== "Option 1") throw new Error("unexpected option text: " + options.options[0].text); \\const direct = extract([{ selector: "#sel option", limit: 1 }]); \\if (!Array.isArray(direct)) throw new Error("array schema should return an array"); \\if (direct[0] !== "Option 1") throw new Error("unexpected direct array extract: " + direct[0]);