@rei-standard/amsg-shared 0.4.0-next.2 → 0.4.0-next.3

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/README.md CHANGED
@@ -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
 
@@ -85,7 +99,6 @@ fields above are validated by the builders when present.
85
99
  | `title` | `string?` | Notification title. |
86
100
  | `contactName` | `string?` | Sender display name. |
87
101
  | `avatarUrl` | `string \| null?` | Sender avatar URL (`https:` only — `data:` is rejected upstream). |
88
- | `taskId` | `string \| null?` | Scheduled task ID (server only). |
89
102
 
90
103
  ### `ReasoningPush` — LLM meta-thinking
91
104
 
@@ -96,11 +109,17 @@ fields above are validated by the builders when present.
96
109
  | `title` | `string?` | |
97
110
  | `contactName` | `string?` | |
98
111
  | `avatarUrl` | `string \| null?` | |
99
-
100
- **No `messageIndex` / `totalMessages`.** Reasoning is one push per
101
- LLM round, never a split-burst. Those fields are absent at the type
102
- level on purpose — making them optional would leave callers
103
- wondering when they're set.
112
+ | `messageIndex` | `number?` | 1-based part index when a producer sentence-splits reasoning into a burst. Omit for singletons. |
113
+ | `totalMessages` | `number?` | Total parts of that split. Omit for singletons. |
114
+ | `chunkIndex` | `number?` | Transport-only slice index when a single segment exceeds the Web Push payload limit (see `chunkReasoningByUtf8Bytes`). |
115
+ | `totalChunks` | `number?` | Total transport slices. Omit when not sliced. |
116
+
117
+ Both multi-part axes are **omitted from the wire when the part count
118
+ is 1**, so single-shot reasoning stays byte-for-byte identical to
119
+ older payloads. Current producers emit one `ReasoningPush` per LLM
120
+ round and set neither — oversized reasoning rides the generic
121
+ multipart transport instead. The two axes can coexist when a
122
+ sentence-split segment is itself oversized.
104
123
 
105
124
  Emitted **before** the matching `ContentPush` burst when the LLM
106
125
  response carried a non-empty `reasoning_content`.
@@ -116,8 +135,11 @@ response carried a non-empty `reasoning_content`.
116
135
  | `message` | `string?` | Optional human-readable tag for the request. |
117
136
 
118
137
  Emitted by an agentic-loop hook returning
119
- `{ decision: 'tool-request', pushPayload }`. The client is expected
120
- to execute the tool and resume via `/continue`.
138
+ `{ decision: 'tool-request', pushPayloads }`. In the default
139
+ (amsg-instant) flavor the client executes the tools and resumes via
140
+ `/continue`; amsg-server's fire-time loop runs the tools in-process
141
+ instead and may carry a `toolCalls` array directly — see
142
+ `assertValidDecision` below.
121
143
 
122
144
  ### `ErrorPush` — producer-level error
123
145
 
