Просмотр исходного кода

feat(codemode): support Promise.any and new Promise construction (#36339)

Aiden Cline 1 месяц назад
Родитель
Сommit
fe09a2e9b7

+ 6 - 3
packages/codemode/codemode.md

@@ -64,10 +64,13 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
 ### Tool execution
 ### Tool execution
 
 
 Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls,
 Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls,
-async functions, chained `.then`/`.catch`/`.finally` reactions, `Promise.all`, `Promise.allSettled`, `Promise.race`,
-`Promise.resolve`, and `Promise.reject`. Nested functions therefore cannot end the lifetime of work they started.
+async functions, chained `.then`/`.catch`/`.finally` reactions, `new Promise(executor)` constructions, and the
+`Promise.all`/`allSettled`/`race`/`any`/`resolve`/`reject` statics. Nested functions therefore cannot end the lifetime
+of work they started.
 Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler.
 Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler.
-`Promise.race` uses native non-cancelling settlement semantics: its first result wins while losers continue running.
+`Promise.race` and `Promise.any` use native non-cancelling settlement semantics: the deciding member wins while losers
+continue running, and an all-rejected `Promise.any` rejects with an `AggregateError`. `new Promise(...)` hands the
+executor first-class resolve/reject callables that may escape and settle the promise later, exactly once.
 Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach
 Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach
 order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count
 order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count
 parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,
 parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,

+ 14 - 7
packages/codemode/interpreter-support.md

@@ -94,7 +94,7 @@ ultimate source of truth.
 - [x] Sequence expressions (the comma operator).
 - [x] Sequence expressions (the comma operator).
 - [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its
 - [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its
       continuation one reaction turn.
       continuation one reaction turn.
-- [x] `new` for Error types, Date, RegExp, Map, Set, URL, and URLSearchParams.
+- [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
 - [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
 - [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
 - [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
 - [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
 - [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
 - [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
@@ -103,15 +103,15 @@ ultimate source of truth.
 - [x] Prefix and postfix `++` and `--`.
 - [x] Prefix and postfix `++` and `--`.
 - [x] Plain, arithmetic, bitwise, and logical assignment operators.
 - [x] Plain, arithmetic, bitwise, and logical assignment operators.
 - [ ] Unary `void` and `delete`.
 - [ ] Unary `void` and `delete`.
-- [ ] Arbitrary constructors and `new Promise(...)`.
+- [ ] Arbitrary constructors.
 
 
 ## Promises and tools
 ## Promises and tools
 
 
 - [x] Tool calls start eagerly and return supervised, run-once sandbox promises.
 - [x] Tool calls start eagerly and return supervised, run-once sandbox promises.
 - [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
 - [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
 - [x] `Promise.resolve` and `Promise.reject`.
 - [x] `Promise.resolve` and `Promise.reject`.
-- [x] `Promise.all`, `Promise.allSettled`, and `Promise.race` over supported collections containing promises and plain
-      values.
+- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing
+      promises and plain values.
 - [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings.
 - [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings.
 - [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records.
 - [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records.
 - [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
 - [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
@@ -130,9 +130,14 @@ ultimate source of truth.
       diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted
       diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted
       without a warning.
       without a warning.
 - [x] `try`/`catch` can handle awaited tool and promise failures.
 - [x] `try`/`catch` can handle awaited tool and promise failures.
-- [ ] `Promise.any`.
+- [x] `Promise.any`: first fulfillment wins; all-rejected rejects with an `AggregateError` whose `errors` array holds
+      the catch-normalized reasons in input order, and empty input rejects with an empty `AggregateError`.
+- [x] `new Promise((resolve, reject) => ...)`: the executor runs synchronously and receives first-class resolve/reject
+      callables that settle the promise exactly once (they may escape the executor and settle later); an executor
+      throw rejects unless the promise already settled, resolving with a promise adopts it, and resolving with the
+      promise itself rejects with a `TypeError`. Resolver callables work as `.then`/`.catch` handlers and collection
+      callbacks but remain opaque references that cannot cross the data boundary.
 - [ ] Thenable assimilation (objects with a `then` method are plain data, not promises).
 - [ ] Thenable assimilation (objects with a `then` method are plain data, not promises).
-- [ ] Custom promise construction with `new Promise(...)`.
 - [ ] Async iterables, host streams, and stream consumption.
 - [ ] Async iterables, host streams, and stream consumption.
 
 
 ## Objects and properties
 ## Objects and properties
@@ -163,7 +168,7 @@ ultimate source of truth.
 - [ ] The mapper and `thisArg` forms of `Array.from`.
 - [ ] The mapper and `thisArg` forms of `Array.from`.
 - [ ] `Array.prototype.toSpliced`.
 - [ ] `Array.prototype.toSpliced`.
 - [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
 - [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
-- [ ] Complete sparse-array parity.
+- [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS.
 - [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
 - [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
 
 
 ## Strings
 ## Strings
@@ -268,6 +273,8 @@ ultimate source of truth.
 
 
 - [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
 - [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
       or without `new`.
       or without `new`.
+- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
+      an all-rejected `Promise.any`.
 - [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
 - [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
 - [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
 - [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
 - [x] Catchable interpreter failures and awaited tool failures.
 - [x] Catchable interpreter failures and awaited tool failures.

+ 8 - 2
packages/codemode/src/interpreter/model.ts

@@ -61,7 +61,7 @@ export class ComputedValue {
 
 
 export class PromiseNamespace {}
 export class PromiseNamespace {}
 
 
-export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject"
+export type PromiseMethodName = "all" | "allSettled" | "race" | "any" | "resolve" | "reject"
 
 
 export class PromiseMethodReference {
 export class PromiseMethodReference {
   constructor(readonly name: PromiseMethodName) {}
   constructor(readonly name: PromiseMethodName) {}
@@ -76,6 +76,12 @@ export class PromiseInstanceMethodReference {
   ) {}
   ) {}
 }
 }
 
 
+// The resolve/reject callables handed to a `new Promise(executor)` executor. `settle` closes
+// over the promise's deferred and is first-settlement-wins; later calls are no-ops, as in JS.
+export class PromiseCapabilityFunction {
+  constructor(readonly settle: (value: unknown) => void) {}
+}
+
 export type GlobalNamespaceName =
 export type GlobalNamespaceName =
   | "Object"
   | "Object"
   | "Math"
   | "Math"
@@ -131,7 +137,7 @@ export type DiagnosticKind =
 export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
 export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
 
 
 export const supportedSyntaxMessage =
 export const supportedSyntaxMessage =
-  "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls, and promise chaining with .then/.catch/.finally."
+  "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/any/resolve/reject over arrays mixing promises and plain values for parallel tool calls, promise chaining with .then/.catch/.finally, and new Promise((resolve, reject) => ...) construction."
 
 
 export class InterpreterRuntimeError extends Error {
 export class InterpreterRuntimeError extends Error {
   readonly node?: AstNode
   readonly node?: AstNode

+ 110 - 20
packages/codemode/src/interpreter/runtime.ts

@@ -1,5 +1,5 @@
 import { parse } from "acorn"
 import { parse } from "acorn"
-import { Cause, Effect, Exit, Fiber, Scope, Semaphore } from "effect"
+import { Cause, Deferred, Effect, Exit, Fiber, Scope, Semaphore } from "effect"
 import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
 import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
 import {
 import {
   copyIn,
   copyIn,
@@ -42,6 +42,7 @@ import {
   isRecord,
   isRecord,
   type MemberReference,
   type MemberReference,
   OptionalShortCircuit,
   OptionalShortCircuit,
+  PromiseCapabilityFunction,
   PromiseInstanceMethodReference,
   PromiseInstanceMethodReference,
   PromiseMethodReference,
   PromiseMethodReference,
   type PromiseMethodName,
   type PromiseMethodName,
@@ -94,6 +95,7 @@ import {
   coerceToNumber,
   coerceToNumber,
   coerceToString,
   coerceToString,
   compoundOperators,
   compoundOperators,
+  createAggregateErrorValue,
   createErrorValue,
   createErrorValue,
   errorBrandName,
   errorBrandName,
   errorConstructors,
   errorConstructors,
@@ -226,11 +228,22 @@ const settleAfterTurn = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A
 const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
 const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
   new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
   new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
 
 
-type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction
+// Short-circuit marker for Promise.any: the first fulfillment travels the error channel of
+// the flipped members so fail-fast Effect.all stops observing on it.
+class PromiseAnyFulfilled {
+  constructor(readonly value: unknown) {}
+}
+
+type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction | PromiseCapabilityFunction
 
 
 // Non-callables are ignored as in JS: `.then(undefined, f)` relies on the passthrough.
 // Non-callables are ignored as in JS: `.then(undefined, f)` relies on the passthrough.
 const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
 const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
-  if (value instanceof CodeModeFunction || value instanceof CoercionFunction || value instanceof UriFunction) {
+  if (
+    value instanceof CodeModeFunction ||
+    value instanceof CoercionFunction ||
+    value instanceof UriFunction ||
+    value instanceof PromiseCapabilityFunction
+  ) {
     return value
     return value
   }
   }
   if (typeofValue(value) === "function") {
   if (typeofValue(value) === "function") {
@@ -250,6 +263,21 @@ const caughtErrorValue = (thrown: unknown): unknown => {
   return createErrorValue(name, normalizeError(thrown).message)
   return createErrorValue(name, normalizeError(thrown).message)
 }
 }
 
 
+// `new Error("msg")` and the no-new call form share this; AggregateError alone takes
+// (errors, message?) with a required errors collection, as in JS.
+const constructErrorValue = (name: string, args: Array<unknown>, node: AstNode): SafeObject => {
+  if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
+  const errors = spreadItems(args[0])
+  if (errors === undefined) {
+    throw new InterpreterRuntimeError(
+      "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).",
+      node,
+    ).as("TypeError")
+  }
+  // Copy: spreadItems returns array input itself, and the error value must not alias caller data.
+  return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1]))
+}
+
 const isRuntimeReference = (value: unknown): boolean =>
 const isRuntimeReference = (value: unknown): boolean =>
   value instanceof CodeModeFunction ||
   value instanceof CodeModeFunction ||
   value instanceof ToolReference ||
   value instanceof ToolReference ||
@@ -262,6 +290,7 @@ const isRuntimeReference = (value: unknown): boolean =>
   value instanceof SandboxPromise ||
   value instanceof SandboxPromise ||
   value instanceof CoercionFunction ||
   value instanceof CoercionFunction ||
   value instanceof UriFunction ||
   value instanceof UriFunction ||
+  value instanceof PromiseCapabilityFunction ||
   value instanceof ErrorConstructorReference ||
   value instanceof ErrorConstructorReference ||
   isSandboxValue(value)
   isSandboxValue(value)
 
 
@@ -305,6 +334,7 @@ const typeofValue = (value: unknown): string => {
     value instanceof PromiseMethodReference ||
     value instanceof PromiseMethodReference ||
     value instanceof PromiseInstanceMethodReference ||
     value instanceof PromiseInstanceMethodReference ||
     value instanceof PromiseNamespace ||
     value instanceof PromiseNamespace ||
+    value instanceof PromiseCapabilityFunction ||
     value instanceof ErrorConstructorReference
     value instanceof ErrorConstructorReference
   )
   )
     return "function"
     return "function"
@@ -1584,19 +1614,10 @@ class Interpreter<R> {
     const argNodes = getArray(node, "arguments")
     const argNodes = getArray(node, "arguments")
     const self = this
     const self = this
     if (name === "Promise") {
     if (name === "Promise") {
-      throw new InterpreterRuntimeError(
-        "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
-        node,
-        "UnsupportedSyntax",
-        [supportedSyntaxMessage],
-      )
+      return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => self.constructPromise(args[0], node))
     }
     }
     if (errorConstructors.has(name)) {
     if (errorConstructors.has(name)) {
-      return Effect.gen(function* () {
-        const arg =
-          argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined
-        return createErrorValue(name, arg === undefined ? "" : coerceToString(arg))
-      })
+      return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node))
     }
     }
     if (valueConstructors.has(name)) {
     if (valueConstructors.has(name)) {
       return Effect.gen(function* () {
       return Effect.gen(function* () {
@@ -2066,7 +2087,11 @@ class Interpreter<R> {
       }
       }
       // `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
       // `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
       if (callable instanceof ErrorConstructorReference) {
       if (callable instanceof ErrorConstructorReference) {
-        return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0]))
+        return constructErrorValue(callable.name, args, node)
+      }
+      if (callable instanceof PromiseCapabilityFunction) {
+        callable.settle(args[0])
+        return undefined
       }
       }
       throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
       throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
     })
     })
@@ -2255,8 +2280,8 @@ class Interpreter<R> {
       return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
       return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
     }
     }
 
 
-    const items = spreadItems(args[0])
-    if (items === undefined) {
+    const spread = spreadItems(args[0])
+    if (spread === undefined) {
       return this.createPromise(
       return this.createPromise(
         Effect.fail(
         Effect.fail(
           new InterpreterRuntimeError(
           new InterpreterRuntimeError(
@@ -2266,6 +2291,8 @@ class Interpreter<R> {
         ),
         ),
       )
       )
     }
     }
+    // Densify: JS combinator iteration reads sparse holes as undefined members; .map would skip them.
+    const items = Array.from(spread)
 
 
     // JS makes combinator members "handled" synchronously at the call - their rejections
     // JS makes combinator members "handled" synchronously at the call - their rejections
     // belong to the aggregate from this moment, even ones settling before it runs.
     // belong to the aggregate from this moment, even ones settling before it runs.
@@ -2331,6 +2358,29 @@ class Interpreter<R> {
         // and is interrupted at normal completion (already observed) or by teardown.
         // and is interrupted at normal completion (already observed) or by teardown.
         return this.createPromise(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
         return this.createPromise(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
       }
       }
+      case "any": {
+        // De Morgan dual of Promise.all: members are flipped so the first fulfillment
+        // short-circuits fail-fast Effect.all, and all-rejected completes with the reasons
+        // in input order for the AggregateError.
+        const flipped = items.map((item) =>
+          item instanceof SandboxPromise
+            ? Effect.flatMap(this.promises.await(item), (exit) => {
+                if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
+                if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
+                return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause)))
+              })
+            : Effect.fail(new PromiseAnyFulfilled(item)),
+        )
+        const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe(
+          Effect.flatMap((reasons) =>
+            Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
+          ),
+          Effect.catch((error) =>
+            error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
+          ),
+        )
+        return this.createPromise(settleAfterTurn(body))
+      }
     }
     }
   }
   }
 
 
@@ -2405,6 +2455,43 @@ class Interpreter<R> {
     )
     )
   }
   }
 
 
+  // new Promise(executor): the promise's fiber awaits a Deferred that resolve/reject settle
+  // exactly once. The executor runs synchronously; its throw rejects the promise unless it
+  // already settled (JS swallows post-settlement executor throws).
+  private constructPromise(executor: unknown, node: AstNode): Effect.Effect<SandboxPromise, unknown, R> {
+    if (!(executor instanceof CodeModeFunction)) {
+      throw new InterpreterRuntimeError(
+        "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
+        node,
+      ).as("TypeError")
+    }
+    const self = this
+    return Effect.gen(function* () {
+      const deferred = Deferred.makeUnsafe<unknown, unknown>()
+      const box: { own?: SandboxPromise } = {}
+      const promise = yield* self.createPromise(
+        Effect.flatMap(Deferred.await(deferred), (value) => {
+          if (!(value instanceof SandboxPromise)) return Effect.succeed(value)
+          if (value === box.own) return Effect.fail(selfResolutionError(node))
+          return self.settlePromise(value)
+        }),
+      )
+      box.own = promise
+      const resolve = new PromiseCapabilityFunction((value) => {
+        Deferred.doneUnsafe(deferred, Exit.succeed(value))
+      })
+      const reject = new PromiseCapabilityFunction((value) => {
+        Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value)))
+      })
+      const executed = yield* Effect.exit(self.invokeFunction(executor, [resolve, reject]))
+      if (!Exit.isSuccess(executed)) {
+        if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
+        Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
+      }
+      return promise
+    })
+  }
+
   private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
   private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
     const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promises, this.logs, this.callPermits)
     const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promises, this.logs, this.callPermits)
     invocation.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
     invocation.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
@@ -2568,7 +2655,8 @@ class Interpreter<R> {
     if (
     if (
       !(callback instanceof CodeModeFunction) &&
       !(callback instanceof CodeModeFunction) &&
       !(callback instanceof CoercionFunction) &&
       !(callback instanceof CoercionFunction) &&
-      !(callback instanceof UriFunction)
+      !(callback instanceof UriFunction) &&
+      !(callback instanceof PromiseCapabilityFunction)
     ) {
     ) {
       throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
       throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
     }
     }
@@ -2577,7 +2665,9 @@ class Interpreter<R> {
         ? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
         ? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
         : callback instanceof UriFunction
         : callback instanceof UriFunction
           ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
           ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
-          : this.invokeFunction(callback, callbackArgs)
+          : callback instanceof PromiseCapabilityFunction
+            ? Effect.sync(() => callback.settle(callbackArgs[0]))
+            : this.invokeFunction(callback, callbackArgs)
   }
   }
 
 
   private invokeMapMethod(
   private invokeMapMethod(
@@ -3185,7 +3275,7 @@ class Interpreter<R> {
           return new PromiseMethodReference(key as PromiseMethodName)
           return new PromiseMethodReference(key as PromiseMethodName)
         }
         }
         throw new InterpreterRuntimeError(
         throw new InterpreterRuntimeError(
-          `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`,
+          `Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`,
           propertyNode,
           propertyNode,
         )
         )
       }
       }

+ 1 - 1
packages/codemode/src/stdlib/promise.ts

@@ -1,6 +1,6 @@
 import type { PromiseMethodName } from "../interpreter/model.js"
 import type { PromiseMethodName } from "../interpreter/model.js"
 
 
-export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "resolve", "reject"])
+export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
 
 
 /** Maximum number of eagerly forked tool calls that may run concurrently. */
 /** Maximum number of eagerly forked tool calls that may run concurrently. */
 export const TOOL_CALL_CONCURRENCY = 8
 export const TOOL_CALL_CONCURRENCY = 8

+ 4 - 0
packages/codemode/src/stdlib/value.ts

@@ -6,6 +6,7 @@ export const errorConstructors = new Set([
   "ReferenceError",
   "ReferenceError",
   "EvalError",
   "EvalError",
   "URIError",
   "URIError",
+  "AggregateError",
 ])
 ])
 
 
 export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
 export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
@@ -20,6 +21,9 @@ export const createErrorValue = (name: string, message: string): SafeObject => {
   return value
   return value
 }
 }
 
 
+export const createAggregateErrorValue = (errors: Array<unknown>, message: string): SafeObject =>
+  Object.assign(createErrorValue("AggregateError", message), { errors })
+
 export const errorBrandName = (value: unknown): string | undefined =>
 export const errorBrandName = (value: unknown): string | undefined =>
   value !== null && typeof value === "object"
   value !== null && typeof value === "object"
     ? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
     ? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)

+ 1 - 1
packages/codemode/src/tool-runtime.ts

@@ -611,7 +611,7 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
     "## Language",
     "## Language",
     "",
     "",
     "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
     "Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
-    "Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and new Promise(...) are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
+    "Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
     "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
     "Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
     "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
     "Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
   ]
   ]

+ 2 - 1
packages/codemode/test/codemode.test.ts

@@ -672,9 +672,10 @@ describe("CodeMode public contract", () => {
     expect(instructions).toContain("not a general-purpose runtime")
     expect(instructions).toContain("not a general-purpose runtime")
     expect(instructions).not.toContain("Standard modern JavaScript works")
     expect(instructions).not.toContain("Standard modern JavaScript works")
     expect(instructions).not.toContain("TypeScript type annotations")
     expect(instructions).not.toContain("TypeScript type annotations")
-    for (const missing of ["Modules/imports", "classes", "generators", "fetch", "new Promise(...)"]) {
+    for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
       expect(instructions).toContain(missing)
       expect(instructions).toContain(missing)
     }
     }
+    expect(instructions).not.toContain("new Promise(...) are unavailable")
     expect(instructions).not.toContain("promise chaining")
     expect(instructions).not.toContain("promise chaining")
     expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
     expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
     expect(instructions).not.toContain("host globals")
     expect(instructions).not.toContain("host globals")

+ 423 - 4
packages/codemode/test/promise-test262.test.ts

@@ -573,13 +573,14 @@ describe("Test262 async functions and await", () => {
 })
 })
 
 
 describe("Test262 expected Promise conformance", () => {
 describe("Test262 expected Promise conformance", () => {
-  for (const name of ["all", "allSettled", "race"] as const) {
+  for (const name of ["all", "allSettled", "race", "any"] as const) {
     test(`Promise.${name} rejects invalid input with TypeError`, async () => {
     test(`Promise.${name} rejects invalid input with TypeError`, async () => {
       // Sources:
       // Sources:
       // test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
       // test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
       // test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
       // test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
       // test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
       // test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
       // test/built-ins/Promise/race/iter-arg-is-number-reject.js
       // test/built-ins/Promise/race/iter-arg-is-number-reject.js
+      // test/built-ins/Promise/any/iter-arg-is-number-reject.js
       expect(
       expect(
         await value(`
         await value(`
           try {
           try {
@@ -595,7 +596,7 @@ describe("Test262 expected Promise conformance", () => {
     })
     })
   }
   }
 
 
-  test.failing("Promise.all consumes sparse positions as undefined", async () => {
+  test("Promise.all consumes sparse positions as undefined", async () => {
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     expect(
     expect(
       await value(`
       await value(`
@@ -607,7 +608,7 @@ describe("Test262 expected Promise conformance", () => {
     ).toEqual([2, true, 1])
     ).toEqual([2, true, 1])
   })
   })
 
 
-  test.failing("Promise.allSettled consumes sparse positions as undefined", async () => {
+  test("Promise.allSettled consumes sparse positions as undefined", async () => {
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     expect(
     expect(
       await value(`
       await value(`
@@ -619,7 +620,7 @@ describe("Test262 expected Promise conformance", () => {
     ).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
     ).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
   })
   })
 
 
-  test.failing("Promise.race consumes a sparse first position as undefined", async () => {
+  test("Promise.race consumes a sparse first position as undefined", async () => {
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
     expect(
     expect(
       await value(`
       await value(`
@@ -630,6 +631,17 @@ describe("Test262 expected Promise conformance", () => {
     ).toBe(true)
     ).toBe(true)
   })
   })
 
 
+  test("Promise.any consumes a sparse first position as an undefined fulfillment", async () => {
+    // Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
+    expect(
+      await value(`
+        const input = []
+        input[1] = Promise.reject("loses")
+        return (await Promise.any(input)) === undefined
+      `),
+    ).toBe(true)
+  })
+
   test("Promise.all settles after reactions attached to its inputs", async () => {
   test("Promise.all settles after reactions attached to its inputs", async () => {
     // Sources:
     // Sources:
     // test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
     // test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
@@ -1002,3 +1014,410 @@ describe("Test262 expected Promise conformance", () => {
     ).toBe(true)
     ).toBe(true)
   })
   })
 })
 })
+
+describe("Test262 Promise.any", () => {
+  test("is a callable static that returns a promise", async () => {
+    // Sources:
+    // test/built-ins/Promise/any/is-function.js
+    // test/built-ins/Promise/any/returns-promise.js
+    expect(
+      await value(`
+        const promise = Promise.any([1])
+        return [typeof Promise.any, promise instanceof Promise, await promise]
+      `),
+    ).toEqual(["function", true, 1])
+  })
+
+  test("fulfills with the first fulfilled member, ignoring rejections", async () => {
+    // Sources:
+    // test/built-ins/Promise/any/resolved-sequence-mixed.js
+    // test/built-ins/Promise/any/resolved-sequence-with-rejections.js
+    // test/built-ins/Promise/any/reject-ignored-immed.js
+    expect(
+      await value(`
+        return [
+          await Promise.any([Promise.reject("a"), Promise.resolve(1), Promise.resolve(2)]),
+          await Promise.any([Promise.reject("a"), "plain", Promise.reject("b")]),
+        ]
+      `),
+    ).toEqual([1, "plain"])
+  })
+
+  test("a fulfillment wins over a later rejection of another member", async () => {
+    // Sources:
+    // test/built-ins/Promise/any/resolve-ignores-late-rejection.js
+    // test/built-ins/Promise/any/resolve-ignores-late-rejection-deferred.js
+    expect(
+      await value(`
+        let rejectLate
+        const late = new Promise((_, reject) => { rejectLate = reject })
+        const result = await Promise.any([late, Promise.resolve("won")])
+        rejectLate("too late")
+        return result
+      `),
+    ).toBe("won")
+  })
+
+  test("rejects with an AggregateError carrying the reasons in input order", async () => {
+    // Sources:
+    // test/built-ins/Promise/any/reject-all-mixed.js
+    // test/built-ins/Promise/any/reject-immed.js
+    // test/built-ins/Promise/any/reject-deferred.js
+    expect(
+      await value(`
+        let rejectLate
+        const late = new Promise((_, reject) => { rejectLate = reject })
+        const aggregate = Promise.any([Promise.reject("first"), late, Promise.reject("third")])
+        rejectLate("second")
+        try {
+          await aggregate
+          return "fulfilled"
+        } catch (error) {
+          return {
+            isAggregate: error instanceof AggregateError,
+            isError: error instanceof Error,
+            name: error.name,
+            message: error.message,
+            errors: error.errors,
+          }
+        }
+      `),
+    ).toEqual({
+      isAggregate: true,
+      isError: true,
+      name: "AggregateError",
+      message: "All promises were rejected",
+      errors: ["first", "second", "third"],
+    })
+  })
+
+  test("rejects an empty input with an empty AggregateError", async () => {
+    // Source: test/built-ins/Promise/any/iter-arg-is-empty-iterable-reject.js
+    expect(
+      await value(`
+        try {
+          await Promise.any([])
+          return "fulfilled"
+        } catch (error) {
+          return [error instanceof AggregateError, error.errors.length]
+        }
+      `),
+    ).toEqual([true, 0])
+  })
+
+  test("consumes a string input as its characters", async () => {
+    // Source: test/built-ins/Promise/any/iter-arg-is-string-resolve.js
+    expect(await value(`return await Promise.any("abc")`)).toBe("a")
+  })
+
+  test("rejects an empty string input with an empty AggregateError", async () => {
+    // Source: test/built-ins/Promise/any/iter-arg-is-empty-string-reject.js
+    expect(
+      await value(`
+        try {
+          await Promise.any("")
+          return "fulfilled"
+        } catch (error) {
+          return [error instanceof AggregateError, error.errors.length]
+        }
+      `),
+    ).toEqual([true, 0])
+  })
+
+  test("fulfills with the first member that does not reject", async () => {
+    // Sources:
+    // test/built-ins/Promise/any/resolve-from-reject-catch.js
+    // test/built-ins/Promise/any/resolve-from-resolve-reject-catch.js
+    expect(
+      await value(`
+        return await Promise.any([
+          Promise.reject("a"),
+          new Promise((resolve, reject) => reject("b")),
+          Promise.all([Promise.reject("c")]),
+          Promise.resolve(Promise.reject("d").catch((reason) => reason)),
+        ])
+      `),
+    ).toBe("d")
+  })
+
+  test("settles after reactions attached to its inputs", async () => {
+    // Source: test/built-ins/Promise/any/resolved-sequence.js
+    expect(
+      await value(`
+        const sequence = [1]
+        const input = Promise.resolve(1)
+        const aggregate = Promise.any([input])
+        aggregate.then(() => sequence.push(4))
+        input.then(() => sequence.push(3)).then(() => sequence.push(5))
+        sequence.push(2)
+        await aggregate
+        await Promise.resolve()
+        return sequence
+      `),
+    ).toEqual([1, 2, 3, 4, 5])
+  })
+})
+
+describe("Test262 AggregateError", () => {
+  test("constructs from an errors collection and an optional message", async () => {
+    // Sources:
+    // test/built-ins/AggregateError/errors-iterabletolist.js
+    // test/built-ins/AggregateError/message-undefined-no-prop.js
+    expect(
+      await value(`
+        const input = ["x", "y"]
+        const withMessage = new AggregateError(input, "msg")
+        const bare = new AggregateError([])
+        return [
+          withMessage.name,
+          withMessage.message,
+          withMessage.errors,
+          withMessage.errors !== input,
+          withMessage instanceof AggregateError,
+          withMessage instanceof Error,
+          bare.message,
+          bare.errors,
+        ]
+      `),
+    ).toEqual(["AggregateError", "msg", ["x", "y"], true, true, true, "", []])
+  })
+
+  test("rejects a non-collection errors argument with TypeError", async () => {
+    // Source: test/built-ins/AggregateError/errors-iterabletolist-failures.js
+    expect(
+      await value(`
+        try {
+          new AggregateError(42)
+          return "constructed"
+        } catch (error) {
+          return error.name
+        }
+      `),
+    ).toBe("TypeError")
+  })
+
+  test("is callable without new", async () => {
+    // Source: test/built-ins/AggregateError/newtarget-is-undefined.js
+    expect(
+      await value(`
+        const error = AggregateError(["x"], "m")
+        return [error instanceof AggregateError, error instanceof Error, error.name, error.message, error.errors]
+      `),
+    ).toEqual([true, true, "AggregateError", "m", ["x"]])
+  })
+
+  test("coerces a non-string message to a string", async () => {
+    // Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the
+    // upstream object-with-toString case is omitted because the sandbox has no user toString dispatch)
+    expect(
+      await value(`
+        return [
+          new AggregateError([], 42).message,
+          new AggregateError([], false).message,
+          new AggregateError([], true).message,
+          new AggregateError([], null).message,
+        ]
+      `),
+    ).toEqual(["42", "false", "true", "null"])
+  })
+})
+
+describe("Test262 Promise constructor", () => {
+  test("constructs a promise, handing the executor callable resolve/reject", async () => {
+    // Sources:
+    // test/built-ins/Promise/constructor.js
+    // test/built-ins/Promise/exec-args.js
+    expect(
+      await value(`
+        let observed
+        const promise = new Promise((resolve, reject) => {
+          observed = [typeof resolve, typeof reject]
+          resolve("done")
+        })
+        return [promise instanceof Promise, observed, await promise]
+      `),
+    ).toEqual([true, ["function", "function"], "done"])
+  })
+
+  test("a missing or non-callable executor is a TypeError", async () => {
+    // Source: test/built-ins/Promise/executor-not-callable.js
+    expect(
+      await value(`
+        const outcomes = []
+        for (const make of [() => new Promise(), () => new Promise(1), () => new Promise({})]) {
+          try {
+            make()
+            outcomes.push("constructed")
+          } catch (error) {
+            outcomes.push(error.name)
+          }
+        }
+        return outcomes
+      `),
+    ).toEqual(["TypeError", "TypeError", "TypeError"])
+  })
+
+  test("resolves immediately or later through an escaping resolver", async () => {
+    // Sources:
+    // test/built-ins/Promise/resolve-non-thenable-immed.js
+    // test/built-ins/Promise/resolve-non-thenable-deferred.js
+    // test/built-ins/Promise/create-resolving-functions-resolve.js
+    expect(
+      await value(`
+        let settle
+        const deferred = new Promise((resolve) => { settle = resolve })
+        const immediate = new Promise((resolve) => resolve("now"))
+        settle("later")
+        return [await immediate, await deferred]
+      `),
+    ).toEqual(["now", "later"])
+  })
+
+  test("rejects through reject and through an abrupt executor completion", async () => {
+    // Sources:
+    // test/built-ins/Promise/reject-via-fn-immed.js
+    // test/built-ins/Promise/reject-via-abrupt.js
+    expect(
+      await value(`
+        const observe = async (promise) => {
+          try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason.message ?? reason] }
+        }
+        return [
+          await observe(new Promise((_, reject) => reject("nope"))),
+          await observe(new Promise(() => { throw new Error("boom") })),
+        ]
+      `),
+    ).toEqual([
+      ["rejected", "nope"],
+      ["rejected", "boom"],
+    ])
+  })
+
+  test("only the first settlement counts", async () => {
+    // Sources:
+    // test/built-ins/Promise/reject-ignored-via-fn-immed.js
+    // test/built-ins/Promise/resolve-ignored-via-fn-immed.js
+    expect(
+      await value(`
+        return [
+          await new Promise((resolve, reject) => { resolve("first"); reject("second"); resolve("third") }),
+          await new Promise((resolve) => { resolve(resolve("inner") === undefined ? "unreached" : "also unreached") }),
+        ]
+      `),
+    ).toEqual(["first", "inner"])
+  })
+
+  test("escaped resolvers keep first-settle-wins in both directions", async () => {
+    // Sources:
+    // test/built-ins/Promise/reject-ignored-via-fn-deferred.js
+    // test/built-ins/Promise/resolve-ignored-via-fn-deferred.js
+    expect(
+      await value(`
+        let resolveRejected, rejectRejected
+        const rejected = new Promise((resolve, reject) => { resolveRejected = resolve; rejectRejected = reject })
+        rejectRejected("first")
+        const lateResolve = resolveRejected("late")
+        let resolveFulfilled, rejectFulfilled
+        const fulfilled = new Promise((resolve, reject) => { resolveFulfilled = resolve; rejectFulfilled = reject })
+        resolveFulfilled()
+        const lateReject = rejectFulfilled(new Promise(() => {}))
+        try {
+          await rejected
+          return "fulfilled"
+        } catch (reason) {
+          return [reason, lateResolve === undefined, (await fulfilled) === undefined, lateReject === undefined]
+        }
+      `),
+    ).toEqual(["first", true, true, true])
+  })
+
+  test("a queued reaction chain observes a later rejection through a handler-less then", async () => {
+    // Sources:
+    // test/built-ins/Promise/reject-via-fn-immed-queue.js
+    // test/built-ins/Promise/reject-via-fn-deferred-queue.js
+    // test/built-ins/Promise/reject-via-abrupt-queue.js
+    expect(
+      await value(`
+        const observe = (promise) => promise.then(() => "wrong").then(() => "also wrong", (reason) => "caught:" + reason)
+        let reject
+        const deferred = new Promise((_, r) => { reject = r })
+        const chained = observe(deferred)
+        reject("boom")
+        return [
+          await observe(new Promise((_, r) => r("immed"))),
+          await chained,
+          await observe(new Promise(() => { throw "abrupt" })),
+        ]
+      `),
+    ).toEqual(["caught:immed", "caught:boom", "caught:abrupt"])
+  })
+
+  test("an exception after resolve is ignored", async () => {
+    // Source: test/built-ins/Promise/exception-after-resolve-in-executor.js
+    expect(await value(`return await new Promise((resolve) => { resolve("kept"); throw new Error("dropped") })`)).toBe(
+      "kept",
+    )
+  })
+
+  test("resolving with a promise adopts its settlement", async () => {
+    // Sources:
+    // test/built-ins/Promise/resolve-thenable-immed.js (promise-adoption portion)
+    // test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js (resolution adoption semantics)
+    expect(
+      await value(`
+        const adoptedValue = await new Promise((resolve) => resolve(Promise.resolve("adopted")))
+        try {
+          await new Promise((resolve) => resolve(Promise.reject("bad")))
+          return [adoptedValue, "fulfilled"]
+        } catch (reason) {
+          return [adoptedValue, reason]
+        }
+      `),
+    ).toEqual(["adopted", "bad"])
+  })
+
+  test("resolving with the promise itself rejects with TypeError", async () => {
+    // Source: test/built-ins/Promise/resolve-self.js
+    expect(
+      await value(`
+        let settle
+        const promise = new Promise((resolve) => { settle = resolve })
+        settle(promise)
+        try {
+          await promise
+          return "fulfilled"
+        } catch (error) {
+          return error.name
+        }
+      `),
+    ).toBe("TypeError")
+  })
+
+  test("executor runs synchronously before the constructor returns", async () => {
+    // Source: test/built-ins/Promise/executor-call-context-strict.js (synchronous Call(executor) step)
+    expect(
+      await value(`
+        const sequence = []
+        sequence.push("before")
+        new Promise((resolve) => { sequence.push("executor"); resolve() })
+        sequence.push("after")
+        return sequence
+      `),
+    ).toEqual(["before", "executor", "after"])
+  })
+
+  test.failing("calling Promise without new throws TypeError", async () => {
+    // Source: test/built-ins/Promise/undefined-newtarget.js
+    // The sandbox currently reports a generic Error ("Only tools are callable in CodeMode.").
+    expect(
+      await value(`
+        try {
+          Promise(() => {})
+          return "called"
+        } catch (error) {
+          return error.name
+        }
+      `),
+    ).toBe("TypeError")
+  })
+})

+ 172 - 8
packages/codemode/test/promise.test.ts

@@ -1062,15 +1062,179 @@ describe("unsupported promise surface", () => {
   })
   })
 
 
   test("unknown Promise statics list what is available", async () => {
   test("unknown Promise statics list what is available", async () => {
-    const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`)
-    expect(diagnostic.message).toContain("Promise.any is not available")
-    expect(diagnostic.message).toContain("Promise.allSettled")
+    const diagnostic = await error(`return await Promise.withResolvers()`)
+    expect(diagnostic.message).toContain("Promise.withResolvers is not available")
+    expect(diagnostic.message).toContain("Promise.any")
   })
   })
+})
+
+describe("Promise.any", () => {
+  test("first tool success wins; failing and losing calls are handled silently", async () => {
+    const trace = makeTrace()
+    const result = await run(
+      `
+        const winner = await Promise.any([
+          tools.host.fail({}),
+          tools.host.sleepy({ id: 1, ms: 5 }),
+          tools.host.sleepy({ id: 2, ms: 60000 }),
+        ])
+        return winner
+      `,
+      { trace },
+    )
+    expect(result.ok).toBe(true)
+    if (!result.ok) return
+    expect(result.value).toBe(1)
+    // The slow loser stays execution-owned and is interrupted at completion; the tool
+    // failure was observed by the aggregate, so no rejection warning survives.
+    expect(result.warnings).toBeUndefined()
+    expect(trace.interrupted).toBe(1)
+  })
+
+  test("all members failing rejects with catch-normalized reasons in input order", async () => {
+    expect(
+      await value(`
+        try {
+          await Promise.any([tools.host.fail({}), Promise.reject("plain")])
+          return "fulfilled"
+        } catch (error) {
+          return [error.name, error.errors.map((reason) => reason.message ?? reason)]
+        }
+      `),
+    ).toEqual(["AggregateError", ["Lookup refused", "plain"]])
+  })
+
+  test("settles one reaction turn after its deciding member, as in V8", async () => {
+    expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2)
+  })
+
+  test("a tie is decided by settlement order, not input order", async () => {
+    // Handlers run in attach order, so `first` settles before `second` and wins
+    // despite its later input position - as in real JS.
+    expect(
+      await value(`
+        const first = Promise.resolve().then(() => "one")
+        const second = Promise.resolve().then(() => "two")
+        return await Promise.any([second, first])
+      `),
+    ).toBe("one")
+  })
+
+  test("an abandoned rejecting aggregate is interrupted silently at the return", async () => {
+    const result = await run(`
+      Promise.any([Promise.reject(new Error("boom"))])
+      return "done"
+    `)
+    expect(result.ok).toBe(true)
+    if (!result.ok) return
+    expect(result.value).toBe("done")
+    expect(result.warnings).toBeUndefined()
+  })
+})
 
 
-  test("new Promise(...) points at tool calls instead", async () => {
-    const diagnostic = await error(`return new Promise((resolve) => resolve(1))`)
-    expect(diagnostic.kind).toBe("UnsupportedSyntax")
-    expect(diagnostic.message).toContain("new Promise(...) is not supported")
-    expect(diagnostic.message).toContain("already return promises")
+describe("promise construction", () => {
+  test("a deferred gate coordinates tool results across async functions", async () => {
+    expect(
+      await value(`
+        let openGate
+        const gate = new Promise((resolve) => { openGate = resolve })
+        const worker = (async () => {
+          const id = await gate
+          return id * 2
+        })()
+        openGate(await tools.host.sleepy({ id: 21, ms: 5 }))
+        return await worker
+      `),
+    ).toBe(42)
+  })
+
+  test("the .then(resolve) bridge settles a constructed promise", async () => {
+    expect(
+      await value(`
+        const bridged = new Promise((resolve, reject) => {
+          tools.host.sleepy({ id: 7, ms: 5 }).then(resolve, reject)
+        })
+        return await bridged
+      `),
+    ).toBe(7)
+  })
+
+  test("constructed promises participate in combinators", async () => {
+    expect(
+      await value(`
+        let settle
+        const manual = new Promise((resolve) => { settle = resolve })
+        const race = Promise.race([manual, tools.host.sleepy({ id: 3, ms: 60000 })])
+        const all = Promise.all([manual, "plain"])
+        const any = Promise.any([manual, new Promise(() => {})])
+        settle("manual")
+        return [await race, await all, await any]
+      `),
+    ).toEqual(["manual", ["manual", "plain"], "manual"])
+  })
+
+  test("resolving with a pending promise adopts its later settlement", async () => {
+    expect(
+      await value(`
+        let innerResolve, innerReject
+        const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve })))
+        const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject })))
+        innerResolve("later")
+        innerReject("bad")
+        try {
+          return [await adopted, await adoptedRejection]
+        } catch (reason) {
+          return [await adopted, reason]
+        }
+      `),
+    ).toEqual(["later", "bad"])
+  })
+
+  test("an async executor's post-await resolve settles the promise", async () => {
+    expect(
+      await value(`
+        const result = new Promise(async (resolve) => {
+          const id = await tools.host.sleepy({ id: 5, ms: 5 })
+          resolve(id * 2)
+        })
+        return await result
+      `),
+    ).toBe(10)
+  })
+
+  test("a never-settled promise is abandoned silently at the return", async () => {
+    const result = await run(`
+      const forever = new Promise(() => {})
+      forever.then(() => {})
+      return "done"
+    `)
+    expect(result.ok).toBe(true)
+    if (!result.ok) return
+    expect(result.value).toBe("done")
+    expect(result.warnings).toBeUndefined()
+  })
+
+  test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => {
+    const result = await run(`
+      new Promise((_, reject) => reject(new Error("dropped")))
+      await Promise.resolve()
+      await Promise.resolve()
+      return "done"
+    `)
+    expect(result.ok).toBe(true)
+    if (!result.ok) return
+    expect(result.value).toBe("done")
+    expect(result.warnings).toHaveLength(1)
+    expect(result.warnings?.[0].message).toContain("Unhandled rejection")
+    expect(result.warnings?.[0].message).toContain("dropped")
+  })
+
+  test("resolver functions cannot cross the data boundary", async () => {
+    const diagnostic = await error(`
+      let escaped
+      new Promise((resolve) => { escaped = resolve })
+      return { escaped }
+    `)
+    expect(diagnostic.kind).toBe("InvalidDataValue")
   })
   })
 })
 })