diff --git a/index.bs b/index.bs
index b72f33f..d0677b9 100644
--- a/index.bs
+++ b/index.bs
@@ -142,6 +142,78 @@ typedef (
) LanguageModelMessageValue;
+
Prompt processing
+
+This will be incorporated into a proper part of the specification later. For now, we're just writing out this algorithm as a full spec, since it's complicated.
+
+
+ To validate and canonicalize a prompt given a {{LanguageModelPrompt}} |input|, a [=list=] of {{LanguageModelMessageType}}s |expectedTypes|, and a boolean |isInitial|, perform the following steps. The return value will be a non-empty [=list=] of {{LanguageModelMessage}}s in their "longhand" form.
+
+ 1. [=Assert=]: |expectedTypes| [=list/contains=] "{{LanguageModelMessageType/text}}".
+
+ 1. If |input| is a [=string=], then return «
+ «[
+ "{{LanguageModelMessage/role}}" → "{{LanguageModelMessageRole/user}}",
+ "{{LanguageModelMessage/content}}" → «
+ «[
+ "{{LanguageModelMessageContent/type}}" → "{{LanguageModelMessageType/text}}",
+ "{{LanguageModelMessageContent/value}}" → |input|
+ ]»
+ »
+ ]»
+ ».
+
+ 1. [=Assert=]: |input| is a [=list=] of {{LanguageModelMessage}}s.
+
+ 1. Let |seenNonSystemRole| be false.
+
+ 1. Let |messages| be an empty [=list=] of {{LanguageModelMessage}}s.
+
+ 1. [=list/For each=] |message| of |input|:
+
+ 1. If |message|["{{LanguageModelMessage/content}}"] is a [=string=], then set |message| to «[
+ "{{LanguageModelMessage/role}}" → |message|["{{LanguageModelMessage/role}}"],
+ "{{LanguageModelMessage/content}}" → «
+ «[
+ "{{LanguageModelMessageContent/type}}" → "{{LanguageModelMessageType/text}}",
+ "{{LanguageModelMessageContent/value}}" → |message|
+ ]»
+ »
+ ]» to |messages|.
+
+ 1. [=list/For each=] |content| of |message|["{{LanguageModelMessage/content}}"]:
+
+ 1. If |message|["{{LanguageModelMessage/role}}"] is "{{LanguageModelMessageRole/system}}", then:
+
+ 1. If |isInitial| is false, then throw a "{{NotSupportedError}}" {{DOMException}}.
+
+ 1. If |seenNonSystemRole| is true, then throw a "{{SyntaxError}}" {{DOMException}}.
+
+ 1. If |message|["{{LanguageModelMessage/role}}"] is not "{{LanguageModelMessageRole/system}}", then set |seenNonSystemRole| to true.
+
+ 1. If |message|["{{LanguageModelMessage/role}}"] is "{{LanguageModelMessageRole/assistant}}" and |content|["{{LanguageModelMessageContent/type}}"] is not "{{LanguageModelMessageType/text}}", then throw a "{{NotSupportedError}}" {{DOMException}}.
+
+ 1. If |content|["{{LanguageModelMessageContent/type}}"] is "{{LanguageModelMessageType/text}}" and |content|["{{LanguageModelMessageContent/value}}"] is not a [=string=], then throw a {{TypeError}}.
+
+ 1. If |content|["{{LanguageModelMessageContent/type}}"] is "{{LanguageModelMessageType/image}}", then:
+
+ 1. If |expectedTypes| does not [=list/contain=] "{{LanguageModelMessageType/image}}", then throw a "{{NotSupportedError}}" {{DOMException}}.
+
+ 1. If |content|["{{LanguageModelMessageContent/value}}"] is not an {{ImageBitmapSource}} or {{BufferSource}}, then throw a {{TypeError}}.
+
+ 1. If |content|["{{LanguageModelMessageContent/type}}"] is "{{LanguageModelMessageType/audio}}", then:
+
+ 1. If |expectedTypes| does not [=list/contain=] "{{LanguageModelMessageType/audio}}", then throw a "{{NotSupportedError}}" {{DOMException}}.
+
+ 1. If |content|["{{LanguageModelMessageContent/value}}"] is not an {{AudioBuffer}}, {{BufferSource}}, or {{Blob}}, then throw a {{TypeError}}.
+
+ 1. [=list/Append=] |message| to |messages|.
+
+ 1. If |messages| [=list/is empty=], then throw a "{{SyntaxError}}" {{DOMException}}.
+
+ 1. Return |messages|.
+
+
Permissions policy integration
Access to the prompt API is gated behind the [=policy-controlled feature=] "language-model", which has a [=policy-controlled feature/default allowlist=] of [=default allowlist/'self'=]
.