Add promise settlement streaming helpers to code mode (#51126)

## What changed

Expose two JavaScript globals for consuming results as inputs settle:

- `as_settled(promises)` returns an async iterator of fulfilled and rejected records in settlement order, preserving queued results while the consumer is busy.
- `stream_settled(promises, emit)` delivers those records to a callback, awaiting each invocation and propagating callback errors.

Records include an `index`, a `status`, and either a `value` or a `reason`. Iterable inputs use positional indices; `Map` inputs retain their keys. Both helpers accept promises, thenables, and plain values.

## Testing

Add runtime coverage for ordering, input types, object identity, callback behavior, early iteration exit, and termination. Add stdio and gRPC host tests for partial results across waits, plus a scenario snapshot verifying fulfilled and rejected records reach the model.

GitOrigin-RevId: cddc9f3679ae04f2421e154caa0417afb15f550d
This commit is contained in:
Channing Conger
2026-10-05 16:58:29 +00:00
committed by copyberry
parent 402f5b6fdf
commit 28a264fbc7
9 changed files with 624 additions and 0 deletions
@@ -46,6 +46,12 @@ pub(super) fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), St
set_global(scope, global, "notify", notify.into())?;
set_global(scope, global, "yield_control", yield_control.into())?;
set_global(scope, global, "exit", exit.into())?;
let source = v8::String::new(scope, include_str!("settled.js"))
.ok_or_else(|| "failed to allocate settlement helpers".to_string())?;
v8::Script::compile(scope, source, /*origin*/ None)
.and_then(|script| script.run(scope))
.ok_or_else(|| "failed to initialize settlement helpers".to_string())?;
Ok(())
}
@@ -0,0 +1,67 @@
// Runtime-owned promise combinators; all state stays in the cell's V8 heap.
// Observe each input once and queue settlements while the consumer is busy.
// Repeated races can lose that order and attach extra reactions to pending inputs.
(() => {
function as_settled(promises) {
let ready = [];
let pending = 0;
let wake;
let closed = false;
function settle(result) {
pending--;
if (closed) return;
ready.push(result);
if (wake) {
const resolve = wake;
wake = undefined;
resolve();
}
}
const keyed = promises instanceof Map;
let position = 0;
try {
for (const entry of promises) {
const [index, promise] = keyed ? entry : [position++, entry];
pending++;
Promise.resolve(promise).then(
(value) => settle({ index, status: "fulfilled", value }),
(reason) => settle({ index, status: "rejected", reason }),
);
}
} catch (error) {
closed = true;
throw error;
}
return (async function* () {
try {
while (pending > 0 || ready.length > 0) {
if (ready.length === 0) {
await new Promise((resolve) => {
wake = resolve;
});
}
const batch = ready;
ready = [];
yield* batch;
}
} finally {
closed = true;
ready = [];
}
})();
}
async function stream_settled(promises, emit) {
if (typeof emit !== "function") {
throw new TypeError("stream_settled requires a callback");
}
for await (const result of as_settled(promises)) {
await emit(result);
}
}
Object.assign(globalThis, { as_settled, stream_settled });
})();
@@ -1127,6 +1127,7 @@ async fn global_scope_contains_only_allowed_items() {
"WeakSet",
"__codexContentItems",
"add_content",
"as_settled",
"audio",
"decodeURI",
"decodeURIComponent",
@@ -1146,6 +1147,7 @@ async fn global_scope_contains_only_allowed_items() {
"parseInt",
"setTimeout",
"store",
"stream_settled",
"text",
"tools",
"undefined",