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.
This commit is contained in:
Adrià Arrufat
2026-06-04 09:45:54 +02:00
parent b29f68ce82
commit b35691bc2f
2 changed files with 16 additions and 3 deletions

View File

@@ -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.

View File

@@ -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]);