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

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
@@ -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` | Package (fixed) |
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
@@ -154,6 +154,22 @@ Replaces the legacy 0.7.0 `{ type: 'error', code: '...' }` envelope.
154
154
  The legacy `type` field is **gone** — do not look for it on
155
155
  `ErrorPush`.
156
156
 
157
+ ### `ResultPush` — 宿主自定义的一条结果
158
+
159
+ | Field | Type | Notes |
160
+ |---------------|------------|----------------------------------------------------------------|
161
+ | `messageKind` | `'result'` | Discriminator. |
162
+ | `resultKind` | `string` | 宿主给这类结果起的名字(`'fire-pack'`、`'ledger-entry'`……),客户端按它分流。 |
163
+ | `title` | `string?` | 通知标题的兜底(`notification.title` 优先)。 |
164
+ | `body` | `string?` | 通知正文的兜底(`notification.body` 优先)。 |
165
+
166
+ 不是聊天内容,而是「这次跑完产出了点什么,客户端拿去自己消化」:整理好的一份数据、一条账目、后台生成的产物。形状由宿主定,`buildResultPush` 是唯一**保留自己不认识的字段**的 builder——白名单式的复制会把内容删掉一半。
167
+
168
+ 两处与别的 kind 不同:
169
+
170
+ - **投递路径**:产出方(`@rei-standard/amsg-server` 的 `ctx.emitResult()`)除了推送,还把它落进服务端收件箱,客户端下次 `GET /outbox?since=` 一定拿得到。
171
+ - **通知**:SW 侧默认弹(与 `content` 同待遇,其余三种是静默送给页面)——结果往往正是「跑完了,回来看看」那句话。不想弹就带 `notification: { show: false }`。
172
+
157
173
  ---
158
174
 
159
175
  ## Usage
@@ -184,6 +200,9 @@ function dispatch(push: AmsgPush) {
184
200
  case 'error':
185
201
  console.error(push.code, push.message);
186
202
  break;
203
+ case 'result':
204
+ // push.resultKind is `string` — 宿主自己的结果,按它分流
205
+ break;
187
206
  }
188
207
  }
189
208
  ```
@@ -196,6 +215,7 @@ import {
196
215
  buildReasoningPush,
197
216
  buildToolRequestPush,
198
217
  buildErrorPush,
218
+ buildResultPush,
199
219
  } from '@rei-standard/amsg-shared';
200
220
 
201
221
  // One sentence in an N-split burst
@@ -238,12 +258,24 @@ const error = buildErrorPush({
238
258
  message: 'onLLMOutput threw: ...',
239
259
  iteration: 2,
240
260
  });
261
+
262
+ // 宿主自定义的一条结果(认识以外的字段原样保留)
263
+ const result = buildResultPush({
264
+ messageType: 'auto',
265
+ source: 'scheduled',
266
+ messageId: 'msg_task_7@1700000000000_result_0',
267
+ sessionId: 'sess_abc',
268
+ resultKind: 'fire-pack',
269
+ packId: 'pack_42',
270
+ entries: [{ id: 1 }, { id: 2 }],
271
+ notification: { title: '整理好了', body: '点开看看' },
272
+ });
241
273
  ```
242
274
 
243
275
  ### Type guards
244
276
 
245
277
  ```js
246
- import { isContentPush, isReasoningPush, isToolRequestPush, isErrorPush } from '@rei-standard/amsg-shared';
278
+ import { isContentPush, isReasoningPush, isToolRequestPush, isErrorPush, isResultPush } from '@rei-standard/amsg-shared';
247
279
 
