From 853a0aa3857856e0483d5898050f0a432055349b Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 17 Jul 2026 21:33:47 +0000 Subject: [PATCH 01/12] WebMCP: Spec executeTool() algorithm This CL introduces the `executeTool()` method to the `ModelContext` interface in the WebMCP specification. This aligns the spec with the C++ implementation in Blink. Key changes: - Adds `executeTool()` and `ExecuteToolOptions` IDL definitions. - Specifies the `executeTool()` algorithm, detailing cross-document tool lookup, origin visibility checks, and asynchronous invocation in the target document`s realm. - Clarifies the execution callback arguments and JSON serialization. - Incorporates `AbortSignal` handling for tool cancellation, dispatching a `toolcancel` event in the target document. - Fires `toolactivated` event in the target document. - Updates IDL member access syntax to use idiomatic Bikeshed style. Small things Fix some things Don't serialize Object argument enforcement Add AbortSignal issue Small things --- index.bs | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 172 insertions(+), 11 deletions(-) diff --git a/index.bs b/index.bs index 48c44a1..c7a42b9 100644 --- a/index.bs +++ b/index.bs @@ -164,10 +164,9 @@ A tool definition is a [=struct=] with the following [=struct/items=] [[!JSON-SCHEMA]] : execute steps - :: a set of steps to invoke the tool. + :: an algorithm that takes both a [=string=] and an algorithm completionSteps that itself takes a [=string=]-or-null and a [=boolean=]. - Note: For tools registered imperatively, these steps will simply invoke the supplied - {{ToolExecuteCallback}} callback. For tools registered + Note: For tools registered imperatively, these steps will simply invoke the [=imperative execute steps=]. For tools registered [declaratively](https://github.com/webmachinelearning/webmcp/pull/76), this will be a set of "internal" steps that have not been defined yet, that describe how to fill out a <{form}> and its [=form-associated elements=]. @@ -257,6 +256,38 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin +
+The imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Window}} +|targetWindow|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows: + +1. [=Assert=]: these steps are running on |targetWindow|'s [=relevant agent=]'s [=agent/event + loop=]. + +1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given + |inputArguments| and |targetWindow|'s [=relevant realm=]. If exception was thrown, then + run |completionSteps| given null and false, and abort these steps. + + Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}. + +1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null and false, and abort these steps. + + Issue(#146): Specify and fire the "toolactivated" event. + +1. Let |toolPromise| be the result of [=invoke|invoking=] |tool|'s {{ModelContextTool/execute}} with + |inputObject|. + +1. [=promise/React=] to |toolPromise|: + + - If |toolPromise| was fulfilled with value |v|: + + 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps. + + 1. Run |completionSteps| given |serializedResult| and true. + + - If |toolPromise| was rejected with reason r, then run |completionSteps| given null and false. + +
+
To unregister a tool given a {{ModelContext}} |modelContext| and a [=string=] |tool name|, run these steps: @@ -323,6 +354,7 @@ The {{ModelContext}} interface provides methods for web applications to register interface ModelContext : EventTarget { Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {}); Promise> getTools(optional ModelContextGetToolOptions options = {}); + Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {}); attribute EventHandler ontoolchange; }; @@ -348,6 +380,12 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}. agents written in JavaScript, and possibly living in <{iframe}>s. The [=user agent=]'s [=browser agent=] uses a different internal mechanism to retrieve the tools exposed to it.

+ +
document.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}
+
+

Executes a tool on the document it was registered on. Returns a promise that resolves to the + result of the tool's execution.

+
@@ -451,7 +489,8 @@ The registerTool(tool, optionsregisterTool(tool, optionsgetTools(options) method steps
+
+The executeTool(tool, inputArguments, +options) method steps are: + +1. Let |invokingDocument| be [=this=]'s [=relevant global object=]'s [=associated + Document|associated Document=]. + +1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=] + an "{{InvalidStateError}}" {{DOMException}}. + +1. If this's [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and this's + [=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not + "file", then return [=a promise rejected with=] a "{{SecurityError}}" + {{DOMException}}. + +1. If |invokingDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise + rejected with=] a "{{NotAllowedError}}" {{DOMException}}. + +1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s + {{RegisteredTool/origin}}. + +1. If |expectedTargetOriginURL| is failure, then return [=a promise rejected with=] a + "{{DataError}}" {{DOMException}}. + +1. Let |expectedTargetOrigin| be |expectedTargetOriginURL|'s [=url/origin=]. + +1. [=Assert=]: |expectedTargetOrigin| is not an [=opaque origin=]. + +1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=]. + +1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null. + +1. If |signal| is not null, then: + + 1. If |signal| is [=AbortSignal/aborted=], then return [=a promise rejected with=] |signal|'s + [=AbortSignal/abort reason=]. + + 1. [=AbortSignal/add|Add the following abort steps=] to |signal|: + + 1. [=Reject=] |promise| with |signal|'s [=AbortSignal/abort reason=]. + + Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so that tool abort is communicated all the way through. + +1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}. + +1. Let |targetDocument| be |targetWindow|'s [=associated Document|associated + Document=]. + +1. Run the following steps [=in parallel=]: + + 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=] is not + |invokingDocument|'s [=node navigable=]'s [=navigable/traversable navigable=], then [=queue a + global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global + object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these + steps. + + Issue: TODO(domfarolino): File an issue about cross-frame-tree, same-browsing-context-group + tool execution. + + 1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=]. + + 1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=]. + + 1. If |targetOrigin| is not [=same origin=] with |expectedTargetOrigin|, then [=queue a global + task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to + [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. + + Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. + + 1. Let |targetToolMap| be |targetDocument|'s [=Document/associated ModelContext|associated + ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. + + 1. Let |toolName| be |tool|'s {{RegisteredTool/name}}. + + 1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the + [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] + |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. + + Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. + + 1. Let |tool definition| be |targetToolMap|[|toolName|]. + + 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool + definition/exposed origins=], and |callerOrigin| is false, then [=queue a global task=] on the + [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] + |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. + + Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. + + 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps: + + 1. [=Assert=]: these steps are running [=in parallel=]. + + 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|. + + 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}. + + 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run |tool + definition|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|. + +1. Return |promise|. + +
+

ModelContextTool Dictionary

The {{ModelContextTool}} dictionary describes a tool that can be invoked by [=agents=]. @@ -617,19 +760,21 @@ callback ToolExecuteCallback = Promise (object input);
tool["{{ModelContextTool/name}}"]
-

A unique identifier for the tool. This is used by [=agents=] to reference the tool when making tool calls. +

A unique identifier for the tool. This is used by [=agents=] to reference the tool when + making tool calls.

tool["{{ModelContextTool/title}}"]
-

A label for the tool. This is used by the user agent to reference the tool in the user interface. -

It is recommended that this string be localized to the user's -{{NavigatorLanguage/language}}. +

A label for the tool. This is used by the user agent to reference the tool in the user + interface.

It is recommended that this string be localized to the user's + {{NavigatorLanguage/language}}.

tool["{{ModelContextTool/description}}"]
-

A natural language description of the tool's functionality. This helps [=agents=] understand when and how to use the tool. +

A natural language description of the tool's functionality. This helps [=agents=] understand + when and how to use the tool.

tool["{{ModelContextTool/inputSchema}}"]
@@ -700,6 +845,22 @@ dictionary ModelContextGetToolOptions { same-origin documents.
+

ExecuteToolOptions Dictionary

+ +The {{ExecuteToolOptions}} dictionary allows web applications to pass options to +{{ModelContext/executeTool()}}. + + +dictionary ExecuteToolOptions { + AbortSignal signal; +}; + + +
+ : options["{{ExecuteToolOptions/signal}}"] + :: An {{AbortSignal}} that can be used to cancel the execution of the tool. +
+

RegisteredTool Dictionary

The {{RegisteredTool}} dictionary represents a tool that has been registered and is available for From 90ccdfa2db714dc610d2ce87f6b7393310b7b453 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Tue, 21 Jul 2026 19:59:20 -0400 Subject: [PATCH 02/12] Remove spec issue now that #227 is filed --- index.bs | 3 --- 1 file changed, 3 deletions(-) diff --git a/index.bs b/index.bs index c7a42b9..13081bf 100644 --- a/index.bs +++ b/index.bs @@ -685,9 +685,6 @@ The executeTool(tool, inputArgument object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. - Issue: TODO(domfarolino): File an issue about cross-frame-tree, same-browsing-context-group - tool execution. - 1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=]. 1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=]. From f9ef39efa61968296d800c711dd00c24b939bf44 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 09:12:08 -0400 Subject: [PATCH 03/12] Break tool execute steps out; perform on-loop existence check, and document race --- index.bs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/index.bs b/index.bs index 13081bf..cbdd980 100644 --- a/index.bs +++ b/index.bs @@ -257,15 +257,73 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin
-The imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Window}} -|targetWindow|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows: +The tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|, +a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows. The +|completionSteps| algorithm takes a [=string=]-or-null result and a [=boolean=] success. + +1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=]. + +1. Let |toolMap| be |targetDocument|'s [=Document/associated ModelContext|associated + ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. + +1. If |toolMap|[|toolName|] does not [=map/exist=], then run |completionSteps| given null and false, + and abort these steps. + + Issue: Support the plumbing of more granular errors back to the invoker; this should result in a + "{{NotFoundError}}" in the invoking document. + +
+

This protects us against a race between tool unregistration and execution. While tool + existence is protected from this race, tool unregistration followed by a quick + re-registration of a tool with the same |toolName| but input schema is not + protected against.

+ +

This might result in |inputArguments| for an old tool being applied to the + [=tool definition/input schema=] of a newer tool, and causing whatever error that might cause, + when issue #92 is + resolved.

+ +
+ + // -- Tool owner document. -- + const oldInputSchema = {...}; + const newInputSchema = {...}; + const ac = new AbortController(); + document.modelContext.registerTool({..., inputSchema: oldInputSchema}, {signal: ac.signal}); + + // Unregister, and quickly re-register with an updated input schema. + ac.abort(); + document.modelContext.registerTool({..., inputSchema: newInputSchema}); + + + // -- Executing document. -- + // + // This could target either the "old" tool, or the "new" one above, + // and the execution might encounter any requisite errors due to the mismatch. + const [tool] = await document.modelContext.getTools(); + document.modelContext.executeTool(tool, "{a: 10}"); + +
+
+ +1. Let |tool| be |toolMap|[|toolName|]. + +1. Run |tool|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|. + + Note: This is the point where we branch into either the [=imperative execute steps=] or the [=declarative execute steps=]. + +
-1. [=Assert=]: these steps are running on |targetWindow|'s [=relevant agent=]'s [=agent/event +
+The imperative execute steps, given a {{ModelContextTool}} |tool|, a {{Document}} |targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows: + +1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=]. 1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given - |inputArguments| and |targetWindow|'s [=relevant realm=]. If exception was thrown, then - run |completionSteps| given null and false, and abort these steps. + |inputArguments| and |targetDocument|'s [=relevant realm=]. If exception was thrown, then run |completionSteps| given null and false, and abort + these steps. Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}. @@ -489,8 +547,7 @@ The registerTool(tool, optionsexecuteTool(tool, inputArgument 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}. - 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run |tool - definition|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|. + 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|. 1. Return |promise|. @@ -950,6 +1006,12 @@ The synthesize a declarative JSON Schema object algorithm, given a <{ } +
+The declarative execute steps are as follows. + +domfarolino +
+

Events

The following are the [=event handlers=] (and their corresponding [=event handler event types=]) From c968f7988cc5313f37ef6c209b13e7db4e1934a8 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 14:18:55 +0000 Subject: [PATCH 04/12] bf-cache note and cross-BCG note --- index.bs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/index.bs b/index.bs index cbdd980..babb083 100644 --- a/index.bs +++ b/index.bs @@ -96,6 +96,9 @@ spec:html; type:dfn; text:is origin-keyed
+spec: html; urlPrefix: https://html.spec.whatwg.org/C
+  type: dfn
+    text: browsing context group
 spec: SECURE-CONTEXTS; urlPrefix: https://w3c.github.io/webappsec-secure-contexts/
   type: abstract-op
     text: is origin potentially trustworthy?; url: is-origin-trustworthy
@@ -742,6 +745,9 @@ The executeTool(tool, inputArgument
       object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these
       steps.
 
+      Issue(#227): Consider supporting tool execution across top-level documents in the same
+      [=browsing context group=].
+
    1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=].
 
    1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=].
@@ -782,6 +788,11 @@ The executeTool(tool, inputArgument
 
    1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|.
 
+      Note: Because documents only process tasks on their event loops when [=Document/fully
+      active=], if |targetDocument| is not [=Document/fully active=], this will simply queue the
+      steps to execute the tool, to run when the document finally becomes active again (i.e., when
+      it leaves the bf-cache).
+
 1. Return |promise|.
 
 
From 1d6b4bdaa289f38566759cf2d2bca9e9a9a76849 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 14:27:57 +0000 Subject: [PATCH 05/12] Default webidl spec --- index.bs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.bs b/index.bs index babb083..991ad81 100644 --- a/index.bs +++ b/index.bs @@ -94,6 +94,8 @@ spec:html; type:dfn; text:browsing context group set text:unique internal value text:is origin-keyed +spec: webidl; type: dfn; + text: an exception was thrown
 spec: html; urlPrefix: https://html.spec.whatwg.org/C
@@ -324,7 +326,7 @@ The imperative execute steps, given a {{ModelContextTool}} |tool|, a
    loop=].
 
 1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given
-   |inputArguments| and |targetDocument|'s [=relevant realm=]. If exception was thrown, then run |completionSteps| given null and false, and abort
    these steps.
 

From fda9bde1a7c960a3644046d66dea3000623d7abb Mon Sep 17 00:00:00 2001
From: Dominic Farolino 
Date: Mon, 27 Jul 2026 15:13:06 +0000
Subject: [PATCH 06/12] Formalize this

---
 index.bs | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/index.bs b/index.bs
index 991ad81..a0124d5 100644
--- a/index.bs
+++ b/index.bs
@@ -417,7 +417,7 @@ The {{ModelContext}} interface provides methods for web applications to register
 interface ModelContext : EventTarget {
   Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
   Promise> getTools(optional ModelContextGetToolOptions options = {});
-  Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ExecuteToolOptions options = {});
+  Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {});
 
   attribute EventHandler ontoolchange;
 };
@@ -701,7 +701,7 @@ The executeTool(tool, inputArgument
 1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=]
    an "{{InvalidStateError}}" {{DOMException}}.
 
-1. If this's [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and this's
+1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and [=this=]'s
    [=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not
    "file", then return [=a promise rejected with=] a "{{SecurityError}}"
    {{DOMException}}.
@@ -721,9 +721,9 @@ The executeTool(tool, inputArgument
 
 1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].
 
-1. Let |signal| be |options|'s {{ExecuteToolOptions/signal}} if it [=map/exists=]; otherwise null.
+1. If |options|'s {{ModelContextExecuteToolOptions/signal}} [=map/exists=], then:
 
-1. If |signal| is not null, then:
+   1. Let |signal| be |options|'s {{ModelContextExecuteToolOptions/signal}}.
 
    1. If |signal| is [=AbortSignal/aborted=], then return [=a promise rejected with=] |signal|'s
       [=AbortSignal/abort reason=].
@@ -911,19 +911,18 @@ dictionary ModelContextGetToolOptions {
      same-origin documents.
 
 
-

ExecuteToolOptions Dictionary

+

ModelContextExecuteToolOptions Dictionary

-The {{ExecuteToolOptions}} dictionary allows web applications to pass options to -{{ModelContext/executeTool()}}. +The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to {{ModelContext/executeTool()}}. -dictionary ExecuteToolOptions { +dictionary ModelContextExecuteToolOptions { AbortSignal signal; };
- : options["{{ExecuteToolOptions/signal}}"] + : options["{{ModelContextExecuteToolOptions/signal}}"] :: An {{AbortSignal}} that can be used to cancel the execution of the tool.
From e10a9f208ba9c41ff44d3912124698069c1f5779 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 15:39:44 +0000 Subject: [PATCH 07/12] Fix origin errors --- index.bs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/index.bs b/index.bs index a0124d5..0c28b39 100644 --- a/index.bs +++ b/index.bs @@ -446,8 +446,7 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}.
document.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}
-

Executes a tool on the document it was registered on. Returns a promise that resolves to the - result of the tool's execution.

+

Executes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.

@@ -712,8 +711,9 @@ The executeTool(tool, inputArgument 1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s {{RegisteredTool/origin}}. -1. If |expectedTargetOriginURL| is failure, then return [=a promise rejected with=] a - "{{DataError}}" {{DOMException}}. +1. If |expectedTargetOriginURL| is failure, or |expectedTargetOriginURL|'s [=url/origin=] is an + [=opaque origin=], then return [=a promise rejected with=] a "{{NotSupportedError}}" + {{DOMException}}. 1. Let |expectedTargetOrigin| be |expectedTargetOriginURL|'s [=url/origin=]. @@ -750,6 +750,8 @@ The executeTool(tool, inputArgument Issue(#227): Consider supporting tool execution across top-level documents in the same [=browsing context group=]. + Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. + 1. Let |targetOrigin| be |targetDocument|'s [=Document/origin=]. 1. Let |callerOrigin| be |invokingDocument|'s [=Document/origin=]. @@ -774,9 +776,9 @@ The executeTool(tool, inputArgument 1. Let |tool definition| be |targetToolMap|[|toolName|]. 1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool - definition/exposed origins=], and |callerOrigin| is false, then [=queue a global task=] on the - [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] - |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. + definition/exposed origins=], and |callerOrigin| returns false, then [=queue a global task=] + on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to + [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}, and abort these steps. Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. From d6ce1bb30a59ce0c70a040c8099fa776204afa2c Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 11:57:59 -0400 Subject: [PATCH 08/12] Report a warning --- index.bs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/index.bs b/index.bs index 0c28b39..3907e76 100644 --- a/index.bs +++ b/index.bs @@ -343,11 +343,15 @@ The imperative execute steps, given a {{ModelContextTool}} |tool|, a - If |toolPromise| was fulfilled with value |v|: - 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps. + 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps. - 1. Run |completionSteps| given |serializedResult| and true. + 1. Run |completionSteps| given |serializedResult| and true. - - If |toolPromise| was rejected with reason r, then run |completionSteps| given null and false. + - If |toolPromise| was rejected with reason |r|, then: + + 1. Optionally [=report a warning to the console=] describing |r|. + + 1. Run |completionSteps| given null and false. From 2f3688b4e9599738059fab56c38770a02f2ad646 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 14:00:16 -0400 Subject: [PATCH 09/12] Reference toolcanceled event --- index.bs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 3907e76..46ca1a8 100644 --- a/index.bs +++ b/index.bs @@ -736,7 +736,10 @@ The executeTool(tool, inputArgument 1. [=Reject=] |promise| with |signal|'s [=AbortSignal/abort reason=]. - Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so that tool abort is communicated all the way through. + Issue(#48): Wire up |signal| to the tool execution callback in the tool host's document, so + that tool abort is communicated all the way through. This should also fire the + "toolcanceled" event in the target document. See also issue #146. 1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}. From 8828678d135b7cf9832f00c764f6202d344ab74f Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Mon, 27 Jul 2026 14:32:26 -0400 Subject: [PATCH 10/12] Don't let DOMString be null --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index 46ca1a8..12a59a4 100644 --- a/index.bs +++ b/index.bs @@ -421,7 +421,7 @@ The {{ModelContext}} interface provides methods for web applications to register interface ModelContext : EventTarget { Promise registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {}); Promise> getTools(optional ModelContextGetToolOptions options = {}); - Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {}); + Promise executeTool(RegisteredTool tool, DOMString inputArguments, optional ModelContextExecuteToolOptions options = {}); attribute EventHandler ontoolchange; }; From eea098a999a25657037793738acc8ab7f3780fea Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Tue, 28 Jul 2026 16:11:32 +0000 Subject: [PATCH 11/12] Spec the full pending execution cancellation infrastructure --- index.bs | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 5 deletions(-) diff --git a/index.bs b/index.bs index 12a59a4..1abf191 100644 --- a/index.bs +++ b/index.bs @@ -193,6 +193,76 @@ An annotations is a [=struct=] with the following [=struct/items=]: :: a [=boolean=], initially false. +

Pending tool executions

+ +A pending tool execution is a [=struct=] with the following [=struct/items=]: + +
+ : caller document + :: a {{Document}}. + + : target document + :: a {{Document}}. + + : tool name + :: a [=string=]. + + : completion steps + :: an algorithm that takes a [=string=]-or-null and a [=boolean=]. +
+ +A [=traversable navigable=] has a pending tool executions +map, which is a [=map=] whose keys are [=unique internal values=] and whose values are +[=pending tool execution=] [=structs=]. It is initially empty. + +Note: This map is only ever mutated from steps [=in parallel=]. This simulates the single, +authoritative "browser process" that most modern browsers implement, where execution tracking sits +outside any individual Document process's event loop, and is accessed asynchronously via some +inter-process communication mechanism. + +
+ +
+This specification's [=unloading document cleanup steps=], given a {{Document}} |document|, are as +follows: + +1. Let |traversable| be |document|'s [=node navigable=]'s [=navigable/traversable navigable=]. + +1. Run the following steps [=in parallel=]: + + 1. Let |executionsToRemove| be an empty [=list=]. + + 1. [=map/For each=] |uuid| → |execution| of |traversable|'s [=traversable navigable/pending + tool executions map=]: + + 1. If |document| is |execution|'s [=pending tool execution/target document=] or |document| is + |execution|'s [=pending tool execution/caller document=], then [=list/append=] |uuid| to + |executionsToRemove|. + + 1. [=list/For each=] |uuid| of |executionsToRemove|: + + 1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions + map=][|uuid|]. + + 1.

If |document| is |execution|'s [=pending tool + execution/target document=], then run |execution|'s [=pending tool execution/completion + steps=] given null and false.

+ + Note: This removes |execution| from the [=traversable navigable/pending tool executions + map=]. + + 1. If |document| is |execution|'s [=pending tool execution/caller document=], then: + + 1. [=map/Remove=] |traversable|'s [=traversable navigable/pending tool executions + map=][|uuid|]. + + 1. Issue(#48): Abort the `AbortSignal` in |execution|'s [=pending tool execution/target + document=], so the tool can abort early now that the caller is dead. + + 1. [=Assert=]: |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|] + does not [=map/exist=]. + +

@@ -288,7 +358,7 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow when issue #92 is resolved.

-
+
// -- Tool owner document. -- const oldInputSchema = {...}; @@ -371,10 +441,33 @@ To <dfn for="model context">unregister a tool</dfn> given a {{ModelContext}} |mo 1. [=map/Remove=] |tool map|[|tool name|]. +1. Let |targetDocument| be |modelContext|'s [=relevant global object=]'s [=associated + Document|associated <code>Document</code>=]. + +1. Let |traversable| be |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]. + 1. Run the following steps [=in parallel=]: - 1. [=Notify documents of a tool change=] given |modelContext|'s [=relevant global object=]'s - [=associated Document|associated <code>Document</code>=] and |exposed origins|. + 1. Let |canceledExecutions| be an empty [=list=]. + + 1. [=map/For each=] |uuid| &rarr; |execution| of |traversable|'s [=traversable navigable/pending + tool executions map=]: + + 1. If |execution|'s [=pending tool execution/target document=] is |targetDocument| and + |execution|'s [=pending tool execution/tool name=] is |tool name|, then [=list/append=] + |uuid| to |canceledExecutions|. + + 1. [=list/For each=] |uuid| of |canceledExecutions|: + + 1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions + map=][|uuid|]. + + 1. Run |execution|'s [=pending tool execution/completion steps=] given null and false. + + Note: This removes |execution| from the [=traversable navigable/pending tool executions + map=]. + + 1. [=Notify documents of a tool change=] given |targetDocument| and |exposed origins|. </div> @@ -789,14 +882,67 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArgument Issue: Support more granular errors than "{{UnknownError}}", based on each failure case. + 1. Let |uuid| be a new [=unique internal value=]. + 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps: 1. [=Assert=]: these steps are running [=in parallel=]. + 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then return. + + <div class=note> + <p>It is possible that a pending execution identified by |uuid| no longer exists. This can + happen due to a race between (a) tool cancellation when the caller document <a + href=#caller-destroyed-cleanup>gets destroyed</a>; and (b) tool promise resolution. Both + of these race to invoke |completionSteps|, and the first invocation will remove the + pending execution by its key |uuid|, this check protects subsequent racing invocations.</p> + + <p>This can also happen due to a limitation with tool unregistration. Consider this + example:</p> + + <div class=example id=unregistration-race> + <xmp class=language-js> + const ac = new AbortController(); + document.modelContext.registerTool({ + name: "tool" + description: "a racy tool", + execute: async () => { + return new Promise(resolve => { + resolve("tool result"); + ac.abort(); + }); + } + }, {signal: ac.signal}); + + +

In this case, tool unregistration happens after a tool Promise is resolved, but before + its fulfillment handler runs. Since both run |completionSteps|, this check protects the + second invocation from trying to remove the same pending execution twice.

+
+
+ + 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|]. + 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|. 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}. + 1. Let |execution| be a new [=pending tool execution=], with the following [=struct/items=]: + + : [=pending tool execution/caller document=] + :: |invokingDocument| + + : [=pending tool execution/target document=] + :: |targetDocument| + + : [=pending tool execution/tool name=] + :: |toolName| + + : [=pending tool execution/completion steps=] + :: |completionSteps| + + 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] to |execution|. + 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|. Note: Because documents only process tasks on their event loops when [=Document/fully @@ -1028,9 +1174,10 @@ The synthesize a declarative JSON Schema object algorithm, given a <{
-The declarative execute steps are as follows. +The declarative execute steps are as follows: + +Issue: Spec the declarative execution steps, and their integration with form elements. -domfarolino

Events

From 381b37654797159d09a46c93d42b26a686c4b996 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Tue, 28 Jul 2026 12:15:05 -0400 Subject: [PATCH 12/12] specfmt --- index.bs | 91 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/index.bs b/index.bs index 1abf191..e884548 100644 --- a/index.bs +++ b/index.bs @@ -169,9 +169,11 @@ A tool definition is a [=struct=] with the following [=struct/items=] [[!JSON-SCHEMA]] : execute steps - :: an algorithm that takes both a [=string=] and an algorithm completionSteps that itself takes a [=string=]-or-null and a [=boolean=]. + :: an algorithm that takes both a [=string=] and an algorithm completionSteps + that itself takes a [=string=]-or-null and a [=boolean=]. - Note: For tools registered imperatively, these steps will simply invoke the [=imperative execute steps=]. For tools registered + Note: For tools registered imperatively, these steps will simply invoke the [=imperative + execute steps=]. For tools registered [declaratively](https://github.com/webmachinelearning/webmcp/pull/76), this will be a set of "internal" steps that have not been defined yet, that describe how to fill out a <{form}> and its [=form-associated elements=]. @@ -332,11 +334,13 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin
-The tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|, -a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows. The -|completionSteps| algorithm takes a [=string=]-or-null result and a [=boolean=] success. +The tool execute steps, given a [=string=] |toolName|, a {{Document}} |targetDocument|, a +[=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows. The +|completionSteps| algorithm takes a [=string=]-or-null result and a [=boolean=] +success. -1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=]. +1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event + loop=]. 1. Let |toolMap| be |targetDocument|'s [=Document/associated ModelContext|associated ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=]. @@ -350,13 +354,12 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow

This protects us against a race between tool unregistration and execution. While tool existence is protected from this race, tool unregistration followed by a quick - re-registration of a tool with the same |toolName| but input schema is not - protected against.

+ re-registration of a tool with the same |toolName| but input schema is not protected + against.

-

This might result in |inputArguments| for an old tool being applied to the - [=tool definition/input schema=] of a newer tool, and causing whatever error that might cause, - when issue #92 is - resolved.

+

This might result in |inputArguments| for an old tool being applied to the [=tool + definition/input schema=] of a newer tool, and causing whatever error that might cause, when issue #92 is resolved.

@@ -385,24 +388,28 @@ a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follow 1. Run |tool|'s [=tool definition/execute steps=] given |inputArguments| and |completionSteps|. - Note: This is the point where we branch into either the [=imperative execute steps=] or the [=declarative execute steps=]. + Note: This is the point where we branch into either the [=imperative execute steps=] or the + [=declarative execute steps=]. </div> <div algorithm> -The <dfn>imperative execute steps</dfn>, given a {{ModelContextTool}} |tool|, a {{Document}} |targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows: +The <dfn>imperative execute steps</dfn>, given a {{ModelContextTool}} |tool|, a {{Document}} +|targetDocument|, a [=string=] |inputArguments|, and an algorithm |completionSteps|, are as follows: 1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event loop=]. 1. Let |inputObject| be the result of [=parse a JSON string to a JavaScript value=] given - |inputArguments| and |targetDocument|'s [=relevant realm=]. If <a lt="an exception - was thrown">exception was thrown</a>, then run |completionSteps| given null and false, and abort + |inputArguments| and |targetDocument|'s [=relevant realm=]. If <a lt="an exception was + thrown">exception was thrown</a>, then run |completionSteps| given null and false, and abort these steps. - Issue: Support more granular errors; here we should return something that prompts the caller to reject its {{Promise}} with a "{{DataError}}" {{DOMException}}. + Issue: Support more granular errors; here we should return something that prompts the caller to + reject its {{Promise}} with a "{{DataError}}" {{DOMException}}. -1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null and false, and abort these steps. +1. If |inputObject| [=Object type|is not an Object=] is false, then run |completionSteps| given null + and false, and abort these steps. Issue(#146): Specify and fire the "<code>toolactivated</code>" event. @@ -413,7 +420,9 @@ The <dfn>imperative execute steps</dfn>, given a {{ModelContextTool}} |tool|, a - If |toolPromise| was fulfilled with value |v|: - 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON string=] given |v|. If this throws an exception, run |completionSteps| given null and false, and abort these steps. + 1. Let |serializedResult| be the result of [=serializing a JavaScript value to a JSON + string=] given |v|. If this throws an exception, run |completionSteps| given null and + false, and abort these steps. 1. Run |completionSteps| given |serializedResult| and true. @@ -543,7 +552,8 @@ is a [=model context=] [=struct=] created alongside the {{ModelContext}}. <dt><code><var ignore>document</var>.{{Document/modelContext}}.{{ModelContext/executeTool(tool, inputArguments, options)}}</code></dt> <dd> - <p>Executes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.</p> + <p>Executes a tool on the document it was registered on. Returns a promise that resolves to the + stringified result of the tool's execution.</p> </dd> </dl> @@ -648,7 +658,9 @@ The <dfn method for=ModelContext>registerTool(<var>tool</var>, <var>options</var :: |stringified input schema| : [=tool definition/execute steps=] - :: An algorithm that take a [=string=] |inputArguments| and an algorithm |completionSteps| and runs the [=imperative execute steps=] given |tool|, |tool owner|, |inputArguments|, and |completionSteps|. + :: An algorithm that take a [=string=] |inputArguments| and an algorithm |completionSteps| and + runs the [=imperative execute steps=] given |tool|, |tool owner|, |inputArguments|, and + |completionSteps|. : [=tool definition/annotations=] :: null if |tool|'s {{ModelContextTool/annotations}} does not [=map/exist=]. Otherwise, an @@ -797,10 +809,10 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArgument 1. If |invokingDocument| is not [=Document/fully active=], then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}. -1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and [=this=]'s - [=relevant settings object=]'s [=environment settings object/origin=]'s [=origin/scheme=] is not - "<code>file</code>", then return [=a promise rejected with=] a "{{SecurityError}}" - {{DOMException}}. +1. If [=this=]'s [=surrounding agent=]'s [=agent cluster=]'s [=is origin-keyed=] is false and + [=this=]'s [=relevant settings object=]'s [=environment settings object/origin=]'s + [=origin/scheme=] is not "<code>file</code>", then return [=a promise rejected with=] a + "{{SecurityError}}" {{DOMException}}. 1. If |invokingDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise rejected with=] a "{{NotAllowedError}}" {{DOMException}}. @@ -884,18 +896,22 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArgument 1. Let |uuid| be a new [=unique internal value=]. - 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a [=boolean=] |success|, and runs the following steps: + 1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a + [=boolean=] |success|, and runs the following steps: 1. [=Assert=]: these steps are running [=in parallel=]. - 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then return. + 1. If |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s + [=traversable navigable/pending tool executions map=][|uuid|] does not [=map/exist=], then + return. <div class=note> <p>It is possible that a pending execution identified by |uuid| no longer exists. This can happen due to a race between (a) tool cancellation when the caller document <a href=#caller-destroyed-cleanup>gets destroyed</a>; and (b) tool promise resolution. Both of these race to invoke |completionSteps|, and the first invocation will remove the - pending execution by its key |uuid|, this check protects subsequent racing invocations.</p> + pending execution by its key |uuid|, this check protects subsequent racing + invocations.</p> <p>This can also happen due to a limitation with tool unregistration. Consider this example:</p> @@ -921,11 +937,15 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArgument </div> </div> - 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|]. + 1. [=map/Remove=] |targetDocument|'s [=node navigable=]'s [=navigable/traversable + navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|]. - 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|. + 1. If |success| is true, then [=queue a global task=] on the [=webmcp task source=] given + |invokingDocument|'s [=relevant global object=] to [=resolve=] |promise| with |result|. - 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" {{DOMException}}. + 1. Otherwise, [=queue a global task=] on the [=webmcp task source=] given |invokingDocument|'s + [=relevant global object=] to [=reject=] |promise| with an "{{UnknownError}}" + {{DOMException}}. 1. Let |execution| be a new [=pending tool execution=], with the following [=struct/items=]: @@ -941,9 +961,11 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputArgument : [=pending tool execution/completion steps=] :: |completionSteps| - 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s [=traversable navigable/pending tool executions map=][|uuid|] to |execution|. + 1. Set |targetDocument|'s [=node navigable=]'s [=navigable/traversable navigable=]'s + [=traversable navigable/pending tool executions map=][|uuid|] to |execution|. - 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|. + 1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool + execute steps=] given |toolName|, |targetDocument|, |inputArguments|, and |completionSteps|. Note: Because documents only process tasks on their event loops when [=Document/fully active=], if |targetDocument| is not [=Document/fully active=], this will simply queue the @@ -1068,7 +1090,8 @@ dictionary ModelContextGetToolOptions { <h4 id="model-context-execute-tool-options">ModelContextExecuteToolOptions Dictionary</h4> -The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to {{ModelContext/executeTool()}}. +The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to +{{ModelContext/executeTool()}}. <xmp class="idl"> dictionary ModelContextExecuteToolOptions {