diff --git a/index.bs b/index.bs index 48c44a1..e884548 100644 --- a/index.bs +++ b/index.bs @@ -94,8 +94,13 @@ 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
+  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
@@ -164,10 +169,11 @@ 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=].
@@ -189,6 +195,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=]. + +

@@ -257,6 +333,107 @@ 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. + +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=]. + +
+ +
+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 |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}}. + +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: + + 1. Optionally [=report a warning to the console=] describing |r|. + + 1. Run |completionSteps| given null and false. + +
+
To unregister a tool given a {{ModelContext}} |modelContext| and a [=string=] |tool name|, run these steps: @@ -273,10 +450,33 @@ To unregister a tool given a {{ModelContext}} |mo 1. [=map/Remove=] |tool map|[|tool name|]. +1. Let |targetDocument| be |modelContext|'s [=relevant global object=]'s [=associated + Document|associated Document=]. + +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 Document=] and |exposed origins|. + 1. Let |canceledExecutions| be an empty [=list=]. + + 1. [=map/For each=] |uuid| → |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|.
@@ -323,6 +523,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 = {}); attribute EventHandler ontoolchange; }; @@ -348,6 +549,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 + stringified result of the tool's execution.

+
@@ -451,7 +658,9 @@ 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, 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=]. + +1. [=Assert=]: |expectedTargetOrigin| is not an [=opaque origin=]. + +1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=]. + +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. [=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 + "toolcanceled" event in the target document. See also issue #146. + +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(#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=]. + + 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| 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. + + 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. + +
+

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 gets destroyed; 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.

+ +

This can also happen due to a limitation with tool unregistration. Consider this + example:

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

ModelContextTool Dictionary

The {{ModelContextTool}} dictionary describes a tool that can be invoked by [=agents=]. @@ -617,19 +1003,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 +1088,22 @@ dictionary ModelContextGetToolOptions { same-origin documents.
+

ModelContextExecuteToolOptions Dictionary

+ +The {{ModelContextExecuteToolOptions}} dictionary allows web applications to pass options to +{{ModelContext/executeTool()}}. + + +dictionary ModelContextExecuteToolOptions { + AbortSignal signal; +}; + + +
+ : options["{{ModelContextExecuteToolOptions/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 @@ -792,6 +1196,13 @@ The synthesize a declarative JSON Schema object algorithm, given a <{ }
+
+The declarative execute steps are as follows: + +Issue: Spec the declarative execution steps, and their integration with form elements. + +
+

Events

The following are the [=event handlers=] (and their corresponding [=event handler event types=])