@rei-standard/amsg-shared 0.4.0-next.1 → 0.4.0-next.11
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/LICENSE +21 -0
- package/README.md +178 -15
- package/dist/index.cjs +932 -29
- package/dist/index.d.cts +249 -26
- package/dist/index.d.ts +249 -26
- package/dist/index.mjs +930 -29
- package/dist/llm-call.d.cts +126 -0
- package/dist/llm-call.d.ts +126 -0
- package/dist/llm-messages.d.cts +66 -0
- package/dist/llm-messages.d.ts +66 -0
- package/dist/multipart.d.cts +26 -0
- package/dist/multipart.d.ts +26 -0
- package/dist/protocol.d.cts +93 -0
- package/dist/protocol.d.ts +93 -0
- package/dist/webcrypto-utils.d.cts +42 -0
- package/dist/webcrypto-utils.d.ts +42 -0
- package/dist/webpush.d.cts +69 -0
- package/dist/webpush.d.ts +69 -0
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ReiStandard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A single push is described by three independent dimensions:
|
|
|
17
17
|
|----------------|-------------------|-------------------------------------------------------|--------------------|
|
|
18
18
|
| Dispatch | `messageType` | `instant` / `fixed` / `prompted` / `auto` | Package (fixed) |
|
|
19
19
|
| Business | `messageSubtype` | Any string | Caller (free-form) |
|
|
20
|
-
| Content | `messageKind` | `content` / `reasoning` / `tool_request` / `error`
|
|
20
|
+
| Content | `messageKind` | `content` / `reasoning` / `tool_request` / `error` / `result` | Package (fixed) |
|
|
21
21
|
|
|
22
22
|
`messageType` answers **how this push was produced** (one-shot
|
|
23
23
|
`instant` worker, scheduled `fixed` ping, AI-`prompted` reply, fully
|
|
@@ -41,10 +41,24 @@ origin** (`'instant'` for `amsg-instant`, `'scheduled'` for any
|
|
|
41
41
|
| `messageType` | `MessageType` | Dispatch axis. |
|
|
42
42
|
| `source` | `'instant' \| 'scheduled'` | Routing origin. |
|
|
43
43
|
| `messageId` | `string` | Unique per push. Format owned by the producer. |
|
|
44
|
-
| `sessionId` | `string` | **Shared across all pushes from the same LLM round** (reasoning + content), and across all iterations of a single agentic-loop request. |
|
|
44
|
+
| `sessionId` | `string` | **Shared across all pushes from the same LLM round** (reasoning + content), and across all iterations of a single agentic-loop request. Opaque — do not parse it for task identity, read the scheduling fields below. |
|
|
45
45
|
| `timestamp` | `string` (ISO 8601) | Producer-side wall clock. |
|
|
46
46
|
| `messageSubtype` | `string?` | Caller's business namespace. Defaults to `'chat'` at producers. |
|
|
47
47
|
| `metadata` | `object?` | **Caller passthrough.** Packages MUST NOT write here. |
|
|
48
|
+
| `taskId` | `number \| string \| null?` | Scheduled task row id. |
|
|
49
|
+
| `taskUuid` | `string \| null?` | Scheduled task uuid (the id the scheduling side chose). |
|
|
50
|
+
| `recurrenceType` | `'none' \| 'daily' \| 'weekly'?` | Whether that task fires again. |
|
|
51
|
+
| `occurrenceMs` | `number \| null?` | Nominal fire time of this occurrence (epoch ms). |
|
|
52
|
+
|
|
53
|
+
### Scheduling identity
|
|
54
|
+
|
|
55
|
+
`taskId` / `taskUuid` / `recurrenceType` / `occurrenceMs` are stamped by
|
|
56
|
+
`@rei-standard/amsg-server` on every push that came out of a scheduled task
|
|
57
|
+
row. They tell the client which task this is, whether the task will come
|
|
58
|
+
back, and which nominal fire time produced this burst — so a task the client
|
|
59
|
+
never created (one the character scheduled for itself at fire time) still
|
|
60
|
+
arrives fully identified. Pushes with no task behind them (`amsg-instant`)
|
|
61
|
+
omit all four.
|
|
48
62
|
|
|
49
63
|
---
|
|
50
64
|
|
|
@@ -56,7 +70,7 @@ origin** (`'instant'` for `amsg-instant`, `'scheduled'` for any
|
|
|
56
70
|
|
|
57
71
|
| Field | Type | Notes |
|
|
58
72
|
|----------------------|-------------------------------------------|-------|
|
|
59
|
-
| `show` | `'auto' \| 'always' \| 'when-hidden' \| false` | Display policy
|
|
73
|
+
| `show` | `'auto' \| 'always' \| 'when-hidden' \| false` | Display policy — see [选哪个 `show`](#选哪个-show). |
|
|
60
74
|
| `title` | `string?` | Notification title override. |
|
|
61
75
|
| `body` | `string?` | Notification body override. |
|
|
62
76
|
| `icon` | `string?` | Notification icon URL. |
|
|
@@ -64,12 +78,56 @@ origin** (`'instant'` for `amsg-instant`, `'scheduled'` for any
|
|
|
64
78
|
| `tag` | `string?` | Notification grouping tag. |
|
|
65
79
|
| `renotify` | `boolean?` | Re-alert when a matching `tag` replaces an existing notification. |
|
|
66
80
|
| `requireInteraction` | `boolean?` | Keep the notification visible until the user dismisses it. |
|
|
67
|
-
| `silent` | `boolean?`
|
|
81
|
+
| `silent` | `(boolean \| 'when-visible')?` | Suppress notification sound and vibration — see [选哪个 `silent`](#选哪个-silent). |
|
|
68
82
|
| `data` | `Record<string, unknown>?` | Custom data passed to the notification. |
|
|
69
83
|
|
|
70
84
|
Unknown fields are preserved for forward compatibility, but the known
|
|
71
85
|
fields above are validated by the builders when present.
|
|
72
86
|
|
|
87
|
+
### 选哪个 `show`
|
|
88
|
+
|
|
89
|
+
订阅是按 `userVisibleOnly: true` 建的,收到 push 却不弹通知就是违约:Chrome 替你弹一条通用横幅,Firefox 有配额、超了退订,iOS 给新订阅几天宽限期(跟条数无关)、过期后一条不弹就吊销订阅。所以口径只有一条,跟机型无关——**要推就一定弹,不想弹就别推。**
|
|
90
|
+
|
|
91
|
+
| 值 | SW 那边 | 什么时候用 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| 不配 / `'auto'` | 按 `messageKind` 走默认(`content` / `result` 弹,其余不弹) | 默认,多数 payload 不用管 |
|
|
94
|
+
| `'always'` | 一定弹 | 要推的一律用它。嫌打扰配 `tag` 折叠加 `silent`,而不是不弹 |
|
|
95
|
+
| `false` | 一定不弹 | 明说这条不弹。有收件箱的发送端据此**根本不发这条 push**,内容落收件箱等客户端补拉 |
|
|
96
|
+
| `'when-hidden'` | 有可见窗口就不弹 | 兼容档,新代码不选——应用在前台时它就是一条不弹的 push,那笔账照记 |
|
|
97
|
+
|
|
98
|
+
所以 `show: false` 在发送端和接收端不是同一件事:有服务端收件箱的发送端(`@rei-standard/amsg-server` 单用户线)见到它压根不发,SW 那边收不到;没有收件箱的发送端才是「推过去、SW 不弹」。
|
|
99
|
+
|
|
100
|
+
想要「立刻推到、但页面自己渲染」没有专门的档:用 `'always'` + `tag` + `silent`,页面自绘不受影响(`postMessage` 跟弹不弹通知无关)。
|
|
101
|
+
|
|
102
|
+
完整取舍见 [`@rei-standard/amsg-sw` README 的「不展示通知的代价」](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/sw/README.md#不展示通知的代价)。
|
|
103
|
+
|
|
104
|
+
### 选哪个 `silent`
|
|
105
|
+
|
|
106
|
+
`silent` 管的是「响不响铃、震不震」,跟弹不弹(`show`)是两件独立的事——通知照样进通知中心,只是安静地进。
|
|
107
|
+
|
|
108
|
+
| 值 | SW 那边 | 什么时候用 |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| 不配 / `false` | 正常响铃震动 | 默认 |
|
|
111
|
+
| `true` | 一律不响 | 一串连着来的通知配 `tag` 折叠,只想在通知中心留个痕迹 |
|
|
112
|
+
| `'when-visible'` | 有可见窗口就静音,没有就照常响 | 页面自己会把内容画出来的那类消息:用户正看着页面就别再响一声,人切后台了照样叫得动 |
|
|
113
|
+
|
|
114
|
+
`'when-visible'` 只能由 Service Worker 当场算:发送端发推那一刻并不知道用户此刻在不在前台,写死 `silent: true` 的话,用户切到后台收到的那条也不会响。判定用的是跟 `show: 'when-hidden'` 同一套窗口可见性口径,一条 payload 只算一次。
|
|
115
|
+
|
|
116
|
+
### `notificationIntent(payload)`
|
|
117
|
+
|
|
118
|
+
把上面这套规则算成一个值,`'always'` / `'when-hidden'` / `'never'`:`notification.show` 说了算,没说才按 `messageKind` 走默认。两端读同一份——SW 拿它决定要不要 `showNotification`,发送端拿它决定这条值不值得占用推送通道(`'never'` 的 payload 推过去不会有任何可见反馈,`@rei-standard/amsg-server` 只把它落进收件箱,等客户端上线 `GET /outbox?since=` 补拉)。
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
import { notificationIntent } from '@rei-standard/amsg-shared';
|
|
122
|
+
|
|
123
|
+
notificationIntent({ messageKind: 'content' }); // 'always'
|
|
124
|
+
notificationIntent({ messageKind: 'reasoning' }); // 'never'
|
|
125
|
+
notificationIntent({ messageKind: 'reasoning', notification: { show: 'always' } }); // 'always'
|
|
126
|
+
notificationIntent({ messageKind: 'content', notification: { show: false } }); // 'never'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`'when-hidden'` 单独占一档,因为它到底弹不弹要看当下有没有可见窗口,那只有 SW 知道;发送端把它当「可能会弹」照发。
|
|
130
|
+
|
|
73
131
|
---
|
|
74
132
|
|
|
75
133
|
## Per-kind fields
|
|
@@ -85,7 +143,6 @@ fields above are validated by the builders when present.
|
|
|
85
143
|
| `title` | `string?` | Notification title. |
|
|
86
144
|
| `contactName` | `string?` | Sender display name. |
|
|
87
145
|
| `avatarUrl` | `string \| null?` | Sender avatar URL (`https:` only — `data:` is rejected upstream). |
|
|
88
|
-
| `taskId` | `string \| null?` | Scheduled task ID (server only). |
|
|
89
146
|
|
|
90
147
|
### `ReasoningPush` — LLM meta-thinking
|
|
91
148
|
|
|
@@ -96,11 +153,17 @@ fields above are validated by the builders when present.
|
|
|
96
153
|
| `title` | `string?` | |
|
|
97
154
|
| `contactName` | `string?` | |
|
|
98
155
|
| `avatarUrl` | `string \| null?` | |
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
| `messageIndex` | `number?` | 1-based part index when a producer sentence-splits reasoning into a burst. Omit for singletons. |
|
|
157
|
+
| `totalMessages` | `number?` | Total parts of that split. Omit for singletons. |
|
|
158
|
+
| `chunkIndex` | `number?` | Transport-only slice index when a single segment exceeds the Web Push payload limit (see `chunkReasoningByUtf8Bytes`). |
|
|
159
|
+
| `totalChunks` | `number?` | Total transport slices. Omit when not sliced. |
|
|
160
|
+
|
|
161
|
+
Both multi-part axes are **omitted from the wire when the part count
|
|
162
|
+
is 1**, so single-shot reasoning stays byte-for-byte identical to
|
|
163
|
+
older payloads. Current producers emit one `ReasoningPush` per LLM
|
|
164
|
+
round and set neither — oversized reasoning rides the generic
|
|
165
|
+
multipart transport instead. The two axes can coexist when a
|
|
166
|
+
sentence-split segment is itself oversized.
|
|
104
167
|
|
|
105
168
|
Emitted **before** the matching `ContentPush` burst when the LLM
|
|
106
169
|
response carried a non-empty `reasoning_content`.
|
|
@@ -116,8 +179,11 @@ response carried a non-empty `reasoning_content`.
|
|
|
116
179
|
| `message` | `string?` | Optional human-readable tag for the request. |
|
|
117
180
|
|
|
118
181
|
Emitted by an agentic-loop hook returning
|
|
119
|
-
`{ decision: 'tool-request',
|
|
120
|
-
|
|
182
|
+
`{ decision: 'tool-request', pushPayloads }`. In the default
|
|
183
|
+
(amsg-instant) flavor the client executes the tools and resumes via
|
|
184
|
+
`/continue`; amsg-server's fire-time loop runs the tools in-process
|
|
185
|
+
instead and may carry a `toolCalls` array directly — see
|
|
186
|
+
`assertValidDecision` below.
|
|
121
187
|
|
|
122
188
|
### `ErrorPush` — producer-level error
|
|
123
189
|
|
|
@@ -132,6 +198,22 @@ Replaces the legacy 0.7.0 `{ type: 'error', code: '...' }` envelope.
|
|
|
132
198
|
The legacy `type` field is **gone** — do not look for it on
|
|
133
199
|
`ErrorPush`.
|
|
134
200
|
|
|
201
|
+
### `ResultPush` — 宿主自定义的一条结果
|
|
202
|
+
|
|
203
|
+
| Field | Type | Notes |
|
|
204
|
+
|---------------|------------|----------------------------------------------------------------|
|
|
205
|
+
| `messageKind` | `'result'` | Discriminator. |
|
|
206
|
+
| `resultKind` | `string` | 宿主给这类结果起的名字(`'fire-pack'`、`'ledger-entry'`……),客户端按它分流。 |
|
|
207
|
+
| `title` | `string?` | 通知标题的兜底(`notification.title` 优先)。 |
|
|
208
|
+
| `body` | `string?` | 通知正文的兜底(`notification.body` 优先)。 |
|
|
209
|
+
|
|
210
|
+
不是聊天内容,而是「这次跑完产出了点什么,客户端拿去自己消化」:整理好的一份数据、一条账目、后台生成的产物。形状由宿主定,`buildResultPush` 是唯一**保留自己不认识的字段**的 builder——白名单式的复制会把内容删掉一半。
|
|
211
|
+
|
|
212
|
+
两处与别的 kind 不同:
|
|
213
|
+
|
|
214
|
+
- **投递路径**:产出方(`@rei-standard/amsg-server` 的 `ctx.emitResult()`)除了推送,还把它落进服务端收件箱,客户端下次 `GET /outbox?since=` 一定拿得到。
|
|
215
|
+
- **通知**:SW 侧默认弹(与 `content` 同待遇,其余三种是静默送给页面)——结果往往正是「跑完了,回来看看」那句话。不想弹就带 `notification: { show: false }`:这一档 `amsg-server` 不发推送、只落收件箱,客户端补拉时拿到(见[选哪个 `show`](#选哪个-show))。
|
|
216
|
+
|
|
135
217
|
---
|
|
136
218
|
|
|
137
219
|
## Usage
|
|
@@ -162,6 +244,9 @@ function dispatch(push: AmsgPush) {
|
|
|
162
244
|
case 'error':
|
|
163
245
|
console.error(push.code, push.message);
|
|
164
246
|
break;
|
|
247
|
+
case 'result':
|
|
248
|
+
// push.resultKind is `string` — 宿主自己的结果,按它分流
|
|
249
|
+
break;
|
|
165
250
|
}
|
|
166
251
|
}
|
|
167
252
|
```
|
|
@@ -174,6 +259,7 @@ import {
|
|
|
174
259
|
buildReasoningPush,
|
|
175
260
|
buildToolRequestPush,
|
|
176
261
|
buildErrorPush,
|
|
262
|
+
buildResultPush,
|
|
177
263
|
} from '@rei-standard/amsg-shared';
|
|
178
264
|
|
|
179
265
|
// One sentence in an N-split burst
|
|
@@ -216,12 +302,24 @@ const error = buildErrorPush({
|
|
|
216
302
|
message: 'onLLMOutput threw: ...',
|
|
217
303
|
iteration: 2,
|
|
218
304
|
});
|
|
305
|
+
|
|
306
|
+
// 宿主自定义的一条结果(认识以外的字段原样保留)
|
|
307
|
+
const result = buildResultPush({
|
|
308
|
+
messageType: 'auto',
|
|
309
|
+
source: 'scheduled',
|
|
310
|
+
messageId: 'msg_task_7@1700000000000_result_0',
|
|
311
|
+
sessionId: 'sess_abc',
|
|
312
|
+
resultKind: 'fire-pack',
|
|
313
|
+
packId: 'pack_42',
|
|
314
|
+
entries: [{ id: 1 }, { id: 2 }],
|
|
315
|
+
notification: { title: '整理好了', body: '点开看看' },
|
|
316
|
+
});
|
|
219
317
|
```
|
|
220
318
|
|
|
221
319
|
### Type guards
|
|
222
320
|
|
|
223
321
|
```js
|
|
224
|
-
import { isContentPush, isReasoningPush, isErrorPush } from '@rei-standard/amsg-shared';
|
|
322
|
+
import { isContentPush, isReasoningPush, isToolRequestPush, isErrorPush, isResultPush } from '@rei-standard/amsg-shared';
|
|
225
323
|
|
|
226
324
|
if (isContentPush(push)) {
|
|
227
325
|
// push.message is `string`
|
|
@@ -239,6 +337,7 @@ MESSAGE_KIND.CONTENT; // 'content'
|
|
|
239
337
|
MESSAGE_KIND.REASONING; // 'reasoning'
|
|
240
338
|
MESSAGE_KIND.TOOL_REQUEST; // 'tool_request'
|
|
241
339
|
MESSAGE_KIND.ERROR; // 'error'
|
|
340
|
+
MESSAGE_KIND.RESULT; // 'result'
|
|
242
341
|
|
|
243
342
|
MESSAGE_TYPE.INSTANT; // 'instant'
|
|
244
343
|
MESSAGE_TYPE.FIXED; // 'fixed'
|
|
@@ -251,6 +350,68 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
|
|
|
251
350
|
|
|
252
351
|
---
|
|
253
352
|
|
|
353
|
+
## Shared building blocks
|
|
354
|
+
|
|
355
|
+
Besides the push schema, this package is the single source of truth
|
|
356
|
+
for helpers that `amsg-instant`, `amsg-server`, and `amsg-sw` used to
|
|
357
|
+
each keep a copy of. All are exported from the package root; the
|
|
358
|
+
one-liners below are just a map — see the JSDoc on each export for
|
|
359
|
+
the full contract.
|
|
360
|
+
|
|
361
|
+
### Bytes / encoding / crypto
|
|
362
|
+
|
|
363
|
+
`toUint8` · `concatBytes` · `utf8` · `utf8Decode` · `bytesToBase64` ·
|
|
364
|
+
`bytesToBase64Url` · `base64UrlToBytes` · `jsonToBase64Url` ·
|
|
365
|
+
`bytesToHex` · `hexToBytes` · `randomBytes` · `hmacSha256` ·
|
|
366
|
+
`timingSafeEqualBytes` — WebCrypto-friendly byte/encoding helpers for
|
|
367
|
+
base64url, hex, HMAC, and constant-time comparison.
|
|
368
|
+
|
|
369
|
+
### LLM call
|
|
370
|
+
|
|
371
|
+
| Export | What it is |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `callLlm(...)` | Call an OpenAI-compatible chat-completions endpoint with timeout / abort handling (default 300 000 ms, injectable `fetch`). |
|
|
374
|
+
| `buildLlmRequestBody(...)` | Build the request body for prompt mode or `messages` mode. |
|
|
375
|
+
| `normalizeAiApiUrl(apiUrl)` | Normalize a base URL to its chat-completions endpoint without doubling `/v1`; unrecognized paths pass through unchanged. |
|
|
376
|
+
| `validateLlmMessagesShape(...)` | Validate a `messages` array, including assistant `tool_calls` and `role: 'tool'` entries. |
|
|
377
|
+
| `LLM_MESSAGES_ERROR` | Stable error codes emitted by that validation. |
|
|
378
|
+
|
|
379
|
+
### Web Push
|
|
380
|
+
|
|
381
|
+
| Export | What it is |
|
|
382
|
+
|---|---|
|
|
383
|
+
| `sendWebPush(...)` | RFC 8291 payload encryption + RFC 8292 VAPID auth + POST to the push service (`err.code = 'PUSH_SEND_FAILED'` on failure). |
|
|
384
|
+
| `buildVapidJwt(...)` / `verifyVapidJwt(...)` | Build / verify the ES256 VAPID JWT. |
|
|
385
|
+
| `normalizeVapidSubject(...)` | Normalize the VAPID subject (e.g. add the `mailto:` prefix). |
|
|
386
|
+
|
|
387
|
+
### Wire-protocol constants
|
|
388
|
+
|
|
389
|
+
| Export | What it is |
|
|
390
|
+
|---|---|
|
|
391
|
+
| `MULTIPART_MESSAGE_KIND` / `MULTIPART_ENCODING` / `MULTIPART_VERSION` | The generic multipart chunk envelope (`'_multipart'`) produced by `amsg-instant` and reassembled by `amsg-sw`. |
|
|
392
|
+
| `DEFAULT_MULTIPART_TTL_MS` / `DEFAULT_MULTIPART_MAX_CHUNKS` / `DEFAULT_MULTIPART_MAX_TOTAL_BYTES` | Multipart reassembly budget defaults. |
|
|
393
|
+
| `REI_AMSG_POSTMESSAGE_TYPE` / `REI_SW_EVENT` / `REI_SW_MESSAGE_TYPE` / `REI_AMSG_DELIVER_MESSAGE_TYPE` | The window ⇄ Service Worker `postMessage` protocol strings — import these instead of hard-coding the literals. |
|
|
394
|
+
|
|
395
|
+
### Agentic-loop contract
|
|
396
|
+
|
|
397
|
+
| Export | What it is |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `assertValidDecision(decision, options?)` | Runtime-validate an `onLLMOutput` hook decision (`finish` / `tool-request` / `continue` / `skip-push`); `{ inlineToolCalls: true }` enables the amsg-server flavor. |
|
|
400
|
+
| `extractToolCallsFromDecision(...)` | Pull tool calls out of either decision flavor (inline `toolCalls` or tool-request `pushPayloads`). |
|
|
401
|
+
| `buildSessionContext(...)` | Build the frozen, credential-free context object handed to agentic hooks. |
|
|
402
|
+
| `extractAssistantMessage(...)` | Safely read `choices[0].message` off an LLM response (never throws). |
|
|
403
|
+
| `readReasoningContent(...)` | Read `reasoning_content` off an assistant message; empty string means "none". |
|
|
404
|
+
| `stripReasoningTags(...)` | Strip reasoning that leaked into `message.content` so it doesn't ship inside the `ContentPush` burst. |
|
|
405
|
+
| `chunkReasoningByUtf8Bytes(text, maxBytes)` | Split reasoning text on safe UTF-8 edges for payload-limited transports. |
|
|
406
|
+
|
|
407
|
+
### Validation misc
|
|
408
|
+
|
|
409
|
+
`isValidUrl` · `validateAvatarUrl` · `AVATAR_URL_MAX_LENGTH` — the
|
|
410
|
+
avatar-URL soft-strip rule shared by client / instant / server
|
|
411
|
+
(standards §6.2).
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
254
415
|
## Invariants
|
|
255
416
|
|
|
256
417
|
1. **`messageKind` is a literal-type discriminator.** Producers must
|
|
@@ -260,8 +421,10 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
|
|
|
260
421
|
`ReasoningPush` and the `ContentPush`(es) it precedes share the
|
|
261
422
|
same `sessionId`. Agentic-loop multi-iteration runs reuse the
|
|
262
423
|
same `sessionId` across iterations.
|
|
263
|
-
3.
|
|
264
|
-
|
|
424
|
+
3. **Multi-part fields are omitted for singletons.** On `ContentPush`
|
|
425
|
+
and `ReasoningPush` alike, `messageIndex` / `totalMessages` (and
|
|
426
|
+
reasoning's `chunkIndex` / `totalChunks`) only appear on genuine
|
|
427
|
+
multi-part bursts — never as a redundant `1 / 1`.
|
|
265
428
|
4. **`metadata` is caller-owned.** Packages must add protocol-level
|
|
266
429
|
data as top-level fields, never inside `metadata`.
|
|
267
430
|
5. **`source` is the routing origin, not the dispatch type.**
|