Skip to content
192 changes: 155 additions & 37 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,18 @@ A <dfn>model context</dfn> is a [=struct=] with the following [=struct/items=]:
: <dfn>tool map</dfn>
:: a [=map=] whose [=map/keys=] are [=strings=] and whose [=map/values=] are [=tool definition=]
[=structs=].
</dl>

: <dfn>local pending tool executions map</dfn>
:: a [=map=] whose [=map/keys=] are [=unique internal values=] and whose [=map/values=] are
[=local pending tool execution=] [=structs=]. It is initially empty.

Note: this map is similar to a [=traversable navigable=]'s [=traversable navigable/pending tool
executions map=], but it only contains pending execution information for tools under a single
{{ModelContext}} object. It is used to store objects that can only be accessed from that
object's event loop, and because it is event-loop-local, it can get out of sync from the
[=traversable navigable=]'s more "global" map.

</dl>

A <dfn>tool definition</dfn> is a [=struct=] with the following [=struct/items=]:

Expand Down Expand Up @@ -170,8 +180,8 @@ A <dfn>tool definition</dfn> is a [=struct=] with the following [=struct/items=]

: <dfn>execute steps</dfn>
Comment thread
domfarolino marked this conversation as resolved.
:: an algorithm that takes a {{Document}} <var ignore>targetDocument</var>, a [=string=] <var
ignore>inputArguments</var>, and an algorithm <var ignore>completionSteps</var> that takes a
[=string=]-or-null and a [=boolean=].
ignore>inputArguments</var>, an algorithm <var ignore>completionSteps</var> that takes a
[=string=]-or-null and a [=boolean=], and a [=unique internal value=] <var ignore>uuid</var>.

Note: For tools registered imperatively, these steps will simply invoke the [=imperative
execute steps=]. For tools registered
Expand All @@ -186,6 +196,13 @@ A <dfn>tool definition</dfn> is a [=struct=] with the following [=struct/items=]
:: a [=list=] or [=origins=], initially [=list/empty=].
</dl>

A <dfn>local pending tool execution</dfn> is a [=struct=] with the following [=struct/items=]:

<dl dfn-for="local pending tool execution">
: <dfn>abort controller</dfn>
:: an {{AbortController}}.
</dl>

An <dfn>annotations</dfn> is a [=struct=] with the following [=struct/items=]:

<dl dfn-for="annotations">
Expand Down Expand Up @@ -223,6 +240,53 @@ authoritative "browser process" that most modern browsers implement, where execu
outside any individual Document process's event loop, and is accessed asynchronously via some
inter-process communication mechanism.

<div algorithm>
To <dfn>cancel a pending tool execution</dfn> given a [=traversable navigable=] |traversable| and a
[=unique internal value=] |uuid|:

1. [=Assert=]: these steps are running [=in parallel=].

1. If |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|] does not
[=map/exist=], then return.

Note: See <a href=#unregistration-race>this note</a> to learn how a tool's natural
resolution/rejection can race with the caller's cancellation. This might result in the pending
execution entry for |uuid| being removed before we get here. In that case, the
{{ModelContext/executeTool()}} promise will still be rejected with the abort
[=AbortSignal/abort reason=], and will never observe the tool's natural resolution/rejection.

1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|].

1. [=map/Remove=] |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|].

1. Let |targetDocument| be |execution|'s [=pending tool execution/target document=].

Note: |targetDocument| is guaranteed to still exist (i.e., not be unloaded or destroyed) when
these steps run, because if |targetDocument| had been destroyed, then <a
href=#target-destroyed-cleanup>this specification's unloading document cleanup steps</a> would
have already removed |execution| from the map, and we'd have ended up in the early return path
above.

1. [=Queue a global task=] on the [=webmcp task source=] given |targetDocument|'s [=relevant global
object=] to run the following steps:

1. Let |localExecutions| be |targetDocument|'s [=Document/associated ModelContext|associated
<code>ModelContext</code>=]'s [=ModelContext/internal context=]'s [=model context/local
pending tool executions map=].

1. If |localExecutions|[|uuid|] does not [=map/exist=], then return.

1. Let |localExecution| be |localExecutions|[|uuid|].

1. [=map/Remove=] |localExecutions|[|uuid|].

1. [=AbortController/signal abort|Signal abort=] on |localExecution|'s [=local pending tool
execution/abort controller=].

Issue(#146): Fire the "toolcanceled" event at |targetDocument|'s relevant global object.

</div>

<hr>

<div algorithm=unloading-document-cleanup-steps>
Expand All @@ -247,20 +311,20 @@ follows:
1. Let |execution| be |traversable|'s [=traversable navigable/pending tool executions
map=][|uuid|].