248
280
  if (isContentPush(push)) {
249
281
  // push.message is `string`
@@ -261,6 +293,7 @@ MESSAGE_KIND.CONTENT; // 'content'
261
293
  MESSAGE_KIND.REASONING; // 'reasoning'
262
294
  MESSAGE_KIND.TOOL_REQUEST; // 'tool_request'
263
295
  MESSAGE_KIND.ERROR; // 'error'
296
+ MESSAGE_KIND.RESULT; // 'result'
264
297
 
265
298
  MESSAGE_TYPE.INSTANT; // 'instant'
266
299
  MESSAGE_TYPE.FIXED; // 'fixed'
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var src_exports = {};
21
21
  __export(src_exports, {
22
22
  AVATAR_URL_MAX_LENGTH: () => AVATAR_URL_MAX_LENGTH,
23
+ DEFAULT_MULTIPART_CHUNK_BYTES: () => DEFAULT_MULTIPART_CHUNK_BYTES,
23
24
  DEFAULT_MULTIPART_MAX_CHUNKS: () => DEFAULT_MULTIPART_MAX_CHUNKS,
24
25
  DEFAULT_MULTIPART_MAX_TOTAL_BYTES: () => DEFAULT_MULTIPART_MAX_TOTAL_BYTES,
25
26
  DEFAULT_MULTIPART_TTL_MS: () => DEFAULT_MULTIPART_TTL_MS,
@@ -27,6 +28,7 @@ __export(src_exports, {
27
28
  MESSAGE_KIND: () => MESSAGE_KIND,
28
29
  MESSAGE_TYPE: () => MESSAGE_TYPE,
29
30
  MULTIPART_ENCODING: () => MULTIPART_ENCODING,
31
+ MULTIPART_FAILURE_REASON: () => MULTIPART_FAILURE_REASON,
30
32
  MULTIPART_MESSAGE_KIND: () => MULTIPART_MESSAGE_KIND,
31
33
  MULTIPART_VERSION: () => MULTIPART_VERSION,
32
34
  PUSH_SOURCE: () => PUSH_SOURCE,
@@ -39,7 +41,9 @@ __export(src_exports, {
39
41
  buildContentPush: () => buildContentPush,
40
42
  buildErrorPush: () => buildErrorPush,
41
43
  buildLlmRequestBody: () => buildLlmRequestBody,
44
+ buildMultipartPushPayloads: () => buildMultipartPushPayloads,
42
45
  buildReasoningPush: () => buildReasoningPush,
46
+ buildResultPush: () => buildResultPush,
43
47
  buildSessionContext: () => buildSessionContext,
44
48
  buildToolRequestPush: () => buildToolRequestPush,
45
49
  buildVapidJwt: () => buildVapidJwt,
@@ -56,6 +60,7 @@ __export(src_exports, {
56
60
  isContentPush: () => isContentPush,
57
61
  isErrorPush: () => isErrorPush,
58
62
  isReasoningPush: () => isReasoningPush,
63
+ isResultPush: () => isResultPush,
59
64
  isToolRequestPush: () => isToolRequestPush,
60
65
  isValidUrl: () => isValidUrl,
61
66
  jsonToBase64Url: () => jsonToBase64Url,
@@ -64,6 +69,7 @@ __export(src_exports, {
64
69
  randomBytes: () => randomBytes,
65
70
  randomUUID: () => randomUUID,
66
71
  readReasoningContent: () => readReasoningContent,
72
+ redactCredentials: () => redactCredentials,
67
73
  sendWebPush: () => sendWebPush,
68
74
  stripReasoningTags: () => stripReasoningTags,
69
75
  timingSafeEqualBytes: () => timingSafeEqualBytes,
@@ -230,6 +236,9 @@ function validateLlmMessagesShape(messages) {
230
236
  }
231
237
 
232
238
  // src/llm-call.js
239
+ var UPSTREAM_ERROR_DETAIL_MAX_CHARS = 300;
240
+ var UPSTREAM_ERROR_CODE_MAX_CHARS = 64;
241
+ var UPSTREAM_ERROR_BODY_MAX_BYTES = 16 * 1024;
233
242
  async function callLlm(payload, options = {}) {
234
243
  const requireContent = options.requireContent !== false;
235
244
  const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 3e5;
@@ -246,13 +255,18 @@ async function callLlm(payload, options = {}) {
246
255
  signal: AbortSignal.timeout(timeoutMs)
247
256
  });
248
257
  if (!aiResponse.ok) {
258
+ const detail = await readUpstreamErrorDetail(aiResponse);
249
259
  if (aiResponse.status === 405) {
250
- throw new Error(
251
- `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
260
+ throw buildUpstreamError(
261
+ `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`,
262
+ aiResponse.status,
263
+ detail
252
264
  );
253
265
  }
254
- throw new Error(
255
- `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
266
+ throw buildUpstreamError(
267
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`,
268
+ aiResponse.status,
269
+ detail
256
270
  );
257
271
  }
258
272
  const aiData = await aiResponse.json();
@@ -264,7 +278,10 @@ async function callLlm(payload, options = {}) {
264
278
  }
265
279
  function buildLlmRequestBody(payload, options = {}) {
266
280
  const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
281
+ const extraBody = payload.llmExtraBody && typeof payload.llmExtraBody === "object" && !Array.isArray(payload.llmExtraBody) ? payload.llmExtraBody : null;
267
282
  const requestBody = {
283
+ // 先展开 extra body,核心字段随后写入(撞键时核心字段赢)。
284
+ ...extraBody || {},
268
285
  model: payload.primaryModel,
269
286
  messages: llmMessages
270
287
  };
@@ -316,6 +333,169 @@ function normalizeAiApiUrl(apiUrl) {
316
333
  parsed.pathname = path;
317
334
  return parsed.toString();
318
335
  }
336
+ function buildUpstreamError(summary, status, detail) {
337
+ const error = new Error(
338
+ summary + (detail.message ? ` \u2014 ${detail.message}` : "") + (detail.code ? ` (provider code: ${detail.code})` : "")
339
+ );
340
+ error.code = "LLM_CALL_FAILED";
341
+ error.llmStatus = status;
342
+ if (detail.code) error.providerCode = detail.code;
343
+ return error;
344
+ }
345
+ async function readUpstreamErrorDetail(response) {
346
+ let raw;
347
+ let truncated = false;
348
+ try {
349
+ ({ text: raw, truncated } = await readBoundedBody(response));
350
+ } catch {
351
+ return { message: "", code: "" };
352
+ }
353
+ if (typeof raw !== "string" || !raw.trim()) return { message: "", code: "" };
354
+ let body;
355
+ try {
356
+ body = JSON.parse(raw);
357
+ } catch {
358
+ if (truncated && looksLikeJson(raw)) {
359
+ const salvaged = salvageJsonStringFields(raw);
360
+ return {
361
+ message: clampDetail(salvaged.message || TRUNCATED_BODY_NOTE),
362
+ code: clampCode(salvaged.code)
363
+ };
364
+ }
365
+ return { message: clampDetail(raw), code: "" };
366
+ }
367
+ const envelope = body && typeof body === "object" ? body : {};
368
+ const inner = envelope.error && typeof envelope.error === "object" ? envelope.error : {};
369
+ const message = firstNonEmptyString(
370
+ inner.message,
371
+ // OpenAI / Anthropic / Gemini
372
+ typeof envelope.error === "string" ? envelope.error : "",
373
+ // `{ error: "unauthorized" }`
374
+ envelope.message,
375
+ // 一批中转把 message 放最外层
376
+ envelope.detail
377
+ // FastAPI 风格的自建中转
378
+ ) || raw;
379
+ const code = firstNonEmptyString(
380
+ inner.code,
381
+ // OpenAI:invalid_api_key / insufficient_quota / context_length_exceeded
382
+ inner.status,
383
+ // Gemini:INVALID_ARGUMENT / RESOURCE_EXHAUSTED
384
+ inner.type,
385
+ // Anthropic:invalid_request_error / overloaded_error
386
+ envelope.code
387
+ );
388
+ return { message: clampDetail(message), code: clampCode(code) };
389
+ }
390
+ async function readBoundedBody(response) {
391
+ const body = response && response.body;
392
+ if (!body || typeof body.getReader !== "function") {
393
+ const text = typeof response.text === "function" ? await response.text() : "";
394
+ return { text, truncated: false };
395
+ }
396
+ const reader = body.getReader();
397
+ const chunks = [];
398
+ let bytes = 0;
399
+ try {
400
+ while (bytes < UPSTREAM_ERROR_BODY_MAX_BYTES) {
401
+ const { done, value } = await reader.read();
402
+ if (done) break;
403
+ if (!value || !value.length) continue;
404
+ chunks.push(value);
405
+ bytes += value.length;
406
+ }
407
+ } finally {
408
+ try {
409
+ await reader.cancel();
410
+ } catch {
411
+ }
412
+ }
413
+ return {
414
+ text: utf8Decode(concatBytes(...chunks)),
415
+ truncated: bytes >= UPSTREAM_ERROR_BODY_MAX_BYTES
416
+ };
417
+ }
418
+ var TRUNCATED_BODY_NOTE = "upstream error body was truncated before any readable message";
419
+ var JSON_STRING_FIELD = /"(message|detail|code|status|type)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
420
+ function looksLikeJson(raw) {
421
+ return /^\s*[{[]/.test(raw);
422
+ }
423
+ function salvageJsonStringFields(raw) {
424
+ const found = {};
425
+ for (const match of raw.matchAll(JSON_STRING_FIELD)) {
426
+ if (found[match[1]] === void 0) found[match[1]] = unescapeJsonString(match[2]);
427
+ }
428
+ return {
429
+ message: firstNonEmptyString(found.message, found.detail),
430
+ code: firstNonEmptyString(found.code, found.status, found.type)
431
+ };
432
+ }
433
+ function unescapeJsonString(value) {
434
+ try {
435
+ return JSON.parse(`"${value}"`);
436
+ } catch {
437
+ return value;
438
+ }
439
+ }
440
+ function clampDetail(text) {
441
+ const flattened = String(text ?? "").replace(/\s+/g, " ").trim();
442
+ const safe = redactCredentials(flattened);
443
+ return safe.length > UPSTREAM_ERROR_DETAIL_MAX_CHARS ? `${safe.slice(0, UPSTREAM_ERROR_DETAIL_MAX_CHARS - 1)}\u2026` : safe;
444
+ }
445
+ function clampCode(code) {
446
+ return String(code ?? "").replace(/\s+/g, " ").trim().slice(0, UPSTREAM_ERROR_CODE_MAX_CHARS);
447
+ }
448
+ var CREDENTIAL_LIKE_TOKEN = /\b[A-Za-z]{2,6}-[A-Za-z0-9_-]{16,}/g;
449
+ var LONG_OPAQUE_RUN = /[A-Za-z0-9+/_.-]{48,}/g;
450
+ var MODEL_ID_LIKE = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]{1,12})+$/;
451
+ var MODEL_ID_MAX_CHARS = 64;
452
+ var CREDENTIAL_PREFIX_SEGMENTS = /* @__PURE__ */ new Set([
453
+ "sk",
454
+ "pk",
455
+ "ak",
456
+ "api",
457
+ "apikey",
458
+ "key",
459
+ "token",
460
+ "secret",
461
+ "auth",
462
+ "bearer",
463
+ "session",
464
+ "sess",
465
+ "pat",
466
+ "xai",
467
+ "gsk"
468
+ ]);
469
+ var UUID_SHAPE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/;
470
+ function alternationCount(segment) {
471
+ return (segment.match(/[a-z]+|[0-9]+/g) || []).length;
472
+ }
473
+ function looksLikeModelId(token) {
474
+ if (token.length > MODEL_ID_MAX_CHARS) return false;
475
+ if (!MODEL_ID_LIKE.test(token)) return false;
476
+ if (UUID_SHAPE.test(token)) return false;
477
+ const segments = token.split(/[.-]/);
478
+ if (CREDENTIAL_PREFIX_SEGMENTS.has(segments[0])) return false;
479
+ const randomLooking = segments.filter((segment) => alternationCount(segment) >= 3);
480
+ if (randomLooking.length === 0) return true;
481
+ return randomLooking.length === 1 && randomLooking[0].length <= 5;
482
+ }
483
+ function redactCredentials(text) {
484
+ let s = text;
485
+ s = s.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [redacted]");
486
+ s = s.replace(CREDENTIAL_LIKE_TOKEN, (token) => looksLikeModelId(token) ? token : "[redacted]");
487
+ s = s.replace(LONG_OPAQUE_RUN, (run) => {
488
+ const token = run.replace(/^[._+/-]+/, "").replace(/[._+/-]+$/, "");
489
+ return looksLikeModelId(token) ? run : "[redacted]";
490
+ });
491
+ return s;
492
+ }
493
+ function firstNonEmptyString(...values) {
494
+ for (const value of values) {
495
+ if (typeof value === "string" && value.trim()) return value;
496
+ }
497
+ return "";
498
+ }
319
499
 
320
500
  // src/webpush.js
321
501
  var KEY_INFO_PREFIX = utf8("WebPush: info\0");
@@ -511,7 +691,7 @@ async function verifyVapidJwt(jwt, publicKey) {
511
691
  utf8(`${h}.${p}`)
512
692
  );
513
693
  if (!ok) throw new Error("VAPID JWT: signature mismatch");
514
- const payload = JSON.parse(new TextDecoder().decode(base64UrlToBytes(p)));
694
+ const payload = JSON.parse(utf8Decode(base64UrlToBytes(p)));
515
695
  if (!payload.exp || payload.exp <= Math.floor(Date.now() / 1e3)) {
516
696
  throw new Error("VAPID JWT: expired");
517
697
  }
@@ -547,23 +727,113 @@ var REI_SW_EVENT = Object.freeze({
547
727
  REASONING_RECEIVED: "rei-amsg-reasoning-received",
548
728
  TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received",
549
729
  ERROR_RECEIVED: "rei-amsg-error-received",
730
+ /** 宿主自定义的一条结果(`messageKind: 'result'`),不是聊天内容。 */
731
+ RESULT_RECEIVED: "rei-amsg-result-received",
550
732
  MULTIPART_EXPIRED: "rei-amsg-multipart-expired",
551
733
  UNKNOWN_RECEIVED: "rei-amsg-unknown-received"
552
734
  });
735
+ var MULTIPART_FAILURE_REASON = Object.freeze({
736
+ /** TTL 到期仍未收齐,或收到的分片本身已经过期。 */
737
+ TTL_EXPIRED: "ttl-expired",
738
+ /** 分片信封不合规:version / encoding 对不上、index 越界、chunk 不是合法 base64url。 */
739
+ INVALID_CHUNK: "invalid-chunk",
740
+ /** 同一个 id 的分片报了不一样的 total / encoding,已收的部分拼不回去。 */
741
+ CHUNK_CONFLICT: "chunk-conflict",
742
+ /** 累计字节数超过 maxTotalBytes。 */
743
+ SIZE_LIMIT_EXCEEDED: "size-limit-exceeded",
744
+ /** 收齐了但拼不回原 payload(缺片、超限、JSON 解不开)。 */
745
+ RESTORE_FAILED: "restore-failed",
746
+ /** 分片仓库(IndexedDB)读写失败。 */
747
+ STORAGE_FAILED: "storage-failed",
748
+ /** 接收端把 multipart 关了(`multipart.enabled === false`),分片没法重组。 */
749
+ DISABLED: "disabled"
750
+ });
553
751
  var REI_SW_MESSAGE_TYPE = Object.freeze({
554
752
  ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST",
555
753
  DELIVER: "REI_AMSG_DELIVER",
556
754
  FLUSH_QUEUE: "REI_FLUSH_QUEUE",
557
- QUEUE_RESULT: "REI_QUEUE_RESULT"
755
+ /**
756
+ * 入队的点对点回执:谁发的 ENQUEUE_REQUEST 就回给谁一条,一次一条。
757
+ * 没转 MessagePort 过来时会落到全局的 `navigator.serviceWorker` message
758
+ * 监听器上。
759
+ */
760
+ QUEUE_RESULT: "REI_QUEUE_RESULT",
761
+ /**
762
+ * 队列请求被永久拒绝、即将从队列里删掉时广播给所有窗口的一条。
763
+ *
764
+ * 跟 QUEUE_RESULT 分开是因为两者的收信人不是一回事:这条是广播,可能来自后台
765
+ * `sync` 冲刷、说的也可能是另一条八竿子打不着的旧请求。共用一个 type 的话,
766
+ * 页面等自己那条入队回执时会先收到这一条、当成自己的结果处理。
767
+ */
768
+ QUEUE_DROPPED: "REI_QUEUE_DROPPED"
558
769
  });
559
770
  var REI_AMSG_DELIVER_MESSAGE_TYPE = REI_SW_MESSAGE_TYPE.DELIVER;
560
771
 
772
+ // src/multipart.js
773
+ var DEFAULT_MULTIPART_CHUNK_BYTES = 1800;
774
+ function buildMultipartPushPayloads(payload, options = {}) {
775
+ const maxChunkBytes = resolvePositiveInteger(
776
+ options.maxChunkBytes,
777
+ DEFAULT_MULTIPART_CHUNK_BYTES,
778
+ "maxChunkBytes"
779
+ );
780
+ const ttlMs = resolvePositiveInteger(options.ttlMs, DEFAULT_MULTIPART_TTL_MS, "ttlMs");
781
+ const id = typeof options.id === "string" && options.id.trim() ? options.id.trim() : `mp_${randomUUID()}`;
782
+ let serialized = typeof options.serializedPayload === "string" ? options.serializedPayload : void 0;
783
+ if (serialized === void 0) {
784
+ try {
785
+ serialized = JSON.stringify(payload);
786
+ } catch (error) {
787
+ throw new TypeError(`buildMultipartPushPayloads: payload is not JSON-serializable: ${error?.message ?? error}`);
788
+ }
789
+ }
790
+ if (typeof serialized !== "string") {
791
+ throw new TypeError("buildMultipartPushPayloads: payload serialized to a non-string");
792
+ }
793
+ const bytes = utf8(serialized);
794
+ const total = Math.max(1, Math.ceil(bytes.byteLength / maxChunkBytes));
795
+ const createdAt = Date.now();
796
+ const originalMessageKind = payload && typeof payload === "object" ? (
797
+ /** @type {{ messageKind?: unknown }} */
798
+ payload.messageKind
799
+ ) : void 0;
800
+ const parts = [];
801
+ for (let i = 0; i < total; i++) {
802
+ const start = i * maxChunkBytes;
803
+ const end = Math.min(start + maxChunkBytes, bytes.byteLength);
804
+ const chunkBytes = bytes.subarray(start, end);
805
+ parts.push({
806
+ messageKind: MULTIPART_MESSAGE_KIND,
807
+ multipart: {
808
+ version: MULTIPART_VERSION,
809
+ id,
810
+ index: i + 1,
811
+ total,
812
+ encoding: MULTIPART_ENCODING,
813
+ originalMessageKind: typeof originalMessageKind === "string" ? originalMessageKind : null,
814
+ createdAt,
815
+ ttlMs
816
+ },
817
+ chunk: bytesToBase64Url(chunkBytes)
818
+ });
819
+ }
820
+ return parts;
821
+ }
822
+ function resolvePositiveInteger(value, fallback, fieldName) {
823
+ if (value === void 0 || value === null) return fallback;
824
+ if (!Number.isInteger(value) || value <= 0) {
825
+ throw new TypeError(`buildMultipartPushPayloads: ${fieldName} must be a positive integer`);
826
+ }
827
+ return value;
828
+ }
829
+
561
830
  // src/index.js
562
831
  var MESSAGE_KIND = Object.freeze({
563
832
  CONTENT: "content",
564
833
  REASONING: "reasoning",
565
834
  TOOL_REQUEST: "tool_request",
566
- ERROR: "error"
835
+ ERROR: "error",
836
+ RESULT: "result"
567
837
  });
568
838
  var MESSAGE_TYPE = Object.freeze({
569
839
  INSTANT: "instant",
@@ -715,8 +985,33 @@ function buildErrorPush(args) {
715
985
  if (args.messageSubtype !== void 0) push.messageSubtype = args.messageSubtype;
716
986
  if (args.metadata !== void 0) push.metadata = args.metadata;
717
987
  if (args.notification !== void 0) push.notification = args.notification;
988
+ if (args.llmStatus !== void 0) push.llmStatus = args.llmStatus;
989
+ if (args.providerCode !== void 0) push.providerCode = args.providerCode;
718
990
  return push;
719
991
  }
992
+ function buildResultPush(args) {
993
+ if (!args || typeof args !== "object" || Array.isArray(args)) {
994
+ throw new Error("[amsg-shared] ResultPush: payload must be a plain object");
995
+ }
996
+ requireField("ResultPush", "messageType", args.messageType);
997
+ requireField("ResultPush", "source", args.source);
998
+ requireField("ResultPush", "messageId", args.messageId);
999
+ requireField("ResultPush", "sessionId", args.sessionId);
1000
+ if (typeof args.resultKind !== "string" || !args.resultKind.trim()) {
1001
+ throw new Error(
1002
+ "[amsg-shared] ResultPush: 'resultKind' must be a non-empty string\uFF08\u7ED9\u8FD9\u7C7B\u7ED3\u679C\u8D77\u4E2A\u540D\u5B57\uFF0C\u5BA2\u6237\u7AEF\u6309\u5B83\u5206\u6D41\uFF09"
1003
+ );
1004
+ }
1005
+ validateNotificationArg("ResultPush", args.notification);
1006
+ return (
1007
+ /** @type {ResultPush} */
1008
+ {
1009
+ ...args,
1010
+ messageKind: "result",
1011
+ timestamp: args.timestamp || (/* @__PURE__ */ new Date()).toISOString()
1012
+ }
1013
+ );
1014
+ }
720
1015
  function isContentPush(value) {
721
1016
  return !!value && typeof value === "object" && /** @type {{messageKind?: unknown}} */
722
1017
  value.messageKind === "content";
@@ -733,6 +1028,10 @@ function isErrorPush(value) {
733
1028
  return !!value && typeof value === "object" && /** @type {{messageKind?: unknown}} */
734
1029
  value.messageKind === "error";
735
1030
  }
1031
+ function isResultPush(value) {
1032
+ return !!value && typeof value === "object" && /** @type {{messageKind?: unknown}} */
1033
+ value.messageKind === "result";
1034
+ }
736
1035
  var REASONING_CHUNK_ENCODER = new TextEncoder();
737
1036
  var REASONING_CHUNK_DECODER = new TextDecoder("utf-8", { fatal: true });
738
1037
  function chunkReasoningByUtf8Bytes(text, maxBytes) {
@@ -836,12 +1135,20 @@ function buildSessionContext({
836
1135
  scratch
837
1136
  }) {
838
1137
  const llmOutputText = readLlmOutputText(llmResponse);
1138
+ const usage = llmResponse && typeof llmResponse === "object" && /** @type {any} */
1139
+ llmResponse.usage && typeof /** @type {any} */
1140
+ llmResponse.usage === "object" ? (
1141
+ /** @type {Record<string, unknown>} */
1142
+ /** @type {any} */
1143
+ llmResponse.usage
1144
+ ) : null;
839
1145
  const ctx = {
840
1146
  sessionId,
841
1147
  charId,
842
1148
  messages,
843
1149
  llmResponse,
844
1150
  llmOutputText,
1151
+ usage,
845
1152
  iteration,
846
1153
  metadata: metadata && typeof metadata === "object" ? metadata : {},
847
1154
  contactName,