@abide/abide 0.115.0 → 0.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/docs/REGISTRY.md +15 -12
- package/docs/RULEBOOK.md +36 -1
- package/docs/abide.md +69 -17
- package/package.json +1 -1
- package/src/compiler/classifyTemplate.ts +21 -14
- package/src/compiler/emitClient.ts +5 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/IMAGE_TYPES.ts +10 -0
- package/src/server/answerWorker.ts +67 -20
- package/src/server/assistant.ts +81 -12
- package/src/server/claudeApi.ts +25 -20
- package/src/server/claudeCode.ts +30 -10
- package/src/server/generatedSurfaces.ts +48 -14
- package/src/server/index.ts +2 -0
- package/src/shared/REACHED_MESSAGE.ts +7 -0
- package/src/shared/framing.ts +10 -2
- package/src/ui/Router.ts +28 -7
- package/src/ui/bindFiles.ts +42 -0
- package/src/ui/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.116.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c1c3924: `bind:files` — a file input's files as an array, one or many: a pick writes them, and an array written to it (one dropped, or `[]` to clear) is what the input holds. A `bind:` an element has no property for, such as `bind:file` or `bind:vlaue`, is now a build error where it compiled to an attribute that bound nothing. RULEBOOK 32.73, 32.74
|
|
8
|
+
- 48cfc3e: tools answer images — a handler whose answer is a png, jpeg, gif or webp body reaches a model as the image: `/__abide/mcp` carries it as an `image` content item (so Claude Code, through a bridge, sees it), and a `Toolbox` call answers a `ToolAnswer` of `content` — the same pieces, text or image — which `claudeApi` shows the model as the call's result. Any other image type, an svg, is answered as its text. Breaking for a hand-written `Provider`: a `ToolAnswer` is `{ content, failed }` where it was `{ text, failed }`. RULEBOOK 20.21, 44.35
|
|
9
|
+
- 48cfc3e: multimodal transcripts — an `AssistantMessage`'s `content` is its words or its pieces in order, each `AssistantContent` text or an image as base64 with its type. `assistant` refuses an image a model can't take or one outside a user turn, and hands a `Provider` every turn as its pieces, words alone as one text piece. `claudeApi` shows each image to the model as an image block in its place, and `claudeCode` sends the one message Claude Code is sent as Messages API blocks, each image in its place. RULEBOOK 44.5, 44.36, 44.37, 44.38, 44.39
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0a78343: a page reached inside the app reloads offline — the router tells the offline worker each address a navigation reaches, and the worker keeps its page where no saved copy would stand in ([`a3975be`](https://github.com/briancray/abide/commit/a3975be224584df096fe799c6291c0dfcd80cf84))
|
|
14
|
+
|
|
3
15
|
## 0.115.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -321,25 +321,27 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
321
321
|
|
|
322
322
|
| Name | Signature | Meaning | Rules |
|
|
323
323
|
| --- | --- | --- | --- |
|
|
324
|
-
| `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | 19.21, 19.22, 20.5, 20.7, 20.8, 20.9, 20.10, 20.12, 20.13, 20.14, 20.16, 20.17, 20.19, 20.20, 36.7, 16.78 |
|
|
324
|
+
| `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | 19.21, 19.22, 20.5, 20.7, 20.8, 20.9, 20.10, 20.12, 20.13, 20.14, 20.16, 20.17, 20.19, 20.20, 20.21, 20.22, 36.7, 16.78 |
|
|
325
325
|
|
|
326
326
|
# Assistant
|
|
327
327
|
|
|
328
328
|
| Name | Signature | Meaning | Rules |
|
|
329
329
|
| --- | --- | --- | --- |
|
|
330
|
-
| `assistant` | `(messages: AssistantMessage[], provider: Provider, options?: AssistantOptions) => AsyncGenerator<AssistantEvent>` | A conversation with a model, as the events it produces. | 44.1, 44.2, 44.3, 44.4, 44.21 |
|
|
331
|
-
| `AssistantMessage` | `{ role: 'user' \| 'assistant'; content: string }` | One turn of the transcript a caller sends. | 44.3, 44.4 |
|
|
330
|
+
| `assistant` | `(messages: AssistantMessage[], provider: Provider, options?: AssistantOptions) => AsyncGenerator<AssistantEvent>` | A conversation with a model, as the events it produces. | 44.1, 44.2, 44.3, 44.4, 44.21, 44.36, 44.37, 44.39 |
|
|
331
|
+
| `AssistantMessage` | `{ role: 'user' \| 'assistant'; content: string \| AssistantContent[] }` | One turn of the transcript a caller sends: its words, or its pieces in order. | 44.3, 44.4 |
|
|
332
|
+
| `AssistantContent` | `{ type: 'text'; text: string } \| { type: 'image'; data: string; mimeType: string }` | A piece of a turn: words, or an image as base64 with its type. | 44.5, 44.35, 44.36, 44.37, 44.38, 44.39 |
|
|
332
333
|
| `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string } \| { type: 'input'; id: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call as it starts, its input once written, its result, and the end. | 44.2, 44.11, 44.28 |
|
|
333
334
|
| `AssistantOptions` | `{ mcp?: boolean; maxTurns?: number }` | What `assistant` takes beside a provider. | 44.6, 44.8, 44.11 |
|
|
334
|
-
| `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | 44.2, 44.28 |
|
|
335
|
-
| `Toolbox` | `{ list: ToolDefinition[]; call(name: string, input: unknown): Promise<
|
|
335
|
+
| `Provider` | `{ run(messages: (AssistantMessage & { content: AssistantContent[] })[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | 44.2, 44.28, 44.39 |
|
|
336
|
+
| `Toolbox` | `{ list: ToolDefinition[]; call(name: string, input: unknown): Promise<ToolAnswer> }` | The app's MCP tools, called as the asker. | 44.6, 44.7, 44.35 |
|
|
337
|
+
| `ToolAnswer` | `{ content: AssistantContent[]; failed: boolean }` | What a tool call answered: its pieces, and whether it failed. | 20.21, 20.22, 44.35 |
|
|
336
338
|
| `ToolDefinition` | `{ name: string; description: string; inputSchema: unknown; annotations: { readOnlyHint: boolean; destructiveHint: boolean; idempotentHint: boolean }; outputSchema: unknown }` | A tool as the app's MCP surface lists it. | 20.14, 20.16, 20.19, 44.6 |
|
|
337
339
|
|
|
338
340
|
## `claudeApi`
|
|
339
341
|
|
|
340
342
|
| Name | Signature | Meaning | Rules |
|
|
341
343
|
| --- | --- | --- | --- |
|
|
342
|
-
| `claudeApi` | `(config: ClaudeApiConfig) => Provider` | The Claude Messages API as a provider. | 44.9, 44.10, 44.12, 44.21 |
|
|
344
|
+
| `claudeApi` | `(config: ClaudeApiConfig) => Provider` | The Claude Messages API as a provider. | 44.9, 44.10, 44.12, 44.21, 44.35, 44.38 |
|
|
343
345
|
| `ClaudeApiConfig` | `{ apiKey: string; baseURL?: string; model: string; max_tokens: number; system?: string \| unknown[]; tools?: unknown[]; [field: string]: unknown }` | A Messages API request body, less `messages` and `stream`, with the client's key and origin. | 44.9, 44.10 |
|
|
344
346
|
|
|
345
347
|
## `claudeCode`
|
|
@@ -519,7 +521,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
519
521
|
|
|
520
522
|
| Name | Signature | Meaning | Rules |
|
|
521
523
|
| --- | --- | --- | --- |
|
|
522
|
-
| `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | 30.7, 30.9, 30.10, 30.11, 30.12, 30.13, 30.14, 30.15, 30.16, 30.17, 30.18, 30.20, 30.21, 30.22, 30.23, 30.24, 30.25, 30.27, 30.29, 30.30, 30.31, 30.32, 45.12 |
|
|
524
|
+
| `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | 30.7, 30.9, 30.10, 30.11, 30.12, 30.13, 30.14, 30.15, 30.16, 30.17, 30.18, 30.20, 30.21, 30.22, 30.23, 30.24, 30.25, 30.27, 30.29, 30.30, 30.31, 30.32, 45.12, 45.24 |
|
|
523
525
|
|
|
524
526
|
# `.abide` files
|
|
525
527
|
|
|
@@ -551,11 +553,12 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
551
553
|
| `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.70, 32.71 |
|
|
552
554
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
|
|
553
555
|
| `name="…{expr}…"` | An interpolated quoted value. | 32.3, 32.71 |
|
|
554
|
-
| `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72 |
|
|
556
|
+
| `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72, 32.74 |
|
|
555
557
|
| `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | 32.6, 33.6, 33.8, 14.18 |
|
|
556
|
-
| `bind:checked` | A boolean bind on an input. | 32.7, 32.72 |
|
|
557
|
-
| `bind:open` | The same, on a `<details>`. | 32.7, 32.72 |
|
|
558
|
-
| `bind:group` | Radio or checkbox membership. | 32.8, 32.72 |
|
|
558
|
+
| `bind:checked` | A boolean bind on an input. | 32.7, 32.72, 32.74 |
|
|
559
|
+
| `bind:open` | The same, on a `<details>`. | 32.7, 32.72, 32.74 |
|
|
560
|
+
| `bind:group` | Radio or checkbox membership. | 32.8, 32.72, 32.74 |
|
|
561
|
+
| `bind:files` | A file input's files, as a list. | 32.73, 32.72, 32.74 |
|
|
559
562
|
| `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
|
|
560
563
|
| `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66, 32.68, 32.69 |
|
|
561
564
|
| `class:name={cond}` | Toggles a class. | 32.10, 32.71 |
|
|
@@ -656,7 +659,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
656
659
|
| `ABIDE_MAX_REQUEST_BODY_SIZE` | `number` | The default ceiling on a mutation's body. | 16.13, 16.54, 16.55, 16.78 |
|
|
657
660
|
| `ABIDE_MAX_SOCKET_MESSAGE_SIZE` | `number` | The ceiling on a frame received over the socket mux, in bytes. | 18.28 |
|
|
658
661
|
| `ABIDE_MAX_SOCKET_SUBSCRIPTIONS` | `number` | How many values one connection to the socket mux may follow. | 18.31, 18.32 |
|
|
659
|
-
| `ABIDE_OFFLINE` | `boolean` | Whether the app keeps its pages and its bundle on the device, for when the network is gone. | 45.1, 45.2, 45.3, 45.4, 45.5, 45.6, 45.7, 45.8, 45.9, 45.10, 45.11, 45.12, 45.13, 45.14, 45.15, 45.16, 45.17, 45.18, 45.19, 45.20, 45.21, 45.22, 45.23 |
|
|
662
|
+
| `ABIDE_OFFLINE` | `boolean` | Whether the app keeps its pages and its bundle on the device, for when the network is gone. | 45.1, 45.2, 45.3, 45.4, 45.5, 45.6, 45.7, 45.8, 45.9, 45.10, 45.11, 45.12, 45.13, 45.14, 45.15, 45.16, 45.17, 45.18, 45.19, 45.20, 45.21, 45.22, 45.23, 45.24 |
|
|
660
663
|
| `ABIDE_STOP_TIMEOUT` | `number` | How long `App.stop` drains, in ms. | 42.17, 42.18 |
|
|
661
664
|
| `ABIDE_MAX_STREAM_BUFFER_SIZE` | `number` | The cap on a stream transcript held in memory. | 36.18 |
|
|
662
665
|
| `ABIDE_MAX_LOG_BUFFER_COUNT` | `number` | How many lines `log.records` is trimmed back to. | 28.1 |
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -1385,6 +1385,13 @@ whatever its handler's args schema declares there.
|
|
|
1385
1385
|
20.20 A handler `/__abide/mcp` would list whose args schema can never be an object MUST refuse the
|
|
1386
1386
|
app at boot, naming the handler.
|
|
1387
1387
|
|
|
1388
|
+
20.21 A `tools/call` that `/__abide/mcp` answers for a call answered with an image body of a type
|
|
1389
|
+
44.36 names MUST carry that body as one `image` content item, base64 with its type, in place of its
|
|
1390
|
+
text, and MUST carry no `structuredContent`.
|
|
1391
|
+
|
|
1392
|
+
20.22 A call to a tool declaring an `outputSchema` that is answered with an `image/*` body MUST be
|
|
1393
|
+
refused, its text naming the tool and the image's type, and MUST be logged per 20.18 as a 500.
|
|
1394
|
+
|
|
1388
1395
|
# 21. Generated headers
|
|
1389
1396
|
|
|
1390
1397
|
21.1 Every response MUST carry `x-content-type-options: nosniff`.
|
|
@@ -2072,6 +2079,13 @@ declarations first, then each directive's in the tag's order.
|
|
|
2072
2079
|
attribute a `bind:name` names, `checked` beside `bind:group`, an `<option selected>` inside a
|
|
2073
2080
|
`<select bind:value>`, or text inside a `<textarea bind:value>`.
|
|
2074
2081
|
|
|
2082
|
+
32.73 `bind:files` MUST hold a list of the input's files, with or without `multiple`, MUST write the
|
|
2083
|
+
files picked back on change, and MUST hand the input a list written to it; the server MUST write
|
|
2084
|
+
nothing for it.
|
|
2085
|
+
|
|
2086
|
+
32.74 A `bind:` on an element naming none of `value`, `checked`, `open`, `group`, `files` and
|
|
2087
|
+
`element` MUST be a build error.
|
|
2088
|
+
|
|
2075
2089
|
# 33. Props
|
|
2076
2090
|
|
|
2077
2091
|
33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
|
|
@@ -3160,7 +3174,7 @@ of its own.
|
|
|
3160
3174
|
each one.
|
|
3161
3175
|
|
|
3162
3176
|
44.5 `claudeCode` MUST send a transcript to Claude Code as one message, its earlier turns folded
|
|
3163
|
-
above the last.
|
|
3177
|
+
above the last and each `AssistantContent` image in its place.
|
|
3164
3178
|
|
|
3165
3179
|
44.6 With `mcp`, the `Toolbox` `assistant` hands a `Provider` MUST list the tools `/__abide/mcp`
|
|
3166
3180
|
lists, and MUST call each as a request through the app answering the request.
|
|
@@ -3251,6 +3265,23 @@ that browser carrying any other.
|
|
|
3251
3265
|
`principal.clear` made at its browser since it opened signs in, at the caller 26.9 rotates it to,
|
|
3252
3266
|
and the command it yields MUST pair that one.
|
|
3253
3267
|
|
|
3268
|
+
44.35 A `Toolbox` call MUST hand as its `ToolAnswer`'s `content` the pieces `/__abide/mcp` carries
|
|
3269
|
+
for the same call, `claudeApi` MUST show the model those pieces as the call's result, and the
|
|
3270
|
+
`result` `AssistantEvent` MUST carry their words and none of their images.
|
|
3271
|
+
|
|
3272
|
+
44.36 `assistant` MUST refuse 400, before any `Provider` is reached, a transcript holding an
|
|
3273
|
+
`AssistantContent` image whose `mimeType` is none of `image/png`, `image/jpeg`, `image/gif` and
|
|
3274
|
+
`image/webp`.
|
|
3275
|
+
|
|
3276
|
+
44.37 `assistant` MUST refuse 400, before any `Provider` is reached, a transcript holding an
|
|
3277
|
+
`AssistantContent` image in a turn that is not the user's.
|
|
3278
|
+
|
|
3279
|
+
44.38 `claudeApi` MUST send each `AssistantContent` image to the model as an image block, base64 with
|
|
3280
|
+
its type, in its place in its turn.
|
|
3281
|
+
|
|
3282
|
+
44.39 `assistant` MUST hand a `Provider` each turn as its `AssistantContent` pieces, a turn's string
|
|
3283
|
+
as one text piece.
|
|
3284
|
+
|
|
3254
3285
|
# 45. Offline
|
|
3255
3286
|
|
|
3256
3287
|
45.1 Where `ABIDE_OFFLINE` is on, a page `abide build` built MUST register the worker
|
|
@@ -3322,6 +3353,10 @@ and that copy is stale by no more than its `stale-if-error`.
|
|
|
3322
3353
|
45.23 The worker `ABIDE_OFFLINE` turns on MUST ask for a page it saves with an `abide-keep` header,
|
|
3323
3354
|
naming `open` where its answer is the one a navigation opens.
|
|
3324
3355
|
|
|
3356
|
+
45.24 The worker `ABIDE_OFFLINE` turns on MUST save the page of an address a `navigate` reached,
|
|
3357
|
+
where it saved no copy of that address 45.19 would answer with, and MUST NOT save one of another
|
|
3358
|
+
origin.
|
|
3359
|
+
|
|
3325
3360
|
# 46. Outbox
|
|
3326
3361
|
|
|
3327
3362
|
46.1 `outbox` MUST answer, oldest first, every call queued in it that was neither delivered nor
|
package/docs/abide.md
CHANGED
|
@@ -72,10 +72,12 @@ at its address with `.md` on the end, and the links below point at those files.
|
|
|
72
72
|
- Generated surfaces — `reference/generated-surfaces.md`
|
|
73
73
|
- `assistant` — `reference/assistant.md`
|
|
74
74
|
- `AssistantMessage` — `reference/types/AssistantMessage.md`
|
|
75
|
+
- `AssistantContent` — `reference/types/AssistantContent.md`
|
|
75
76
|
- `AssistantEvent` — `reference/types/AssistantEvent.md`
|
|
76
77
|
- `AssistantOptions` — `reference/types/AssistantOptions.md`
|
|
77
78
|
- `Provider` — `reference/types/Provider.md`
|
|
78
79
|
- `Toolbox` — `reference/types/Toolbox.md`
|
|
80
|
+
- `ToolAnswer` — `reference/types/ToolAnswer.md`
|
|
79
81
|
- `ToolDefinition` — `reference/types/ToolDefinition.md`
|
|
80
82
|
- `claudeApi` — `reference/claudeApi.md`
|
|
81
83
|
- `ClaudeApiConfig` — `reference/types/ClaudeApiConfig.md`
|
|
@@ -594,6 +596,7 @@ test('typing writes the value, and the value fills the field', async () => {
|
|
|
594
596
|
| `bind:checked` | a checkbox, to a boolean |
|
|
595
597
|
| `bind:open` | a `<details>`, to whether it is open |
|
|
596
598
|
| `bind:group` | radios to one value, or checkboxes to an array of their `value`s |
|
|
599
|
+
| `bind:files` | a file input, to an array of the files picked |
|
|
597
600
|
| `bind:value={{get, set}}` | a field to a pair you write, where the value is computed |
|
|
598
601
|
| `bind:prop={state}` | a component's prop, so the child's write reaches your value |
|
|
599
602
|
|
|
@@ -5404,8 +5407,11 @@ const recorded: Provider = {
|
|
|
5404
5407
|
const input = { name: 'Ada' }
|
|
5405
5408
|
yield { type: 'tool', id: 'toolu_1', name }
|
|
5406
5409
|
yield { type: 'input', id: 'toolu_1', input }
|
|
5407
|
-
const
|
|
5408
|
-
|
|
5410
|
+
const { content, failed } = await tools.call(name, input)
|
|
5411
|
+
// A search answers words, so its one piece is text.
|
|
5412
|
+
const [words] = content
|
|
5413
|
+
const text = words?.type === 'text' ? words.text : ''
|
|
5414
|
+
yield { type: 'result', id: 'toolu_1', text, failed }
|
|
5409
5415
|
yield { type: 'text', text: 'Ada Lovelace is at Analytical Engines.' }
|
|
5410
5416
|
yield { type: 'done', stop: 'end_turn', usage: { input: 0, output: 0 } }
|
|
5411
5417
|
},
|
|
@@ -5458,11 +5464,13 @@ test('the model calls the CRM search, through the app', async () => {
|
|
|
5458
5464
|
|
|
5459
5465
|
| Name | What it does for the CRM |
|
|
5460
5466
|
| --- | --- |
|
|
5461
|
-
| `AssistantMessage` | One turn of the transcript, `{ role, content }`. The last is the user's. |
|
|
5467
|
+
| `AssistantMessage` | One turn of the transcript, `{ role, content }`. The last is the user's. Its `content` is its words, or its pieces in order. |
|
|
5468
|
+
| `AssistantContent` | One piece of a turn: `text`, or an `image` as base64 with its type, so a rep can show the assistant a photo of a business card. PNG, JPEG, GIF and WebP, in the rep's own turns. |
|
|
5462
5469
|
| `AssistantOptions` | `mcp` to offer the tools, and `maxTurns`, 10 by default, to cap the model's turns in one call. |
|
|
5463
5470
|
| `AssistantEvent` | What the page reads: `text`, `tool` as a call starts, `input` once it is written, `result`, then `done` with the stop reason and token usage. The same events from every provider. |
|
|
5464
|
-
| `Provider` | What runs the conversation: one method, `run
|
|
5471
|
+
| `Provider` | What runs the conversation: one method, `run`, handed every turn as its pieces. Write one for another model, or a recording for a test. |
|
|
5465
5472
|
| `Toolbox` | What a provider is handed: `list` of the tools, and `call` to run one as the asker. |
|
|
5473
|
+
| `ToolAnswer` | What `call` answers: its `content`, the same pieces as a turn — the tool's text, or the image a handler drew, which the model is shown as a picture — and whether it failed. |
|
|
5466
5474
|
| `ToolDefinition` | One tool in `list`: its name, description and input schema. |
|
|
5467
5475
|
| `abide:assistant` | The log namespace that writes one `debug` line per conversation once it ends: the stop reason, the transcript's length, the tool calls, the tokens and the time. |
|
|
5468
5476
|
|
|
@@ -5595,7 +5603,12 @@ test('the body is sent as written, with the transcript', async () => {
|
|
|
5595
5603
|
max_tokens: 1024,
|
|
5596
5604
|
system: 'Answer questions about contacts.',
|
|
5597
5605
|
stream: true,
|
|
5598
|
-
messages: [
|
|
5606
|
+
messages: [
|
|
5607
|
+
{
|
|
5608
|
+
role: 'user',
|
|
5609
|
+
content: [{ type: 'text', text: 'Which company is Ada at?' }],
|
|
5610
|
+
},
|
|
5611
|
+
],
|
|
5599
5612
|
})
|
|
5600
5613
|
})
|
|
5601
5614
|
```
|
|
@@ -6627,10 +6640,12 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6627
6640
|
| --- | --- |
|
|
6628
6641
|
| [`assistant`](/reference/assistant.md) | `@abide/abide/server` |
|
|
6629
6642
|
| [`AssistantMessage`](/reference/types/AssistantMessage.md) | `@abide/abide/server` |
|
|
6643
|
+
| [`AssistantContent`](/reference/types/AssistantContent.md) | `@abide/abide/server` |
|
|
6630
6644
|
| [`AssistantEvent`](/reference/types/AssistantEvent.md) | `@abide/abide/server` |
|
|
6631
6645
|
| [`AssistantOptions`](/reference/types/AssistantOptions.md) | `@abide/abide/server` |
|
|
6632
6646
|
| [`Provider`](/reference/types/Provider.md) | `@abide/abide/server` |
|
|
6633
6647
|
| [`Toolbox`](/reference/types/Toolbox.md) | `@abide/abide/server` |
|
|
6648
|
+
| [`ToolAnswer`](/reference/types/ToolAnswer.md) | `@abide/abide/server` |
|
|
6634
6649
|
| [`ToolDefinition`](/reference/types/ToolDefinition.md) | `@abide/abide/server` |
|
|
6635
6650
|
| [`claudeApi`](/reference/claudeApi.md) | `@abide/abide/server` |
|
|
6636
6651
|
| [`ClaudeApiConfig`](/reference/types/ClaudeApiConfig.md) | `@abide/abide/server` |
|
|
@@ -7833,7 +7848,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7833
7848
|
|
|
7834
7849
|
| Name | Signature | Meaning | Rules |
|
|
7835
7850
|
| --- | --- | --- | --- |
|
|
7836
|
-
| `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.9](/rulebook#20.9), [20.10](/rulebook#20.10), [20.12](/rulebook#20.12), [20.13](/rulebook#20.13), [20.14](/rulebook#20.14), [20.16](/rulebook#20.16), [20.17](/rulebook#20.17), [20.19](/rulebook#20.19), [20.20](/rulebook#20.20), [36.7](/rulebook#36.7), [16.78](/rulebook#16.78) |
|
|
7851
|
+
| `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.9](/rulebook#20.9), [20.10](/rulebook#20.10), [20.12](/rulebook#20.12), [20.13](/rulebook#20.13), [20.14](/rulebook#20.14), [20.16](/rulebook#20.16), [20.17](/rulebook#20.17), [20.19](/rulebook#20.19), [20.20](/rulebook#20.20), [20.21](/rulebook#20.21), [20.22](/rulebook#20.22), [36.7](/rulebook#36.7), [16.78](/rulebook#16.78) |
|
|
7837
7852
|
|
|
7838
7853
|
## Taught in
|
|
7839
7854
|
|
|
@@ -7851,7 +7866,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7851
7866
|
|
|
7852
7867
|
| Name | Signature | Meaning | Rules |
|
|
7853
7868
|
| --- | --- | --- | --- |
|
|
7854
|
-
| `assistant` | `(messages: AssistantMessage[], provider: Provider, options?: AssistantOptions) => AsyncGenerator<AssistantEvent>` | A conversation with a model, as the events it produces. | [44.1](/rulebook#44.1), [44.2](/rulebook#44.2), [44.3](/rulebook#44.3), [44.4](/rulebook#44.4), [44.21](/rulebook#44.21) |
|
|
7869
|
+
| `assistant` | `(messages: AssistantMessage[], provider: Provider, options?: AssistantOptions) => AsyncGenerator<AssistantEvent>` | A conversation with a model, as the events it produces. | [44.1](/rulebook#44.1), [44.2](/rulebook#44.2), [44.3](/rulebook#44.3), [44.4](/rulebook#44.4), [44.21](/rulebook#44.21), [44.36](/rulebook#44.36), [44.37](/rulebook#44.37), [44.39](/rulebook#44.39) |
|
|
7855
7870
|
|
|
7856
7871
|
## Taught in
|
|
7857
7872
|
|
|
@@ -7869,7 +7884,25 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7869
7884
|
|
|
7870
7885
|
| Name | Signature | Meaning | Rules |
|
|
7871
7886
|
| --- | --- | --- | --- |
|
|
7872
|
-
| `AssistantMessage` | `{ role: 'user' \| 'assistant'; content: string }` | One turn of the transcript a caller sends. | [44.3](/rulebook#44.3), [44.4](/rulebook#44.4) |
|
|
7887
|
+
| `AssistantMessage` | `{ role: 'user' \| 'assistant'; content: string \| AssistantContent[] }` | One turn of the transcript a caller sends: its words, or its pieces in order. | [44.3](/rulebook#44.3), [44.4](/rulebook#44.4) |
|
|
7888
|
+
|
|
7889
|
+
## Taught in
|
|
7890
|
+
|
|
7891
|
+
- [Open it to machines](/guide/open-it-to-machines.md)
|
|
7892
|
+
|
|
7893
|
+
---
|
|
7894
|
+
|
|
7895
|
+
<!-- reference/types/AssistantContent.md -->
|
|
7896
|
+
|
|
7897
|
+
# `AssistantContent`
|
|
7898
|
+
|
|
7899
|
+
`import type { AssistantContent } from '@abide/abide/server'`
|
|
7900
|
+
|
|
7901
|
+
## Assistant
|
|
7902
|
+
|
|
7903
|
+
| Name | Signature | Meaning | Rules |
|
|
7904
|
+
| --- | --- | --- | --- |
|
|
7905
|
+
| `AssistantContent` | `{ type: 'text'; text: string } \| { type: 'image'; data: string; mimeType: string }` | A piece of a turn: words, or an image as base64 with its type. | [44.5](/rulebook#44.5), [44.35](/rulebook#44.35), [44.36](/rulebook#44.36), [44.37](/rulebook#44.37), [44.38](/rulebook#44.38), [44.39](/rulebook#44.39) |
|
|
7873
7906
|
|
|
7874
7907
|
## Taught in
|
|
7875
7908
|
|
|
@@ -7923,7 +7956,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7923
7956
|
|
|
7924
7957
|
| Name | Signature | Meaning | Rules |
|
|
7925
7958
|
| --- | --- | --- | --- |
|
|
7926
|
-
| `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2), [44.28](/rulebook#44.28) |
|
|
7959
|
+
| `Provider` | `{ run(messages: (AssistantMessage & { content: AssistantContent[] })[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2), [44.28](/rulebook#44.28), [44.39](/rulebook#44.39) |
|
|
7927
7960
|
|
|
7928
7961
|
## Taught in
|
|
7929
7962
|
|
|
@@ -7941,7 +7974,25 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7941
7974
|
|
|
7942
7975
|
| Name | Signature | Meaning | Rules |
|
|
7943
7976
|
| --- | --- | --- | --- |
|
|
7944
|
-
| `Toolbox` | `{ list: ToolDefinition[]; call(name: string, input: unknown): Promise<
|
|
7977
|
+
| `Toolbox` | `{ list: ToolDefinition[]; call(name: string, input: unknown): Promise<ToolAnswer> }` | The app's MCP tools, called as the asker. | [44.6](/rulebook#44.6), [44.7](/rulebook#44.7), [44.35](/rulebook#44.35) |
|
|
7978
|
+
|
|
7979
|
+
## Taught in
|
|
7980
|
+
|
|
7981
|
+
- [Open it to machines](/guide/open-it-to-machines.md)
|
|
7982
|
+
|
|
7983
|
+
---
|
|
7984
|
+
|
|
7985
|
+
<!-- reference/types/ToolAnswer.md -->
|
|
7986
|
+
|
|
7987
|
+
# `ToolAnswer`
|
|
7988
|
+
|
|
7989
|
+
`import type { ToolAnswer } from '@abide/abide/server'`
|
|
7990
|
+
|
|
7991
|
+
## Assistant
|
|
7992
|
+
|
|
7993
|
+
| Name | Signature | Meaning | Rules |
|
|
7994
|
+
| --- | --- | --- | --- |
|
|
7995
|
+
| `ToolAnswer` | `{ content: AssistantContent[]; failed: boolean }` | What a tool call answered: its pieces, and whether it failed. | [20.21](/rulebook#20.21), [20.22](/rulebook#20.22), [44.35](/rulebook#44.35) |
|
|
7945
7996
|
|
|
7946
7997
|
## Taught in
|
|
7947
7998
|
|
|
@@ -7977,7 +8028,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7977
8028
|
|
|
7978
8029
|
| Name | Signature | Meaning | Rules |
|
|
7979
8030
|
| --- | --- | --- | --- |
|
|
7980
|
-
| `claudeApi` | `(config: ClaudeApiConfig) => Provider` | The Claude Messages API as a provider. | [44.9](/rulebook#44.9), [44.10](/rulebook#44.10), [44.12](/rulebook#44.12), [44.21](/rulebook#44.21) |
|
|
8031
|
+
| `claudeApi` | `(config: ClaudeApiConfig) => Provider` | The Claude Messages API as a provider. | [44.9](/rulebook#44.9), [44.10](/rulebook#44.10), [44.12](/rulebook#44.12), [44.21](/rulebook#44.21), [44.35](/rulebook#44.35), [44.38](/rulebook#44.38) |
|
|
7981
8032
|
|
|
7982
8033
|
## Taught in
|
|
7983
8034
|
|
|
@@ -8789,7 +8840,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8789
8840
|
|
|
8790
8841
|
| Name | Signature | Meaning | Rules |
|
|
8791
8842
|
| --- | --- | --- | --- |
|
|
8792
|
-
| `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | [30.7](/rulebook#30.7), [30.9](/rulebook#30.9), [30.10](/rulebook#30.10), [30.11](/rulebook#30.11), [30.12](/rulebook#30.12), [30.13](/rulebook#30.13), [30.14](/rulebook#30.14), [30.15](/rulebook#30.15), [30.16](/rulebook#30.16), [30.17](/rulebook#30.17), [30.18](/rulebook#30.18), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.22](/rulebook#30.22), [30.23](/rulebook#30.23), [30.24](/rulebook#30.24), [30.25](/rulebook#30.25), [30.27](/rulebook#30.27), [30.29](/rulebook#30.29), [30.30](/rulebook#30.30), [30.31](/rulebook#30.31), [30.32](/rulebook#30.32), [45.12](/rulebook#45.12) |
|
|
8843
|
+
| `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | [30.7](/rulebook#30.7), [30.9](/rulebook#30.9), [30.10](/rulebook#30.10), [30.11](/rulebook#30.11), [30.12](/rulebook#30.12), [30.13](/rulebook#30.13), [30.14](/rulebook#30.14), [30.15](/rulebook#30.15), [30.16](/rulebook#30.16), [30.17](/rulebook#30.17), [30.18](/rulebook#30.18), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.22](/rulebook#30.22), [30.23](/rulebook#30.23), [30.24](/rulebook#30.24), [30.25](/rulebook#30.25), [30.27](/rulebook#30.27), [30.29](/rulebook#30.29), [30.30](/rulebook#30.30), [30.31](/rulebook#30.31), [30.32](/rulebook#30.32), [45.12](/rulebook#45.12), [45.24](/rulebook#45.24) |
|
|
8793
8844
|
|
|
8794
8845
|
## Taught in
|
|
8795
8846
|
|
|
@@ -8829,11 +8880,12 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8829
8880
|
| `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37), [32.70](/rulebook#32.70), [32.71](/rulebook#32.71) |
|
|
8830
8881
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
|
|
8831
8882
|
| `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3), [32.71](/rulebook#32.71) |
|
|
8832
|
-
| `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72) |
|
|
8883
|
+
| `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
|
|
8833
8884
|
| `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | [32.6](/rulebook#32.6), [33.6](/rulebook#33.6), [33.8](/rulebook#33.8), [14.18](/rulebook#14.18) |
|
|
8834
|
-
| `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72) |
|
|
8835
|
-
| `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72) |
|
|
8836
|
-
| `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72) |
|
|
8885
|
+
| `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
|
|
8886
|
+
| `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
|
|
8887
|
+
| `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
|
|
8888
|
+
| `bind:files` | A file input's files, as a list. | [32.73](/rulebook#32.73), [32.72](/rulebook#32.72), [32.74](/rulebook#32.74) |
|
|
8837
8889
|
| `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | [32.5](/rulebook#32.5), [32.6](/rulebook#32.6) |
|
|
8838
8890
|
| `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | [32.9](/rulebook#32.9), [32.38](/rulebook#32.38), [32.66](/rulebook#32.66), [32.68](/rulebook#32.68), [32.69](/rulebook#32.69) |
|
|
8839
8891
|
| `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10), [32.71](/rulebook#32.71) |
|
|
@@ -9043,7 +9095,7 @@ Configuration: what is written in place rather than imported.
|
|
|
9043
9095
|
| `ABIDE_MAX_REQUEST_BODY_SIZE` | `number` | The default ceiling on a mutation's body. | [16.13](/rulebook#16.13), [16.54](/rulebook#16.54), [16.55](/rulebook#16.55), [16.78](/rulebook#16.78) |
|
|
9044
9096
|
| `ABIDE_MAX_SOCKET_MESSAGE_SIZE` | `number` | The ceiling on a frame received over the socket mux, in bytes. | [18.28](/rulebook#18.28) |
|
|
9045
9097
|
| `ABIDE_MAX_SOCKET_SUBSCRIPTIONS` | `number` | How many values one connection to the socket mux may follow. | [18.31](/rulebook#18.31), [18.32](/rulebook#18.32) |
|
|
9046
|
-
| `ABIDE_OFFLINE` | `boolean` | Whether the app keeps its pages and its bundle on the device, for when the network is gone. | [45.1](/rulebook#45.1), [45.2](/rulebook#45.2), [45.3](/rulebook#45.3), [45.4](/rulebook#45.4), [45.5](/rulebook#45.5), [45.6](/rulebook#45.6), [45.7](/rulebook#45.7), [45.8](/rulebook#45.8), [45.9](/rulebook#45.9), [45.10](/rulebook#45.10), [45.11](/rulebook#45.11), [45.12](/rulebook#45.12), [45.13](/rulebook#45.13), [45.14](/rulebook#45.14), [45.15](/rulebook#45.15), [45.16](/rulebook#45.16), [45.17](/rulebook#45.17), [45.18](/rulebook#45.18), [45.19](/rulebook#45.19), [45.20](/rulebook#45.20), [45.21](/rulebook#45.21), [45.22](/rulebook#45.22), [45.23](/rulebook#45.23) |
|
|
9098
|
+
| `ABIDE_OFFLINE` | `boolean` | Whether the app keeps its pages and its bundle on the device, for when the network is gone. | [45.1](/rulebook#45.1), [45.2](/rulebook#45.2), [45.3](/rulebook#45.3), [45.4](/rulebook#45.4), [45.5](/rulebook#45.5), [45.6](/rulebook#45.6), [45.7](/rulebook#45.7), [45.8](/rulebook#45.8), [45.9](/rulebook#45.9), [45.10](/rulebook#45.10), [45.11](/rulebook#45.11), [45.12](/rulebook#45.12), [45.13](/rulebook#45.13), [45.14](/rulebook#45.14), [45.15](/rulebook#45.15), [45.16](/rulebook#45.16), [45.17](/rulebook#45.17), [45.18](/rulebook#45.18), [45.19](/rulebook#45.19), [45.20](/rulebook#45.20), [45.21](/rulebook#45.21), [45.22](/rulebook#45.22), [45.23](/rulebook#45.23), [45.24](/rulebook#45.24) |
|
|
9047
9099
|
| `ABIDE_STOP_TIMEOUT` | `number` | How long `App.stop` drains, in ms. | [42.17](/rulebook#42.17), [42.18](/rulebook#42.18) |
|
|
9048
9100
|
| `ABIDE_MAX_STREAM_BUFFER_SIZE` | `number` | The cap on a stream transcript held in memory. | [36.18](/rulebook#36.18) |
|
|
9049
9101
|
| `ABIDE_MAX_LOG_BUFFER_COUNT` | `number` | How many lines `log.records` is trimmed back to. | [28.1](/rulebook#28.1) |
|
package/package.json
CHANGED
|
@@ -84,10 +84,10 @@ export type ClassifiedAttribute = {
|
|
|
84
84
|
// 32.11 — a spread's attributes, less the ones the element names itself, kept written by a
|
|
85
85
|
// binding of its own.
|
|
86
86
|
| { kind: 'spread'; explicit: string[] }
|
|
87
|
-
// 32.5, 32.7, 32.8 — the property and the value, kept in step both ways. `reflects` is
|
|
88
|
-
// attribute the property's value stands at in the tag, and `undefined` where it is not an
|
|
87
|
+
// 32.5, 32.7, 32.8, 32.73 — the property and the value, kept in step both ways. `reflects` is
|
|
88
|
+
// the attribute the property's value stands at in the tag, and `undefined` where it is not an
|
|
89
89
|
// attribute: a `<textarea>`'s value is its text and a `<select>`'s is its options' `selected`,
|
|
90
|
-
// which the markup writes inside the tag (32.61).
|
|
90
|
+
// which the markup writes inside the tag (32.61), and a file input's files are no markup at all.
|
|
91
91
|
| {
|
|
92
92
|
kind: 'bound'
|
|
93
93
|
bind: ValueBind
|
|
@@ -135,7 +135,7 @@ export function classifyAttributes(
|
|
|
135
135
|
readsAnItem(holder: object): boolean
|
|
136
136
|
},
|
|
137
137
|
): ClassifiedAttribute[] {
|
|
138
|
-
|
|
138
|
+
refuseBinds(node)
|
|
139
139
|
const groups = mergeGroupsOf(node)
|
|
140
140
|
const sharing: ReturnType<typeof sharingOf>[] = []
|
|
141
141
|
for (const group of groups) sharing.push(sharingOf(group, lowering))
|
|
@@ -210,7 +210,8 @@ export function classifyAttributes(
|
|
|
210
210
|
kind: 'bound',
|
|
211
211
|
bind,
|
|
212
212
|
reflects:
|
|
213
|
-
bind === '
|
|
213
|
+
bind === 'files' ||
|
|
214
|
+
(bind === 'value' && node.lowerTag !== 'input')
|
|
214
215
|
? undefined
|
|
215
216
|
: bind === 'group'
|
|
216
217
|
? 'checked'
|
|
@@ -354,15 +355,22 @@ function heldClasses(groups: MergeGroup[]): string[] {
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
/**
|
|
358
|
+
* 32.74 — A `bind:` AN ELEMENT HAS NO PROPERTY FOR IS REFUSED: `bind:vlaue` would otherwise be an
|
|
359
|
+
* attribute of that name, binding nothing and saying nothing.
|
|
360
|
+
*
|
|
357
361
|
* 32.72 — WHAT A `bind:` WRITES IS THE BIND'S ALONE: `checked` beside `bind:checked` or
|
|
358
362
|
* `bind:group`, `value` beside `bind:value`, any `bind:name` beside `name` — and an
|
|
359
363
|
* `<option selected>` inside a `<select bind:value>`, and a `<textarea bind:value>`'s text. Two
|
|
360
364
|
* owners of one value render whichever the browser keeps and move with whichever wrote last.
|
|
361
365
|
*/
|
|
362
|
-
function
|
|
366
|
+
function refuseBinds(node: ElementNode): void {
|
|
363
367
|
for (const attribute of node.attributes) {
|
|
364
368
|
const lower = attribute.lowerName
|
|
365
369
|
if (!lower.startsWith('bind:') || lower === 'bind:element') continue
|
|
370
|
+
if (valueBindOf(lower) === undefined)
|
|
371
|
+
throw new Error(
|
|
372
|
+
`abide: <${node.tag}> has ${attribute.name}, which binds nothing — an element binds ${VALUE_BINDS.join(', ')} or element. RULEBOOK 32.74.`,
|
|
373
|
+
)
|
|
366
374
|
const owned =
|
|
367
375
|
lower === 'bind:group' ? 'checked' : lower.slice('bind:'.length)
|
|
368
376
|
const beside = attributeOf(node, owned)
|
|
@@ -468,18 +476,17 @@ function isElementBind(lowerName: string): boolean {
|
|
|
468
476
|
/**
|
|
469
477
|
* The binds that keep a property and a value in step.
|
|
470
478
|
*
|
|
471
|
-
* 32.5, 32.7, 32.8 — both emitters ask: the server writes the value into the tag
|
|
472
|
-
* binds the property.
|
|
479
|
+
* 32.5, 32.7, 32.8, 32.73 — both emitters ask: the server writes the value into the tag, where it
|
|
480
|
+
* has one, and the client binds the property.
|
|
473
481
|
*/
|
|
474
|
-
|
|
482
|
+
const VALUE_BINDS = ['value', 'checked', 'open', 'group', 'files'] as const
|
|
483
|
+
type ValueBind = (typeof VALUE_BINDS)[number]
|
|
475
484
|
|
|
476
485
|
/** The value bind a folded attribute name is, or `undefined` for any other attribute. */
|
|
477
486
|
function valueBindOf(lowerName: string): ValueBind | undefined {
|
|
478
|
-
if (lowerName
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (lowerName === 'bind:group') return 'group'
|
|
482
|
-
return undefined
|
|
487
|
+
if (!lowerName.startsWith('bind:')) return undefined
|
|
488
|
+
const bind = lowerName.slice('bind:'.length) as ValueBind
|
|
489
|
+
return VALUE_BINDS.includes(bind) ? bind : undefined
|
|
483
490
|
}
|
|
484
491
|
|
|
485
492
|
/**
|
|
@@ -769,7 +769,7 @@ function elementUnits(
|
|
|
769
769
|
handlers.push(
|
|
770
770
|
`${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.listener(one.attribute)})`,
|
|
771
771
|
)
|
|
772
|
-
// 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
772
|
+
// 32.5, 32.7, 32.8, 32.73 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
773
773
|
// region's scope writes the property, and a listener writes it back.
|
|
774
774
|
// 32.8 — a group compares against the input's own value, read where the value is.
|
|
775
775
|
else if (one.kind === 'bound' && one.bind === 'group')
|
|
@@ -782,6 +782,10 @@ function elementUnits(
|
|
|
782
782
|
: JSON.stringify(one.own.value)
|
|
783
783
|
}), ${SCOPE})`,
|
|
784
784
|
)
|
|
785
|
+
else if (one.kind === 'bound' && one.bind === 'files')
|
|
786
|
+
binds.push(
|
|
787
|
+
`${lowering.use('bindFiles')}(${element}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
788
|
+
)
|
|
785
789
|
else if (one.kind === 'bound')
|
|
786
790
|
binds.push(
|
|
787
791
|
`${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
|
|
2
2
|
// server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
|
|
3
|
-
export const BRIDGE_VERSION = '0.3.
|
|
3
|
+
export const BRIDGE_VERSION = '0.3.4'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The image types a model is shown (44.36). A leaf, because `assistant` refuses a transcript image of
|
|
3
|
+
* any other and `/__abide/mcp` answers one of any other as its text (20.21).
|
|
4
|
+
*/
|
|
5
|
+
export const IMAGE_TYPES = new Set([
|
|
6
|
+
'image/png',
|
|
7
|
+
'image/jpeg',
|
|
8
|
+
'image/gif',
|
|
9
|
+
'image/webp',
|
|
10
|
+
])
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
OFFLINE_SAVED,
|
|
18
18
|
} from '#shared/OFFLINE_CACHES.ts'
|
|
19
19
|
import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
|
|
20
|
+
import { REACHED_MESSAGE } from '#shared/REACHED_MESSAGE.ts'
|
|
20
21
|
import type { Manifest } from './manifestOf.ts'
|
|
21
22
|
|
|
22
23
|
// What `framingOf` reads as a framed stream (16.32), and a binary's bytes (16.34), which may be one —
|
|
@@ -38,7 +39,7 @@ async function dropOurs(keeping) {
|
|
|
38
39
|
}
|
|
39
40
|
`
|
|
40
41
|
|
|
41
|
-
// 45.3 through 45.8, 45.13,
|
|
42
|
+
// 45.3 through 45.8, 45.13, 45.17 through 45.21, and 45.24.
|
|
42
43
|
const SAVING = `
|
|
43
44
|
self.addEventListener('install', (event) => {
|
|
44
45
|
event.waitUntil(
|
|
@@ -60,26 +61,23 @@ self.addEventListener('activate', (event) => {
|
|
|
60
61
|
await self.registration.navigationPreload?.disable()
|
|
61
62
|
await self.clients.claim()
|
|
62
63
|
for (const client of await self.clients.matchAll({ type: 'window' }))
|
|
63
|
-
await
|
|
64
|
-
(response) =>
|
|
65
|
-
kept(response)
|
|
66
|
-
? keep(new Request(client.url), response)
|
|
67
|
-
: response.body?.cancel(),
|
|
68
|
-
() => {},
|
|
69
|
-
)
|
|
64
|
+
await keepPage(client.url)
|
|
70
65
|
})(),
|
|
71
66
|
)
|
|
72
67
|
})
|
|
73
68
|
|
|
74
69
|
self.addEventListener('message', (event) => {
|
|
75
|
-
if (event.data === 'rotated') event.waitUntil(
|
|
70
|
+
if (event.data === 'rotated') event.waitUntil(dropSaved())
|
|
71
|
+
const reached = event.data?.[REACHED]
|
|
72
|
+
if (typeof reached === 'string' && reached.startsWith(\`\${self.location.origin}/\`))
|
|
73
|
+
event.waitUntil(keepReached(reached))
|
|
76
74
|
})
|
|
77
75
|
|
|
78
76
|
self.addEventListener('fetch', (event) => {
|
|
79
77
|
const request = event.request
|
|
80
78
|
if (request.mode === 'navigate') {
|
|
81
79
|
if (request.method === 'GET') event.respondWith(answer(event, true))
|
|
82
|
-
else event.waitUntil(
|
|
80
|
+
else event.waitUntil(dropSaved())
|
|
83
81
|
return
|
|
84
82
|
}
|
|
85
83
|
if (request.method !== 'GET') return
|
|
@@ -132,8 +130,7 @@ async function answer(event, page) {
|
|
|
132
130
|
try {
|
|
133
131
|
return await fromNetwork(event, page, page)
|
|
134
132
|
} catch {
|
|
135
|
-
if (life !== undefined && life
|
|
136
|
-
return marked(saved)
|
|
133
|
+
if (life !== undefined && standsIn(life)) return marked(saved)
|
|
137
134
|
return page ? offline() : Response.error()
|
|
138
135
|
}
|
|
139
136
|
}
|
|
@@ -169,18 +166,49 @@ async function fromNetwork(event, page, opened) {
|
|
|
169
166
|
const asked = page
|
|
170
167
|
? pageRequest(request.url, request.headers, opened ? OPENED : '1')
|
|
171
168
|
: new Request(request, { cache: 'no-cache' })
|
|
169
|
+
const since = drops
|
|
172
170
|
const response = await fetch(asked)
|
|
173
171
|
// A copy nothing reads is a tee holding every chunk, so one is cloned only to be saved.
|
|
174
172
|
const copy = kept(response) ? response.clone() : undefined
|
|
175
|
-
|
|
176
|
-
event.waitUntil(
|
|
177
|
-
(rotated ? caches.delete(SAVED) : Promise.resolve()).then(() =>
|
|
178
|
-
copy === undefined ? undefined : keep(request, copy),
|
|
179
|
-
),
|
|
180
|
-
)
|
|
173
|
+
event.waitUntil(keep(request, copy, since, response.headers.has(PRINCIPAL)))
|
|
181
174
|
return response
|
|
182
175
|
}
|
|
183
176
|
|
|
177
|
+
// 45.13, 45.24 — A PAGE NO NAVIGATION ASKED FOR, kept as a page behind the one opened is: an open
|
|
178
|
+
// window's as the worker activates, and an address the router reached.
|
|
179
|
+
function keepPage(url) {
|
|
180
|
+
const since = drops
|
|
181
|
+
return fetch(pageRequest(url, undefined, '1')).then((response) => {
|
|
182
|
+
// Nothing reads this answer, so one 45.3 or 45.4 turns away is let go here.
|
|
183
|
+
const copy = kept(response) ? response : undefined
|
|
184
|
+
if (copy === undefined) response.body?.cancel()
|
|
185
|
+
return keep(new Request(url), copy, since, response.headers.has(PRINCIPAL))
|
|
186
|
+
}, () => {})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The addresses being kept, so one reached twice before its copy lands is asked for once.
|
|
190
|
+
const reaching = new Set()
|
|
191
|
+
|
|
192
|
+
// 45.24 — AN ADDRESS THE ROUTER REACHED, whose page no navigation asked for, so none is saved unless
|
|
193
|
+
// this asks — and only where no copy saved would stand in offline: one that would is refreshed by the
|
|
194
|
+
// page's own next load, and asking on every step renders each page twice. A page kept holds no seed
|
|
195
|
+
// (RULEBOOK 41.29), so what a reload paints stale is markup its calls then answer over.
|
|
196
|
+
async function keepReached(url) {
|
|
197
|
+
if (reaching.has(url)) return
|
|
198
|
+
reaching.add(url)
|
|
199
|
+
try {
|
|
200
|
+
const saved = await caches.match(url, { cacheName: SAVED, ignoreVary: true })
|
|
201
|
+
if (saved === undefined || !standsIn(lifeOf(saved))) await keepPage(url)
|
|
202
|
+
} finally {
|
|
203
|
+
reaching.delete(url)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 45.19 — whether a copy kept still stands in where the network gives nothing.
|
|
208
|
+
function standsIn(life) {
|
|
209
|
+
return life.age < life.fresh + life.error
|
|
210
|
+
}
|
|
211
|
+
|
|
184
212
|
// How old a kept copy is and how long each of its windows runs, in seconds; a copy with no date is
|
|
185
213
|
// as old as it can be.
|
|
186
214
|
function lifeOf(response) {
|
|
@@ -219,9 +247,27 @@ function kept(response) {
|
|
|
219
247
|
return !STREAMED.some((streamed) => type.startsWith(streamed))
|
|
220
248
|
}
|
|
221
249
|
|
|
222
|
-
|
|
250
|
+
// 45.7 — EVERY PAGE SAVED DROPPED, and with them every copy asked for before the drop: one asked under
|
|
251
|
+
// the cookies of then would land after it holding the caller signed out since. The drop an answer's
|
|
252
|
+
// own abide-principal makes is the exception, since that answer is the new caller's.
|
|
253
|
+
let drops = 0
|
|
254
|
+
|
|
255
|
+
function dropSaved() {
|
|
256
|
+
drops += 1
|
|
257
|
+
return caches.delete(SAVED)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Saves a copy 45.3 and 45.4 let through where no drop has come since its request was asked — after
|
|
261
|
+
// the drop its own answer makes where that answer rotated the principal — and cancels one where one has.
|
|
262
|
+
async function keep(request, copy, since, rotated) {
|
|
263
|
+
if (rotated) {
|
|
264
|
+
await dropSaved()
|
|
265
|
+
since = drops
|
|
266
|
+
}
|
|
267
|
+
if (copy === undefined) return
|
|
268
|
+
if (since !== drops) return copy.body?.cancel()
|
|
223
269
|
const cache = await caches.open(SAVED)
|
|
224
|
-
await cache.put(request,
|
|
270
|
+
await cache.put(request, copy)
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
function offline() {
|
|
@@ -277,6 +323,7 @@ const KEPT = ${JSON.stringify(KEPT_HEADER)}
|
|
|
277
323
|
const KEEP = ${JSON.stringify(KEEP_HEADER)}
|
|
278
324
|
const OPENED = ${JSON.stringify(KEEP_OPENED)}
|
|
279
325
|
const NAVIGATION = ${JSON.stringify(NAVIGATION_HEADER)}
|
|
326
|
+
const REACHED = ${JSON.stringify(REACHED_MESSAGE)}
|
|
280
327
|
// 45.4 — the types a stream 16.32 frames, which has no end to save.
|
|
281
328
|
const STREAMED = ${JSON.stringify(STREAMED_TYPES)}
|
|
282
329
|
const OFFLINE = ${JSON.stringify(OFFLINE)}
|
package/src/server/assistant.ts
CHANGED
|
@@ -12,11 +12,20 @@
|
|
|
12
12
|
|
|
13
13
|
import { namedLogger } from '#shared/log.ts'
|
|
14
14
|
import { refuse } from '#shared/refuse.ts'
|
|
15
|
+
import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
|
|
15
16
|
import { type McpReply, McpSurface } from './generatedSurfaces.ts'
|
|
16
17
|
import { request, within } from './requestScope.ts'
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
export type
|
|
19
|
+
/** A piece of a turn: words, or an image as base64 with its type. */
|
|
20
|
+
export type AssistantContent =
|
|
21
|
+
| { type: 'text'; text: string }
|
|
22
|
+
| { type: 'image'; data: string; mimeType: string }
|
|
23
|
+
|
|
24
|
+
/** One turn of the transcript a caller sends: its words, or its pieces in order. */
|
|
25
|
+
export type AssistantMessage = {
|
|
26
|
+
role: 'user' | 'assistant'
|
|
27
|
+
content: string | AssistantContent[]
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
/** What a conversation yields, the same from every provider. */
|
|
22
31
|
export type AssistantEvent =
|
|
@@ -43,19 +52,19 @@ export type ToolDefinition = {
|
|
|
43
52
|
outputSchema: unknown
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
/** What a tool call answered: its pieces, and whether it failed. 20.21, 44.35. */
|
|
56
|
+
export type ToolAnswer = { content: AssistantContent[]; failed: boolean }
|
|
57
|
+
|
|
46
58
|
/** The app's MCP tools, called as the asker. */
|
|
47
59
|
export type Toolbox = {
|
|
48
60
|
list: ToolDefinition[]
|
|
49
|
-
call(
|
|
50
|
-
name: string,
|
|
51
|
-
input: unknown,
|
|
52
|
-
): Promise<{ text: string; failed: boolean }>
|
|
61
|
+
call(name: string, input: unknown): Promise<ToolAnswer>
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/** What runs a conversation: a model's API, or something that runs its own loop. */
|
|
56
65
|
export type Provider = {
|
|
57
66
|
run(
|
|
58
|
-
messages: AssistantMessage[],
|
|
67
|
+
messages: (AssistantMessage & { content: AssistantContent[] })[],
|
|
59
68
|
tools: Toolbox,
|
|
60
69
|
limits: { maxTurns: number; signal: AbortSignal },
|
|
61
70
|
): AsyncGenerator<AssistantEvent>
|
|
@@ -81,11 +90,38 @@ export async function* assistant(
|
|
|
81
90
|
400,
|
|
82
91
|
'The transcript has to end on a user turn for the assistant to answer it. RULEBOOK 44.3.',
|
|
83
92
|
)
|
|
93
|
+
// Every turn as its pieces, so a provider reads one shape: words alone are one text piece.
|
|
94
|
+
const turns: Parameters<Provider['run']>[0] = []
|
|
95
|
+
for (const message of messages) {
|
|
96
|
+
if (typeof message.content === 'string') {
|
|
97
|
+
turns.push({
|
|
98
|
+
role: message.role,
|
|
99
|
+
content: [{ type: 'text', text: message.content }],
|
|
100
|
+
})
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
turns.push({ role: message.role, content: message.content })
|
|
104
|
+
for (const piece of message.content) {
|
|
105
|
+
if (piece?.type !== 'image') continue
|
|
106
|
+
// 44.37 — an image is the asker's to show, never a turn the model answered.
|
|
107
|
+
if (message.role !== 'user')
|
|
108
|
+
throw refuse(
|
|
109
|
+
400,
|
|
110
|
+
'An image can only stand in a user turn of the transcript. RULEBOOK 44.37.',
|
|
111
|
+
)
|
|
112
|
+
// 44.36.
|
|
113
|
+
if (!IMAGE_TYPES.has(piece.mimeType))
|
|
114
|
+
throw refuse(
|
|
115
|
+
400,
|
|
116
|
+
`An image in the transcript is ${piece.mimeType}, and a model is shown only ${[...IMAGE_TYPES].join(', ')}. RULEBOOK 44.36.`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
84
120
|
const tools = toolboxOver(
|
|
85
121
|
options.mcp === true ? appSurface() : NO_SURFACE,
|
|
86
122
|
askerOf(request()),
|
|
87
123
|
)
|
|
88
|
-
const events = provider.run(
|
|
124
|
+
const events = provider.run(turns, tools, {
|
|
89
125
|
maxTurns: options.maxTurns ?? MAX_TURNS,
|
|
90
126
|
signal: request().signal,
|
|
91
127
|
})
|
|
@@ -181,10 +217,15 @@ function toolboxOver(surface: McpSurface, asker: Request): Toolbox {
|
|
|
181
217
|
return await surface.call(name, input, asker)
|
|
182
218
|
} catch (thrown) {
|
|
183
219
|
return {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
:
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: 'text',
|
|
223
|
+
text:
|
|
224
|
+
thrown instanceof Error
|
|
225
|
+
? thrown.message
|
|
226
|
+
: String(thrown),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
188
229
|
failed: true,
|
|
189
230
|
}
|
|
190
231
|
}
|
|
@@ -199,3 +240,31 @@ function toolboxOver(surface: McpSurface, asker: Request): Toolbox {
|
|
|
199
240
|
)
|
|
200
241
|
return tools
|
|
201
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Pieces as Messages API blocks, in order: text as text, an image as base64 with its type. A text of
|
|
246
|
+
* whitespace alone is left out, as the API refuses one. 44.38.
|
|
247
|
+
*/
|
|
248
|
+
export function blocksOf(content: AssistantContent[]): unknown[] {
|
|
249
|
+
const blocks: unknown[] = []
|
|
250
|
+
for (const piece of content)
|
|
251
|
+
if (piece.type === 'image')
|
|
252
|
+
blocks.push({
|
|
253
|
+
type: 'image',
|
|
254
|
+
source: {
|
|
255
|
+
type: 'base64',
|
|
256
|
+
media_type: piece.mimeType,
|
|
257
|
+
data: piece.data,
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
else if (/\S/.test(piece.text))
|
|
261
|
+
blocks.push({ type: 'text', text: piece.text })
|
|
262
|
+
return blocks
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The words of `content`, its images left out. */
|
|
266
|
+
export function textOf(content: AssistantContent[]): string {
|
|
267
|
+
let text = ''
|
|
268
|
+
for (const piece of content) if (piece.type === 'text') text += piece.text
|
|
269
|
+
return text
|
|
270
|
+
}
|
package/src/server/claudeApi.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { framedStream } from '#shared/framing.ts'
|
|
10
10
|
import { refuse } from '#shared/refuse.ts'
|
|
11
|
-
import
|
|
11
|
+
import { blocksOf, type Provider, textOf } from './assistant.ts'
|
|
12
12
|
|
|
13
13
|
/** A Messages API request body, less what abide writes, with the client's key and origin. */
|
|
14
14
|
export type ClaudeApiConfig = {
|
|
@@ -80,8 +80,12 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
|
|
|
80
80
|
input_schema: tool.inputSchema,
|
|
81
81
|
})
|
|
82
82
|
const history: { role: string; content: unknown }[] = []
|
|
83
|
+
// 44.38 — each image in its place in its turn.
|
|
83
84
|
for (const message of messages)
|
|
84
|
-
history.push({
|
|
85
|
+
history.push({
|
|
86
|
+
role: message.role,
|
|
87
|
+
content: blocksOf(message.content),
|
|
88
|
+
})
|
|
85
89
|
const usage = { input: 0, output: 0 }
|
|
86
90
|
for (let turn = 1; ; turn++) {
|
|
87
91
|
const answered = await fetch(
|
|
@@ -189,28 +193,29 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
|
|
|
189
193
|
}
|
|
190
194
|
return
|
|
191
195
|
}
|
|
192
|
-
const
|
|
193
|
-
called.map(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
block.name as string,
|
|
201
|
-
block.input,
|
|
202
|
-
)),
|
|
203
|
-
}),
|
|
204
|
-
),
|
|
196
|
+
const answers = await Promise.all(
|
|
197
|
+
called.map(async (block) => ({
|
|
198
|
+
id: block.id as string,
|
|
199
|
+
...(await tools.call(
|
|
200
|
+
block.name as string,
|
|
201
|
+
block.input,
|
|
202
|
+
)),
|
|
203
|
+
})),
|
|
205
204
|
)
|
|
206
205
|
const content: unknown[] = []
|
|
207
|
-
for (const
|
|
208
|
-
|
|
206
|
+
for (const answer of answers) {
|
|
207
|
+
// 44.35 — the event carries the answer's words; an image goes to the model alone.
|
|
208
|
+
yield {
|
|
209
|
+
type: 'result',
|
|
210
|
+
id: answer.id,
|
|
211
|
+
text: textOf(answer.content),
|
|
212
|
+
failed: answer.failed,
|
|
213
|
+
}
|
|
209
214
|
content.push({
|
|
210
215
|
type: 'tool_result',
|
|
211
|
-
tool_use_id:
|
|
212
|
-
content:
|
|
213
|
-
is_error:
|
|
216
|
+
tool_use_id: answer.id,
|
|
217
|
+
content: blocksOf(answer.content),
|
|
218
|
+
is_error: answer.failed,
|
|
214
219
|
})
|
|
215
220
|
}
|
|
216
221
|
// 44.12 — every result of one turn in one message.
|
package/src/server/claudeCode.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import { mounted } from '#shared/mount.ts'
|
|
12
12
|
import { refuse } from '#shared/refuse.ts'
|
|
13
13
|
import {
|
|
14
|
+
type AssistantContent,
|
|
14
15
|
type AssistantEvent,
|
|
15
16
|
type AssistantMessage,
|
|
17
|
+
blocksOf,
|
|
16
18
|
forwardOf,
|
|
17
19
|
type Provider,
|
|
18
20
|
} from './assistant.ts'
|
|
@@ -65,7 +67,7 @@ function claudeCodeProvider(config: ClaudeCodeConfig = {}): Provider {
|
|
|
65
67
|
bridge.frames.push({
|
|
66
68
|
type: 'turn',
|
|
67
69
|
id: turn.id,
|
|
68
|
-
prompt:
|
|
70
|
+
prompt: foldedContent(messages),
|
|
69
71
|
config,
|
|
70
72
|
maxTurns,
|
|
71
73
|
})
|
|
@@ -105,16 +107,34 @@ function claudeCodeProvider(config: ClaudeCodeConfig = {}): Provider {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The transcript as the one message Claude Code is sent, as Messages API blocks: earlier turns folded
|
|
112
|
+
* above the last, and each image in its place. 44.5.
|
|
113
|
+
*/
|
|
114
|
+
function foldedContent(
|
|
115
|
+
messages: (AssistantMessage & { content: AssistantContent[] })[],
|
|
116
|
+
): unknown[] {
|
|
117
|
+
const pieces: AssistantContent[] = []
|
|
118
|
+
let words = messages.length > 1 ? '<conversation>\n' : ''
|
|
119
|
+
const last = messages.length - 1
|
|
120
|
+
for (let index = 0; index <= last; index++) {
|
|
121
|
+
const message = messages[index] as (typeof messages)[number]
|
|
122
|
+
if (index < last) words += `<${message.role}>\n`
|
|
123
|
+
for (let at = 0; at < message.content.length; at++) {
|
|
124
|
+
const piece = message.content[at] as AssistantContent
|
|
125
|
+
if (at > 0) words += '\n'
|
|
126
|
+
if (piece.type === 'text') {
|
|
127
|
+
words += piece.text
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
pieces.push({ type: 'text', text: words }, piece)
|
|
131
|
+
words = ''
|
|
132
|
+
}
|
|
133
|
+
if (index < last) words += `\n</${message.role}>\n`
|
|
134
|
+
if (index === last - 1) words += '</conversation>\n\n'
|
|
116
135
|
}
|
|
117
|
-
|
|
136
|
+
pieces.push({ type: 'text', text: words })
|
|
137
|
+
return blocksOf(pieces)
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
/** The browser asking, which a pairing is to. 44.13. */
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// schema (19.9) as `argsDocument`.
|
|
15
15
|
|
|
16
16
|
import { isFailed } from '#shared/Failed.ts'
|
|
17
|
+
import { essenceOf } from '#shared/framing.ts'
|
|
17
18
|
import { namedLogger } from '#shared/log.ts'
|
|
18
19
|
import { mounted } from '#shared/mount.ts'
|
|
19
20
|
import { type Reactive, reactive } from '#shared/reactive/index.ts'
|
|
@@ -21,9 +22,10 @@ import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
|
21
22
|
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
22
23
|
import { answered } from './answerFailure.ts'
|
|
23
24
|
import { bodyWithin, type Mounted } from './answerRpc.ts'
|
|
24
|
-
import type { ToolDefinition } from './assistant.ts'
|
|
25
|
+
import type { ToolAnswer, ToolDefinition } from './assistant.ts'
|
|
25
26
|
import { appIdentity } from './config.ts'
|
|
26
27
|
import { crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
28
|
+
import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
|
|
27
29
|
import { MCP_PROTOCOL_VERSION } from './MCP_PROTOCOL_VERSION.ts'
|
|
28
30
|
import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
29
31
|
|
|
@@ -307,16 +309,12 @@ export class McpSurface {
|
|
|
307
309
|
}
|
|
308
310
|
|
|
309
311
|
/** What calling the tool `name` as `asker` answered, a refusal or a missing tool a failed one. */
|
|
310
|
-
call(
|
|
311
|
-
name: string,
|
|
312
|
-
args: unknown,
|
|
313
|
-
asker: Request,
|
|
314
|
-
): Promise<{ text: string; failed: boolean }> {
|
|
312
|
+
call(name: string, args: unknown, asker: Request): Promise<ToolAnswer> {
|
|
315
313
|
const tool = this.#tool(name)
|
|
316
314
|
if (tool === undefined) {
|
|
317
315
|
logCall(String(name), 404, undefined)
|
|
318
316
|
return Promise.resolve({
|
|
319
|
-
text: `No tool named ${name}
|
|
317
|
+
content: [{ type: 'text', text: `No tool named ${name}.` }],
|
|
320
318
|
failed: true,
|
|
321
319
|
})
|
|
322
320
|
}
|
|
@@ -331,7 +329,7 @@ export class McpSurface {
|
|
|
331
329
|
one: Published,
|
|
332
330
|
args: Record<string, unknown> | undefined,
|
|
333
331
|
asker: Request,
|
|
334
|
-
): Promise<
|
|
332
|
+
): Promise<ToolAnswer> {
|
|
335
333
|
// 28.7 — a closed namespace does not pay for the line it would have written.
|
|
336
334
|
const started = MCP_LOG.enabled() ? performance.now() : undefined
|
|
337
335
|
let answer: Response
|
|
@@ -345,10 +343,41 @@ export class McpSurface {
|
|
|
345
343
|
}
|
|
346
344
|
answer = answered(thrown)
|
|
347
345
|
}
|
|
346
|
+
const mimeType = essenceOf(answer.headers.get('content-type'))
|
|
347
|
+
if (answer.status < 400 && mimeType.startsWith('image/')) {
|
|
348
|
+
// 20.22 — a tool that promised a value and answered a picture has no structured content
|
|
349
|
+
// to carry: its handler's type is wrong, and the call says so where it happened.
|
|
350
|
+
if (structured(one)) {
|
|
351
|
+
await answer.body?.cancel()
|
|
352
|
+
logCall(one.name, 500, started)
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: 'text',
|
|
357
|
+
text: `${one.name} declares a result schema but answered an image, ${mimeType}.`,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
failed: true,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 20.21 — AN IMAGE A MODEL IS SHOWN IS THE IMAGE, handed on as one rather than its bytes
|
|
364
|
+
// read as text, so a model looks at what a handler drew. Any other (an svg) is its text.
|
|
365
|
+
if (IMAGE_TYPES.has(mimeType)) {
|
|
366
|
+
const data = (await answer.bytes()).toBase64()
|
|
367
|
+
logCall(one.name, answer.status, started)
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: 'image', data, mimeType }],
|
|
370
|
+
failed: false,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
348
374
|
// The body is the call's too: a handler that streams has only begun when its head arrives.
|
|
349
375
|
const text = await answer.text()
|
|
350
376
|
logCall(one.name, answer.status, started)
|
|
351
|
-
return {
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: 'text', text }],
|
|
379
|
+
failed: answer.status >= 400,
|
|
380
|
+
}
|
|
352
381
|
}
|
|
353
382
|
|
|
354
383
|
/** One MCP request, made as `asker`, as JSON-RPC's `result` or `error`. */
|
|
@@ -379,20 +408,25 @@ export class McpSurface {
|
|
|
379
408
|
`No tool named ${String(params?.name)}.`,
|
|
380
409
|
)
|
|
381
410
|
}
|
|
382
|
-
const {
|
|
411
|
+
const { content, failed } = await this.#called(
|
|
383
412
|
tool,
|
|
384
413
|
params?.arguments as Record<string, unknown> | undefined,
|
|
385
414
|
asker,
|
|
386
415
|
)
|
|
416
|
+
const [piece] = content
|
|
387
417
|
return {
|
|
388
418
|
result: {
|
|
389
|
-
|
|
419
|
+
// 20.21 — an AssistantContent piece is already an MCP content item.
|
|
420
|
+
content,
|
|
390
421
|
// 20.16 — the same value as structured content, where the tool declares
|
|
391
|
-
// its shape; a refusal is not that value.
|
|
422
|
+
// its shape; a refusal is not that value. A tool declaring one answers no
|
|
423
|
+
// image (20.22), so its one piece is its text.
|
|
392
424
|
structuredContent:
|
|
393
|
-
failed ||
|
|
425
|
+
failed ||
|
|
426
|
+
!structured(tool) ||
|
|
427
|
+
piece?.type !== 'text'
|
|
394
428
|
? undefined
|
|
395
|
-
: JSON.parse(text),
|
|
429
|
+
: JSON.parse(piece.text),
|
|
396
430
|
isError: failed,
|
|
397
431
|
},
|
|
398
432
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -27,11 +27,13 @@ export {
|
|
|
27
27
|
} from '#shared/rpc.ts'
|
|
28
28
|
// 45 — a conversation with a model, piped through a handler, and the two providers abide ships.
|
|
29
29
|
export {
|
|
30
|
+
type AssistantContent,
|
|
30
31
|
type AssistantEvent,
|
|
31
32
|
type AssistantMessage,
|
|
32
33
|
type AssistantOptions,
|
|
33
34
|
assistant,
|
|
34
35
|
type Provider,
|
|
36
|
+
type ToolAnswer,
|
|
35
37
|
type Toolbox,
|
|
36
38
|
type ToolDefinition,
|
|
37
39
|
} from './assistant.ts'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key of the message the router posts the offline worker, naming the address a navigation
|
|
3
|
+
* reached, which the worker then keeps (45.24).
|
|
4
|
+
*
|
|
5
|
+
* A leaf, because the server writes it into the worker's script and the router posts it.
|
|
6
|
+
*/
|
|
7
|
+
export const REACHED_MESSAGE = 'reached'
|
package/src/shared/framing.ts
CHANGED
|
@@ -33,11 +33,19 @@ export function framingOf(type: string | null | undefined): Framed | null {
|
|
|
33
33
|
export function carriesBytes(type: string | null | undefined): boolean {
|
|
34
34
|
if (type === null || type === undefined || type === '') return false
|
|
35
35
|
if (framingOf(type) !== null) return false
|
|
36
|
-
const
|
|
37
|
-
const essence = semicolon === -1 ? type : type.slice(0, semicolon)
|
|
36
|
+
const essence = essenceOf(type)
|
|
38
37
|
return essence !== 'application/json' && !essence.endsWith('+json')
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
/** A content type less its parameters, folded to lower case, as a type is compared; `''` for none. */
|
|
41
|
+
export function essenceOf(type: string | null | undefined): string {
|
|
42
|
+
if (type === null || type === undefined) return ''
|
|
43
|
+
const semicolon = type.indexOf(';')
|
|
44
|
+
return (semicolon === -1 ? type : type.slice(0, semicolon))
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/** One frame: an sse event's name — `message` where it named none — `id` and `data`, or a jsonl line. */
|
|
42
50
|
export type Frame = { name: string; id: string | undefined; data: string }
|
|
43
51
|
|
package/src/ui/Router.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { firstMatch, type Params, partsOf } from '#shared/matchRoute.ts'
|
|
|
5
5
|
import { unmounted } from '#shared/mount.ts'
|
|
6
6
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
7
7
|
import { OFFLINE_SAVED } from '#shared/OFFLINE_CACHES.ts'
|
|
8
|
+
import { REACHED_MESSAGE } from '#shared/REACHED_MESSAGE.ts'
|
|
8
9
|
import type { ShareScope } from '#shared/reactive/share.ts'
|
|
9
10
|
import { arrive, route } from '#shared/route.ts'
|
|
10
11
|
import { rankRoutes, type Segment } from '#shared/routePattern.ts'
|
|
@@ -400,14 +401,18 @@ export class Router {
|
|
|
400
401
|
*/
|
|
401
402
|
private writeAddress(target: URL, replace: boolean, left: Scroll): void {
|
|
402
403
|
const full = `${target.pathname}${target.search}${target.hash}`
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
// A step to a fragment, or to the address standing, reaches no page the worker lacks.
|
|
405
|
+
const moved =
|
|
406
|
+
target.pathname !== location.pathname ||
|
|
407
|
+
target.search !== location.search
|
|
408
|
+
if (replace) history.replaceState(entryAt(this.index, null), '', full)
|
|
409
|
+
else {
|
|
410
|
+
this.remember(left)
|
|
411
|
+
this.index += 1
|
|
412
|
+
this.scrolls.length = this.index
|
|
413
|
+
history.pushState(entryAt(this.index, null), '', full)
|
|
406
414
|
}
|
|
407
|
-
|
|
408
|
-
this.index += 1
|
|
409
|
-
this.scrolls.length = this.index
|
|
410
|
-
history.pushState(entryAt(this.index, null), '', full)
|
|
415
|
+
if (moved) reached(target)
|
|
411
416
|
}
|
|
412
417
|
|
|
413
418
|
/**
|
|
@@ -720,6 +725,22 @@ async function savedOffline(target: URL): Promise<boolean> {
|
|
|
720
725
|
return saved !== undefined
|
|
721
726
|
}
|
|
722
727
|
|
|
728
|
+
/**
|
|
729
|
+
* Tells the service worker an address a navigation reached, for it to keep (45.24).
|
|
730
|
+
*
|
|
731
|
+
* POSTED AS THE ADDRESS IS WRITTEN, rather than heard from 30.15's ask: a step within the standing
|
|
732
|
+
* route asks nothing, the ask's redirect lands elsewhere than it asked, and a superseded one writes
|
|
733
|
+
* no address.
|
|
734
|
+
*/
|
|
735
|
+
function reached(at: URL): void {
|
|
736
|
+
const controller = navigator.serviceWorker?.controller
|
|
737
|
+
if (controller == null) return
|
|
738
|
+
// A page is kept by its address, which a fragment is no part of.
|
|
739
|
+
const address = new URL(at)
|
|
740
|
+
address.hash = ''
|
|
741
|
+
controller.postMessage({ [REACHED_MESSAGE]: address.href })
|
|
742
|
+
}
|
|
743
|
+
|
|
723
744
|
/**
|
|
724
745
|
* Asks the server's `middleware` about an address; `undefined` where the fetch failed.
|
|
725
746
|
*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { boundValue, writeBound } from '#shared/boundValue.ts'
|
|
2
|
+
import type { Scope } from './Scope.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Keeps a file input's files in step with a `bind:files` value, both ways.
|
|
6
|
+
*
|
|
7
|
+
* 32.73 — A FILE INPUT'S FILES ARE A LIST, with or without `multiple`, and no markup: the server has
|
|
8
|
+
* none to write. A `FileList` cannot be built, so a list written in is handed over through a
|
|
9
|
+
* `DataTransfer`, and an empty one clears the input through `value`, the one write it takes.
|
|
10
|
+
*/
|
|
11
|
+
export function bindFiles(
|
|
12
|
+
element: Element,
|
|
13
|
+
operand: unknown,
|
|
14
|
+
scope: Scope,
|
|
15
|
+
): void {
|
|
16
|
+
const bound = element as HTMLInputElement
|
|
17
|
+
scope.watch(() => {
|
|
18
|
+
const files = (boundValue(operand) ?? []) as readonly File[]
|
|
19
|
+
if (holdsFiles(bound.files, files)) return
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
bound.value = ''
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
const transfer = new DataTransfer()
|
|
25
|
+
for (const file of files) transfer.items.add(file)
|
|
26
|
+
bound.files = transfer.files
|
|
27
|
+
})
|
|
28
|
+
bound.addEventListener('change', () => {
|
|
29
|
+
const picked: File[] = []
|
|
30
|
+
for (const file of bound.files ?? []) picked.push(file)
|
|
31
|
+
writeBound(operand, picked)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether an input already holds `files`, the same files in the same order. */
|
|
36
|
+
function holdsFiles(held: FileList | null, files: readonly File[]): boolean {
|
|
37
|
+
const count = held?.length ?? 0
|
|
38
|
+
if (count !== files.length) return false
|
|
39
|
+
for (let index = 0; index < count; index++)
|
|
40
|
+
if ((held as FileList)[index] !== files[index]) return false
|
|
41
|
+
return true
|
|
42
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
textHere,
|
|
41
41
|
} from './adoptWalk.ts'
|
|
42
42
|
export { bindElement } from './bindElement.ts'
|
|
43
|
+
export { bindFiles } from './bindFiles.ts'
|
|
43
44
|
export { bindGroup } from './bindGroup.ts'
|
|
44
45
|
export { bindProperty } from './bindProperty.ts'
|
|
45
46
|
export { bindSpread } from './bindSpread.ts'
|