1. <p id=caller-destroyed-cleanup>If |document| is |execution|'s [=pending tool
execution/target document=], then run |execution|'s [=pending tool execution/completion
steps=] given null and false.</p>
1. <p id=target-destroyed-cleanup>If |document| is |execution|'s [=pending tool
execution/target document=] and is not |execution|'s [=pending tool execution/caller
document=], then run |execution|'s [=pending tool execution/completion steps=] given null
and false.</p>

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. <p id=caller-destroyed-cleanup>Otherwise, if |document| is |execution|'s [=pending tool
execution/caller document=] and is not |execution|'s [=pending tool execution/target
document=], then [=cancel a pending tool execution=] given |traversable| and |uuid|.</p>
Comment thread
domfarolino marked this conversation as resolved.

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. Otherwise, [=map/Remove=] |traversable|'s [=traversable navigable/pending tool executions
map=][|uuid|].

1. [=Assert=]: |traversable|'s [=traversable navigable/pending tool executions map=][|uuid|]
does not [=map/exist=].
Expand Down Expand Up @@ -336,9 +400,9 @@ a [=list=] of [=origins=] |exposed origins|, and an [=origin=] |accessing origin

<div algorithm>
The <dfn>tool execute steps</dfn>, 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 <var ignore>result</var> and a [=boolean=]
<var ignore>success</var>.
[=string=] |inputArguments|, an algorithm |completionSteps|, and a [=unique internal value=] |uuid|,
are as follows. The |completionSteps| algorithm takes a [=string=]-or-null <var ignore>result</var>
and a [=boolean=] <var ignore>success</var>.

1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event
loop=].
Expand Down Expand Up @@ -387,8 +451,8 @@ The <dfn>tool execute steps</dfn>, given a [=string=] |toolName|, a {{Document}}

1. Let |tool| be |toolMap|[|toolName|].

1. Run |tool|'s [=tool definition/execute steps=] given |targetDocument|, |inputArguments|, and
|completionSteps|.
1. Run |tool|'s [=tool definition/execute steps=] given |targetDocument|, |inputArguments|,
|completionSteps|, and |uuid|.

Note: This is the point where we branch into either the [=imperative execute steps=] or the
[=declarative execute steps=].
Expand All @@ -397,7 +461,8 @@ The <dfn>tool execute steps</dfn>, given a [=string=] |toolName|, a {{Document}}

<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:
|targetDocument|, a [=string=] |inputArguments|, an algorithm |completionSteps|, and a [=unique
internal value=] |uuid|, are as follows:

1. [=Assert=]: these steps are running on |targetDocument|'s [=relevant agent=]'s [=agent/event
loop=].
Expand All @@ -415,13 +480,41 @@ The <dfn>imperative execute steps</dfn>, given a {{ModelContextTool}} |tool|, a

Issue(#146): Specify and fire the "<code>toolactivated</code>" event.

1. Let |controller| be a [=new=] {{AbortController}} created in |targetDocument|'s [=relevant
realm=].

1. Let |localExecution| be a new [=local pending tool execution=] with the following [=struct/items=]:

: [=local pending tool execution/abort controller=]
:: |controller|

1. Set |targetDocument|'s [=Document/associated ModelContext|associated
<code>ModelContext</code>=]'s [=ModelContext/internal context=]'s [=model context/local pending
tool executions map=][|uuid|] to |localExecution|.
1. Let |options| be a new {{ToolExecuteCallbackOptions}} dictionary, with the following fields:

: {{ToolExecuteCallbackOptions/signal}}
:: |controller|'s [=AbortController/signal=]

1. Let |toolPromise| be the result of [=invoke|invoking=] |tool|'s {{ModelContextTool/execute}} with
|inputObject|.
|inputObject| and |options|.

1. [=promise/React=] to |toolPromise|:

- If |toolPromise| was fulfilled with value |v|:

1. Let |localExecutions| be |targetDocument|'s [=Document/associated ModelContext|associated
<code>ModelContext</code>=]'s [=ModelContext/internal context=]'s [=model context/local
pending tool executions map=].

1. If |localExecutions|[|uuid|] does not [=map/exist=], then return.

Note: The entry corresponding to |uuid| will not exist if the execution was [=cancel a
pending tool execution|cancelled=] (and thus the corresponding entry was removed) before
the developer's |toolPromise| settles.

1. [=map/Remove=] |localExecutions|[|uuid|].

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.
Expand All @@ -432,6 +525,14 @@ The <dfn>imperative execute steps</dfn>, given a {{ModelContextTool}} |tool|, a

1. Optionally [=report a warning to the console=] describing |r|.