@@ -221,7 +243,7 @@ const error = buildErrorPush({
221
243
  ### Type guards
222
244
 
223
245
  ```js
224
- import { isContentPush, isReasoningPush, isErrorPush } from '@rei-standard/amsg-shared';
246
+ import { isContentPush, isReasoningPush, isToolRequestPush, isErrorPush } from '@rei-standard/amsg-shared';
225
247
 
226
248
  if (isContentPush(push)) {
227
249
  // push.message is `string`
@@ -251,6 +273,68 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
251
273
 
252
274
  ---
253
275
 
276
+ ## Shared building blocks
277
+
278
+ Besides the push schema, this package is the single source of truth
279
+ for helpers that `amsg-instant`, `amsg-server`, and `amsg-sw` used to
280
+ each keep a copy of. All are exported from the package root; the
281
+ one-liners below are just a map — see the JSDoc on each export for
282
+ the full contract.
283
+
284
+ ### Bytes / encoding / crypto
285
+
286
+ `toUint8` · `concatBytes` · `utf8` · `utf8Decode` · `bytesToBase64` ·
287
+ `bytesToBase64Url` · `base64UrlToBytes` · `jsonToBase64Url` ·
288
+ `bytesToHex` · `hexToBytes` · `randomBytes` · `hmacSha256` ·
289
+ `timingSafeEqualBytes` — WebCrypto-friendly byte/encoding helpers for
290
+ base64url, hex, HMAC, and constant-time comparison.
291
+
292
+ ### LLM call
293
+
294
+ | Export | What it is |
295
+ |---|---|
296
+ | `callLlm(...)` | Call an OpenAI-compatible chat-completions endpoint with timeout / abort handling (default 300 000 ms, injectable `fetch`). |
297
+ | `buildLlmRequestBody(...)` | Build the request body for prompt mode or `messages` mode. |
298
+ | `normalizeAiApiUrl(apiUrl)` | Normalize a base URL to its chat-completions endpoint without doubling `/v1`; unrecognized paths pass through unchanged. |
299
+ | `validateLlmMessagesShape(...)` | Validate a `messages` array, including assistant `tool_calls` and `role: 'tool'` entries. |
300
+ | `LLM_MESSAGES_ERROR` | Stable error codes emitted by that validation. |
301
+
302
+ ### Web Push
303
+
304
+ | Export | What it is |
305
+ |---|---|
306
+ | `sendWebPush(...)` | RFC 8291 payload encryption + RFC 8292 VAPID auth + POST to the push service (`err.code = 'PUSH_SEND_FAILED'` on failure). |
307
+ | `buildVapidJwt(...)` / `verifyVapidJwt(...)` | Build / verify the ES256 VAPID JWT. |
308
+ | `normalizeVapidSubject(...)` | Normalize the VAPID subject (e.g. add the `mailto:` prefix). |
309
+
310
+ ### Wire-protocol constants
311
+
312
+ | Export | What it is |
313
+ |---|---|
314
+ | `MULTIPART_MESSAGE_KIND` / `MULTIPART_ENCODING` / `MULTIPART_VERSION` | The generic multipart chunk envelope (`'_multipart'`) produced by `amsg-instant` and reassembled by `amsg-sw`. |
315
+ | `DEFAULT_MULTIPART_TTL_MS` / `DEFAULT_MULTIPART_MAX_CHUNKS` / `DEFAULT_MULTIPART_MAX_TOTAL_BYTES` | Multipart reassembly budget defaults. |
316
+ | `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. |
317
+
318
+ ### Agentic-loop contract
319
+
320
+ | Export | What it is |
321
+ |---|---|
322
+ | `assertValidDecision(decision, options?)` | Runtime-validate an `onLLMOutput` hook decision (`finish` / `tool-request` / `continue` / `skip-push`); `{ inlineToolCalls: true }` enables the amsg-server flavor. |
323
+ | `extractToolCallsFromDecision(...)` | Pull tool calls out of either decision flavor (inline `toolCalls` or tool-request `pushPayloads`). |
324
+ | `buildSessionContext(...)` | Build the frozen, credential-free context object handed to agentic hooks. |
325
+ | `extractAssistantMessage(...)` | Safely read `choices[0].message` off an LLM response (never throws). |
326
+ | `readReasoningContent(...)` | Read `reasoning_content` off an assistant message; empty string means "none". |
327
+ | `stripReasoningTags(...)` | Strip reasoning that leaked into `message.content` so it doesn't ship inside the `ContentPush` burst. |
328
+ | `chunkReasoningByUtf8Bytes(text, maxBytes)` | Split reasoning text on safe UTF-8 edges for payload-limited transports. |
329
+
330
+ ### Validation misc
331
+
332
+ `isValidUrl` · `validateAvatarUrl` · `AVATAR_URL_MAX_LENGTH` — the
333
+ avatar-URL soft-strip rule shared by client / instant / server
334
+ (standards §6.2).
335
+
336
+ ---
337
+
254
338
  ## Invariants
255
339
 
256
340
  1. **`messageKind` is a literal-type discriminator.** Producers must
@@ -260,8 +344,10 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
260
344
  `ReasoningPush` and the `ContentPush`(es) it precedes share the
261
345
  same `sessionId`. Agentic-loop multi-iteration runs reuse the
262
346
  same `sessionId` across iterations.
263
- 3. **`ReasoningPush` carries no `messageIndex` / `totalMessages`.**
264
- Those fields belong to the content N-split burst.
347
+ 3. **Multi-part fields are omitted for singletons.** On `ContentPush`
348
+ and `ReasoningPush` alike, `messageIndex` / `totalMessages` (and
349
+ reasoning's `chunkIndex` / `totalChunks`) only appear on genuine
350
+ multi-part bursts — never as a redundant `1 / 1`.
265
351
  4. **`metadata` is caller-owned.** Packages must add protocol-level
266
352
  data as top-level fields, never inside `metadata`.
267
353
  5. **`source` is the routing origin, not the dispatch type.**
package/dist/index.d.cts CHANGED
@@ -460,6 +460,14 @@ export const AVATAR_URL_MAX_LENGTH: 2048;
460
460
  * `metadata` is a passthrough namespace owned by the caller. Packages
461
461
  * are forbidden from writing their own fields into `metadata` — any
462
462
  * protocol-level data goes on top-level fields.
463
+ *
464
+ * Scheduling identity (`taskId` / `taskUuid` / `recurrenceType` /
465
+ * `occurrenceMs`) is stamped by `@rei-standard/amsg-server` on every push
466
+ * that came out of a scheduled task row. It tells the client which task
467
+ * this is, whether the task will come back, and which nominal fire time
468
+ * produced this burst — so a task the client never created (one the
469
+ * character scheduled for itself at fire time) still arrives fully
470
+ * identified. Pushes with no task behind them (amsg-instant) omit all four.
463
471
  */
464
472
  export type AmsgPushCommon = {
465
473
  /**
@@ -475,7 +483,7 @@ export type AmsgPushCommon = {
475
483
  */
476
484
  messageId: string;
477
485
  /**
478
- * - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request.
486
+ * - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request. Opaque id — do not parse it for task identity, read the fields below.
479
487
  */
480
488
  sessionId: string;
481
489
  /**
@@ -494,6 +502,22 @@ export type AmsgPushCommon = {
494
502
  * - SW notification strategy.
495
503
  */
496
504
  notification?: NotificationDirective;
505
+ /**
506
+ * - Scheduled task row id.
507
+ */
508
+ taskId?: number | string | null;
509
+ /**
510
+ * - Scheduled task uuid (the id the scheduling side chose).
511
+ */
512
+ taskUuid?: string | null;
513
+ /**
514
+ * - Whether that task fires again.
515
+ */
516
+ recurrenceType?: "none" | "daily" | "weekly";
517
+ /**
518
+ * - Nominal fire time of this occurrence (epoch ms).
519
+ */
520
+ occurrenceMs?: number | null;
497
521
  };
498
522
  /**
499
523
  * SW-rendering directive. Mirrors the fields that `amsg-sw`'s
@@ -568,7 +592,6 @@ export type ContentPush = AmsgPushCommon & {
568
592
  avatarUrl?: string | null;
569
593
  messageIndex?: number;
570
594
  totalMessages?: number;
571
- taskId?: string | null;
572
595
  };
573
596
  /**
574
597
  * LLM "meta-thinking" — `choices[0].message.reasoning_content` lifted
package/dist/index.d.ts CHANGED
@@ -460,6 +460,14 @@ export const AVATAR_URL_MAX_LENGTH: 2048;
460
460
  * `metadata` is a passthrough namespace owned by the caller. Packages
461
461
  * are forbidden from writing their own fields into `metadata` — any
462
462
  * protocol-level data goes on top-level fields.
463
+ *
464
+ * Scheduling identity (`taskId` / `taskUuid` / `recurrenceType` /
465
+ * `occurrenceMs`) is stamped by `@rei-standard/amsg-server` on every push
466
+ * that came out of a scheduled task row. It tells the client which task
467
+ * this is, whether the task will come back, and which nominal fire time
468
+ * produced this burst — so a task the client never created (one the
469
+ * character scheduled for itself at fire time) still arrives fully
470
+ * identified. Pushes with no task behind them (amsg-instant) omit all four.
463
471
  */
464
472
  export type AmsgPushCommon = {
465
473
  /**
@@ -475,7 +483,7 @@ export type AmsgPushCommon = {
475
483
  */
476
484
  messageId: string;
477
485
  /**
478
- * - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request.
486
+ * - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request. Opaque id — do not parse it for task identity, read the fields below.
479
487
  */
480
488
  sessionId: string;
481
489
  /**
@@ -494,6 +502,22 @@ export type AmsgPushCommon = {
494
502
  * - SW notification strategy.
495
503
  */
496
504
  notification?: NotificationDirective;
505
+ /**
506
+ * - Scheduled task row id.
507
+ */
508
+ taskId?: number | string | null;
509
+ /**
510
+ * - Scheduled task uuid (the id the scheduling side chose).
511
+ */
512
+ taskUuid?: string | null;
513
+ /**
514
+ * - Whether that task fires again.
515
+ */
516
+ recurrenceType?: "none" | "daily" | "weekly";
517
+ /**
518
+ * - Nominal fire time of this occurrence (epoch ms).
519
+ */
520
+ occurrenceMs?: number | null;
497
521
  };
498
522
  /**
499
523
  * SW-rendering directive. Mirrors the fields that `amsg-sw`'s
@@ -568,7 +592,6 @@ export type ContentPush = AmsgPushCommon & {
568
592
  avatarUrl?: string | null;
569
593
  messageIndex?: number;
570
594
  totalMessages?: number;
571
- taskId?: string | null;
572
595
  };
573
596
  /**
574
597
  * LLM "meta-thinking" — `choices[0].message.reasoning_content` lifted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-shared",
3
- "version": "0.4.0-next.2",
3
+ "version": "0.4.0-next.3",
4
4
  "description": "ReiStandard Active Messaging shared types and push builders — the lowest layer (no deps on other amsg packages)",
5
5
  "repository": {
6
6
  "type": "git",