1. Let |localExecutions| be |targetDocument|'s [=Document/associated ModelContext|associated
<code>ModelContext</code>=]'s [=ModelContext/internal context=]'s [=model context/local
pending tool executions map=].

1. If |localExecutions|[|uuid|] does not [=map/exist=], then return.

1. [=map/Remove=] |localExecutions|[|uuid|].

1. Run |completionSteps| given null and false.

</div>
Expand Down Expand Up @@ -664,9 +765,10 @@ The <dfn method for=ModelContext>registerTool(<var>tool</var>, <var>options</var
:: |stringified input schema|

: [=tool definition/execute steps=]
:: An algorithm that takes a {{Document}} |targetDocument|, a [=string=] |inputArguments|, and an
algorithm |completionSteps|, and runs the [=imperative execute steps=] given |tool|,
|targetDocument|, |inputArguments|, and |completionSteps|.
:: An algorithm that takes a {{Document}} |targetDocument|, a [=string=] |inputArguments|, an
algorithm |completionSteps|, and a [=unique internal value=] |uuid|, and runs the [=imperative
execute steps=] given |tool|, |targetDocument|, |inputArguments|, |completionSteps|, and
|uuid|.

: [=tool definition/annotations=]
:: null if |tool|'s {{ModelContextTool/annotations}} does not [=map/exist=]. Otherwise, an
Expand Down Expand Up @@ -844,26 +946,28 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputObject</

1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].

1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.

1. Let |targetDocument| be |targetWindow|'s [=associated Document|associated
<code>Document</code>=].

1. Let |uuid| be a new [=unique internal value=].

1. If |options|'s {{ModelContextExecuteToolOptions/signal}} [=map/exists=], 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=].

1. Let |traversable| be |targetDocument|'s [=node navigable=]'s [=navigable/traversable
navigable=].

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. This should also fire the
"<code>toolcanceled</code>" event in the target document. See also <a
href=https://github.com/webmachinelearning/webmcp/pull/146>pull request #146</a>.

1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.

1. Let |targetDocument| be |targetWindow|'s [=associated Document|associated
<code>Document</code>=].
1. [=In parallel=], [=cancel a pending tool execution=] given |traversable| and |uuid|.

1. Run the following steps [=in parallel=]:

Expand Down Expand Up @@ -908,8 +1012,6 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputObject</

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:

Expand All @@ -922,7 +1024,8 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputObject</
<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
href=#caller-destroyed-cleanup>gets destroyed</a> or when the caller aborts the
execution via the options signal; 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>
Expand Down Expand Up @@ -979,7 +1082,8 @@ The <dfn method for=ModelContext>executeTool(<var>tool</var>, <var>inputObject</
[=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|.
execute steps=] given |toolName|, |targetDocument|, |inputArguments|, |completionSteps|,
and |uuid|.

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
Expand Down Expand Up @@ -1011,7 +1115,11 @@ dictionary ToolAnnotations {
boolean untrustedContentHint = false;
};

callback ToolExecuteCallback = Promise<any> (object inputObject);
dictionary ToolExecuteCallbackOptions {
required AbortSignal signal;
};

callback ToolExecuteCallback = Promise<any> (object inputObject, ToolExecuteCallbackOptions options);
</xmp>

<dl class="domintro">
Expand Down Expand Up @@ -1041,7 +1149,8 @@ callback ToolExecuteCallback = Promise<any> (object inputObject);

<dt><code><var ignore>tool</var>["{{ModelContextTool/execute}}"]</code></dt>
<dd>
<p>A callback function that is invoked when an [=agent=] calls the tool. The function receives the input parameters.
<p>A callback function that is invoked when an [=agent=] calls the tool. The function receives
the input parameters and execution options.

<p>The function can be asynchronous and return a promise, in which case the [=agent=] will receive the result once the promise is resolved.
</dd>
Expand All @@ -1062,6 +1171,15 @@ The {{ToolAnnotations}} dictionary provides optional metadata about a tool:
:: If true, indicates that the tool's output contains data that is untrusted, from the perspective of the author registering the tool.
</dl>

<h4 id="tool-execute-callback-options">ToolExecuteCallbackOptions Dictionary</h4>

The {{ToolExecuteCallbackOptions}} dictionary carries options passed to a tool's
{{ToolExecuteCallback}} when the tool is executed.

<dl class="domintro">
: <code><var ignore>options</var>["{{ToolExecuteCallbackOptions/signal}}"]</code>
:: An {{AbortSignal}} that communicates when the execution of the tool has been cancelled.
</dl>

<h4 id="model-context-register-tool-options">ModelContextRegisterToolOptions Dictionary</h4>

Expand Down
Loading