@rei-standard/amsg-shared 0.3.0 → 0.4.0-next.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/dist/index.cjs +169 -0
- package/dist/index.d.cts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.mjs +169 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,13 +23,17 @@ __export(src_exports, {
|
|
|
23
23
|
MESSAGE_KIND: () => MESSAGE_KIND,
|
|
24
24
|
MESSAGE_TYPE: () => MESSAGE_TYPE,
|
|
25
25
|
PUSH_SOURCE: () => PUSH_SOURCE,
|
|
26
|
+
assertValidDecision: () => assertValidDecision,
|
|
26
27
|
base64UrlToBytes: () => base64UrlToBytes,
|
|
27
28
|
buildContentPush: () => buildContentPush,
|
|
28
29
|
buildErrorPush: () => buildErrorPush,
|
|
29
30
|
buildReasoningPush: () => buildReasoningPush,
|
|
31
|
+
buildSessionContext: () => buildSessionContext,
|
|
30
32
|
buildToolRequestPush: () => buildToolRequestPush,
|
|
31
33
|
chunkReasoningByUtf8Bytes: () => chunkReasoningByUtf8Bytes,
|
|
32
34
|
concatBytes: () => concatBytes,
|
|
35
|
+
extractAssistantMessage: () => extractAssistantMessage,
|
|
36
|
+
extractToolCallsFromDecision: () => extractToolCallsFromDecision,
|
|
33
37
|
isContentPush: () => isContentPush,
|
|
34
38
|
isErrorPush: () => isErrorPush,
|
|
35
39
|
isReasoningPush: () => isReasoningPush,
|
|
@@ -333,3 +337,168 @@ function stripReasoningTags(content) {
|
|
|
333
337
|
if (typeof content !== "string" || !content.includes("<")) return content;
|
|
334
338
|
return content.replace(REASONING_TAG_RE_G, "").trim();
|
|
335
339
|
}
|
|
340
|
+
function buildSessionContext({
|
|
341
|
+
sessionId,
|
|
342
|
+
messages,
|
|
343
|
+
llmResponse,
|
|
344
|
+
iteration,
|
|
345
|
+
contactName,
|
|
346
|
+
avatarUrl,
|
|
347
|
+
charId,
|
|
348
|
+
metadata
|
|
349
|
+
}) {
|
|
350
|
+
const llmOutputText = readLlmOutputText(llmResponse);
|
|
351
|
+
const ctx = {
|
|
352
|
+
sessionId,
|
|
353
|
+
charId,
|
|
354
|
+
messages,
|
|
355
|
+
llmResponse,
|
|
356
|
+
llmOutputText,
|
|
357
|
+
iteration,
|
|
358
|
+
metadata: metadata && typeof metadata === "object" ? metadata : {},
|
|
359
|
+
contactName,
|
|
360
|
+
avatarUrl: avatarUrl || void 0
|
|
361
|
+
};
|
|
362
|
+
return Object.freeze(ctx);
|
|
363
|
+
}
|
|
364
|
+
function readLlmOutputText(llmResponse) {
|
|
365
|
+
if (!llmResponse || typeof llmResponse !== "object") return "";
|
|
366
|
+
const choices = (
|
|
367
|
+
/** @type {{ choices?: unknown }} */
|
|
368
|
+
llmResponse.choices
|
|
369
|
+
);
|
|
370
|
+
if (!Array.isArray(choices) || choices.length === 0) return "";
|
|
371
|
+
const message = (
|
|
372
|
+
/** @type {{ message?: { content?: unknown } }} */
|
|
373
|
+
choices[0]?.message
|
|
374
|
+
);
|
|
375
|
+
const content = message?.content;
|
|
376
|
+
return typeof content === "string" ? content : "";
|
|
377
|
+
}
|
|
378
|
+
function extractAssistantMessage(llmResponse) {
|
|
379
|
+
const message = llmResponse && typeof llmResponse === "object" && Array.isArray(
|
|
380
|
+
/** @type {{ choices?: unknown }} */
|
|
381
|
+
llmResponse.choices
|
|
382
|
+
) && /** @type {{ choices: Array<{ message?: unknown }> }} */
|
|
383
|
+
llmResponse.choices[0]?.message;
|
|
384
|
+
if (message && typeof message === "object") {
|
|
385
|
+
return (
|
|
386
|
+
/** @type {ChatMessage} */
|
|
387
|
+
message
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return { role: "assistant", content: "" };
|
|
391
|
+
}
|
|
392
|
+
var VALID_DECISIONS = /* @__PURE__ */ new Set(["finish", "tool-request", "continue", "skip-push"]);
|
|
393
|
+
function assertValidDecision(decision, options = {}) {
|
|
394
|
+
const inlineToolCalls = options.inlineToolCalls === true;
|
|
395
|
+
if (!decision || typeof decision !== "object") {
|
|
396
|
+
throw new TypeError(`onLLMOutput returned invalid decision: ${stringifyDecisionForError(decision)}`);
|
|
397
|
+
}
|
|
398
|
+
const tag = (
|
|
399
|
+
/** @type {{ decision?: unknown }} */
|
|
400
|
+
decision.decision
|
|
401
|
+
);
|
|
402
|
+
if (typeof tag !== "string" || !VALID_DECISIONS.has(tag)) {
|
|
403
|
+
throw new TypeError(`onLLMOutput returned invalid decision tag: ${stringifyDecisionForError(tag)}`);
|
|
404
|
+
}
|
|
405
|
+
const hasSingular = Object.prototype.hasOwnProperty.call(decision, "pushPayload");
|
|
406
|
+
const hasPlural = Object.prototype.hasOwnProperty.call(decision, "pushPayloads");
|
|
407
|
+
if (hasSingular) {
|
|
408
|
+
throw new TypeError(
|
|
409
|
+
hasPlural ? "pushPayload (singular) is removed in 0.8.0, use pushPayloads" : "pushPayload (singular) is removed in 0.8.0, use pushPayloads: [yourPayload]"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (tag === "continue") {
|
|
413
|
+
if (!Array.isArray(
|
|
414
|
+
/** @type {{ nextHistory?: unknown }} */
|
|
415
|
+
decision.nextHistory
|
|
416
|
+
)) {
|
|
417
|
+
throw new TypeError('decision:"continue" requires a nextHistory array');
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (tag === "skip-push") {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (tag === "tool-request" && inlineToolCalls && Object.prototype.hasOwnProperty.call(decision, "toolCalls")) {
|
|
425
|
+
const toolCalls = (
|
|
426
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
427
|
+
decision.toolCalls
|
|
428
|
+
);
|
|
429
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
430
|
+
throw new TypeError('decision:"tool-request" toolCalls must be a non-empty array when set');
|
|
431
|
+
}
|
|
432
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
433
|
+
const t = toolCalls[i];
|
|
434
|
+
if (!t || typeof t !== "object" || Array.isArray(t)) {
|
|
435
|
+
throw new TypeError(`toolCalls[${i}] must be a plain object, got ${stringifyDecisionForError(t)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!hasPlural) return;
|
|
439
|
+
}
|
|
440
|
+
if (!hasPlural || !Array.isArray(
|
|
441
|
+
/** @type {{ pushPayloads?: unknown }} */
|
|
442
|
+
decision.pushPayloads
|
|
443
|
+
)) {
|
|
444
|
+
throw new TypeError(`decision:"${tag}" requires a pushPayloads array`);
|
|
445
|
+
}
|
|
446
|
+
const pushes = (
|
|
447
|
+
/** @type {Array<unknown>} */
|
|
448
|
+
decision.pushPayloads
|
|
449
|
+
);
|
|
450
|
+
if (pushes.length === 0) {
|
|
451
|
+
throw new TypeError("pushPayloads: [] \u2014 use decision: skip-push to skip notification entirely");
|
|
452
|
+
}
|
|
453
|
+
for (let i = 0; i < pushes.length; i++) {
|
|
454
|
+
const p = pushes[i];
|
|
455
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
456
|
+
throw new TypeError(`pushPayloads[${i}] must be a plain object, got ${stringifyDecisionForError(p)}`);
|
|
457
|
+
}
|
|
458
|
+
if (Object.prototype.hasOwnProperty.call(p, "splitPattern")) {
|
|
459
|
+
throw new TypeError(`pushPayloads[${i}].splitPattern is removed in 0.8.0; caller is responsible for splitting`);
|
|
460
|
+
}
|
|
461
|
+
if (Object.prototype.hasOwnProperty.call(p, "messageId")) {
|
|
462
|
+
const id = (
|
|
463
|
+
/** @type {{ messageId?: unknown }} */
|
|
464
|
+
p.messageId
|
|
465
|
+
);
|
|
466
|
+
if (typeof id !== "string" || id === "") {
|
|
467
|
+
throw new TypeError(`pushPayloads[${i}].messageId must be a non-empty string when set, got ${stringifyDecisionForError(id)}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function extractToolCallsFromDecision(decision) {
|
|
473
|
+
if (!decision || typeof decision !== "object") return [];
|
|
474
|
+
const direct = (
|
|
475
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
476
|
+
decision.toolCalls
|
|
477
|
+
);
|
|
478
|
+
if (Array.isArray(direct) && direct.length > 0) {
|
|
479
|
+
return direct;
|
|
480
|
+
}
|
|
481
|
+
const pushPayloads = (
|
|
482
|
+
/** @type {{ pushPayloads?: unknown }} */
|
|
483
|
+
decision.pushPayloads
|
|
484
|
+
);
|
|
485
|
+
if (!Array.isArray(pushPayloads)) return [];
|
|
486
|
+
const out = [];
|
|
487
|
+
for (const push of pushPayloads) {
|
|
488
|
+
if (push && typeof push === "object" && Array.isArray(
|
|
489
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
490
|
+
push.toolCalls
|
|
491
|
+
)) {
|
|
492
|
+
out.push(.../** @type {{ toolCalls: unknown[] }} */
|
|
493
|
+
push.toolCalls);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
function stringifyDecisionForError(value) {
|
|
499
|
+
try {
|
|
500
|
+
return JSON.stringify(value);
|
|
501
|
+
} catch {
|
|
502
|
+
return String(value);
|
|
503
|
+
}
|
|
504
|
+
}
|
package/dist/index.d.cts
CHANGED
|
@@ -297,6 +297,103 @@ export function readReasoningContent(llmResponse: unknown): string | null;
|
|
|
297
297
|
* @returns {string}
|
|
298
298
|
*/
|
|
299
299
|
export function stripReasoningTags(content: string): string;
|
|
300
|
+
/**
|
|
301
|
+
* @typedef {Object} ChatMessage
|
|
302
|
+
* @property {'system' | 'user' | 'assistant' | 'tool'} role
|
|
303
|
+
* @property {string | unknown[] | null} [content]
|
|
304
|
+
* @property {Array<{ id: string, type: 'function', function: { name: string, arguments: string } }>} [tool_calls]
|
|
305
|
+
* @property {string} [tool_call_id]
|
|
306
|
+
* @property {string} [name]
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* @typedef {Object} SessionContext
|
|
310
|
+
* @property {string} sessionId
|
|
311
|
+
* @property {string} [charId]
|
|
312
|
+
* @property {ChatMessage[]} messages - Including the just-appended assistant turn.
|
|
313
|
+
* @property {unknown} llmResponse - Full LLM response (choices, usage, …).
|
|
314
|
+
* @property {string} llmOutputText - May be '' for pure tool-call responses.
|
|
315
|
+
* @property {number} iteration - 0-indexed: the round that just finished.
|
|
316
|
+
* @property {Record<string, unknown>} metadata
|
|
317
|
+
* @property {string} contactName
|
|
318
|
+
* @property {string} [avatarUrl]
|
|
319
|
+
*/
|
|
320
|
+
/**
|
|
321
|
+
* Build the frozen SessionContext handed to an onLLMOutput hook.
|
|
322
|
+
*
|
|
323
|
+
* Credentials (apiKey / apiUrl / pushSubscription / vapid / masterKey) are
|
|
324
|
+
* intentionally NOT part of the shape: a console.log(ctx) from a hook must
|
|
325
|
+
* not leak keys, and a third-party hook must not be able to exfiltrate
|
|
326
|
+
* them. Frozen so a hook cannot mutate the live history — if it chooses
|
|
327
|
+
* `decision:'continue'`, the caller still owns its copy.
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} args
|
|
330
|
+
* @param {string} args.sessionId
|
|
331
|
+
* @param {ChatMessage[]} args.messages
|
|
332
|
+
* @param {unknown} args.llmResponse
|
|
333
|
+
* @param {number} args.iteration
|
|
334
|
+
* @param {string} args.contactName
|
|
335
|
+
* @param {string} [args.avatarUrl]
|
|
336
|
+
* @param {string} [args.charId]
|
|
337
|
+
* @param {Record<string, unknown>} [args.metadata]
|
|
338
|
+
* @returns {SessionContext}
|
|
339
|
+
*/
|
|
340
|
+
export function buildSessionContext({ sessionId, messages, llmResponse, iteration, contactName, avatarUrl, charId, metadata, }: {
|
|
341
|
+
sessionId: string;
|
|
342
|
+
messages: ChatMessage[];
|
|
343
|
+
llmResponse: unknown;
|
|
344
|
+
iteration: number;
|
|
345
|
+
contactName: string;
|
|
346
|
+
avatarUrl?: string;
|
|
347
|
+
charId?: string;
|
|
348
|
+
metadata?: Record<string, unknown>;
|
|
349
|
+
}): SessionContext;
|
|
350
|
+
/**
|
|
351
|
+
* Extract the `choices[0].message` whole object — preserving `tool_calls`
|
|
352
|
+
* / `reasoning_content` / `refusal` etc. — for appending to the running
|
|
353
|
+
* history. Falls back to a minimal placeholder when the response is
|
|
354
|
+
* malformed so the hook still gets a chance to react via
|
|
355
|
+
* `llmOutputText === ''`.
|
|
356
|
+
*
|
|
357
|
+
* Critically, we keep the entire message object (not just
|
|
358
|
+
* `{role, content}`): the next round may need to forward a `tool_calls`
|
|
359
|
+
* array to OpenAI alongside the matching tool-result messages, and
|
|
360
|
+
* stripping the field would make the API reject the request.
|
|
361
|
+
*
|
|
362
|
+
* @param {unknown} llmResponse
|
|
363
|
+
* @returns {ChatMessage}
|
|
364
|
+
*/
|
|
365
|
+
export function extractAssistantMessage(llmResponse: unknown): ChatMessage;
|
|
366
|
+
/**
|
|
367
|
+
* Assert that an onLLMOutput hook returned a structurally valid decision.
|
|
368
|
+
* TypeScript discriminated unions don't survive into runtime, and a
|
|
369
|
+
* misbehaving hook can easily return `null` / `{ decision: 'idk' }` /
|
|
370
|
+
* `undefined` — treat any of those as a hook contract violation.
|
|
371
|
+
*
|
|
372
|
+
* Flavors:
|
|
373
|
+
* - default (amsg-instant): 'tool-request' must carry pushPayloads — the
|
|
374
|
+
* tool_request push goes to the client, which executes the tools and
|
|
375
|
+
* POSTs /continue.
|
|
376
|
+
* - `{ inlineToolCalls: true }` (amsg-server fire-time loop): the host
|
|
377
|
+
* executes tools in-process, so 'tool-request' may instead carry a
|
|
378
|
+
* non-empty `toolCalls` array directly; pushPayloads then become
|
|
379
|
+
* optional. pushPayloads-shaped tool-requests stay valid so a
|
|
380
|
+
* classifier written for instant drops in unchanged.
|
|
381
|
+
*
|
|
382
|
+
* @param {unknown} decision
|
|
383
|
+
* @param {{ inlineToolCalls?: boolean }} [options]
|
|
384
|
+
*/
|
|
385
|
+
export function assertValidDecision(decision: unknown, options?: {
|
|
386
|
+
inlineToolCalls?: boolean;
|
|
387
|
+
}): void;
|
|
388
|
+
/**
|
|
389
|
+
* Pull the toolCalls out of a 'tool-request' decision, whichever shape it
|
|
390
|
+
* came in: `decision.toolCalls` directly (server flavor), or embedded in
|
|
391
|
+
* tool_request pushPayloads (instant classifier flavor).
|
|
392
|
+
*
|
|
393
|
+
* @param {unknown} decision
|
|
394
|
+
* @returns {Array<Record<string, unknown>>}
|
|
395
|
+
*/
|
|
396
|
+
export function extractToolCallsFromDecision(decision: unknown): Array<Record<string, unknown>>;
|
|
300
397
|
/**
|
|
301
398
|
* @rei-standard/amsg-shared
|
|
302
399
|
*
|
|
@@ -554,6 +651,43 @@ export type ErrorPush = AmsgPushCommon & {
|
|
|
554
651
|
* `switch` on `messageKind` and the compiler narrows automatically.
|
|
555
652
|
*/
|
|
556
653
|
export type AmsgPush = ContentPush | ReasoningPush | ToolRequestPush | ErrorPush;
|
|
654
|
+
export type ChatMessage = {
|
|
655
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
656
|
+
content?: string | unknown[] | null;
|
|
657
|
+
tool_calls?: Array<{
|
|
658
|
+
id: string;
|
|
659
|
+
type: "function";
|
|
660
|
+
function: {
|
|
661
|
+
name: string;
|
|
662
|
+
arguments: string;
|
|
663
|
+
};
|
|
664
|
+
}>;
|
|
665
|
+
tool_call_id?: string;
|
|
666
|
+
name?: string;
|
|
667
|
+
};
|
|
668
|
+
export type SessionContext = {
|
|
669
|
+
sessionId: string;
|
|
670
|
+
charId?: string;
|
|
671
|
+
/**
|
|
672
|
+
* - Including the just-appended assistant turn.
|
|
673
|
+
*/
|
|
674
|
+
messages: ChatMessage[];
|
|
675
|
+
/**
|
|
676
|
+
* - Full LLM response (choices, usage, …).
|
|
677
|
+
*/
|
|
678
|
+
llmResponse: unknown;
|
|
679
|
+
/**
|
|
680
|
+
* - May be '' for pure tool-call responses.
|
|
681
|
+
*/
|
|
682
|
+
llmOutputText: string;
|
|
683
|
+
/**
|
|
684
|
+
* - 0-indexed: the round that just finished.
|
|
685
|
+
*/
|
|
686
|
+
iteration: number;
|
|
687
|
+
metadata: Record<string, unknown>;
|
|
688
|
+
contactName: string;
|
|
689
|
+
avatarUrl?: string;
|
|
690
|
+
};
|
|
557
691
|
/**
|
|
558
692
|
* What the push carries. Fixed enum — packages must not add values.
|
|
559
693
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -297,6 +297,103 @@ export function readReasoningContent(llmResponse: unknown): string | null;
|
|
|
297
297
|
* @returns {string}
|
|
298
298
|
*/
|
|
299
299
|
export function stripReasoningTags(content: string): string;
|
|
300
|
+
/**
|
|
301
|
+
* @typedef {Object} ChatMessage
|
|
302
|
+
* @property {'system' | 'user' | 'assistant' | 'tool'} role
|
|
303
|
+
* @property {string | unknown[] | null} [content]
|
|
304
|
+
* @property {Array<{ id: string, type: 'function', function: { name: string, arguments: string } }>} [tool_calls]
|
|
305
|
+
* @property {string} [tool_call_id]
|
|
306
|
+
* @property {string} [name]
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* @typedef {Object} SessionContext
|
|
310
|
+
* @property {string} sessionId
|
|
311
|
+
* @property {string} [charId]
|
|
312
|
+
* @property {ChatMessage[]} messages - Including the just-appended assistant turn.
|
|
313
|
+
* @property {unknown} llmResponse - Full LLM response (choices, usage, …).
|
|
314
|
+
* @property {string} llmOutputText - May be '' for pure tool-call responses.
|
|
315
|
+
* @property {number} iteration - 0-indexed: the round that just finished.
|
|
316
|
+
* @property {Record<string, unknown>} metadata
|
|
317
|
+
* @property {string} contactName
|
|
318
|
+
* @property {string} [avatarUrl]
|
|
319
|
+
*/
|
|
320
|
+
/**
|
|
321
|
+
* Build the frozen SessionContext handed to an onLLMOutput hook.
|
|
322
|
+
*
|
|
323
|
+
* Credentials (apiKey / apiUrl / pushSubscription / vapid / masterKey) are
|
|
324
|
+
* intentionally NOT part of the shape: a console.log(ctx) from a hook must
|
|
325
|
+
* not leak keys, and a third-party hook must not be able to exfiltrate
|
|
326
|
+
* them. Frozen so a hook cannot mutate the live history — if it chooses
|
|
327
|
+
* `decision:'continue'`, the caller still owns its copy.
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} args
|
|
330
|
+
* @param {string} args.sessionId
|
|
331
|
+
* @param {ChatMessage[]} args.messages
|
|
332
|
+
* @param {unknown} args.llmResponse
|
|
333
|
+
* @param {number} args.iteration
|
|
334
|
+
* @param {string} args.contactName
|
|
335
|
+
* @param {string} [args.avatarUrl]
|
|
336
|
+
* @param {string} [args.charId]
|
|
337
|
+
* @param {Record<string, unknown>} [args.metadata]
|
|
338
|
+
* @returns {SessionContext}
|
|
339
|
+
*/
|
|
340
|
+
export function buildSessionContext({ sessionId, messages, llmResponse, iteration, contactName, avatarUrl, charId, metadata, }: {
|
|
341
|
+
sessionId: string;
|
|
342
|
+
messages: ChatMessage[];
|
|
343
|
+
llmResponse: unknown;
|
|
344
|
+
iteration: number;
|
|
345
|
+
contactName: string;
|
|
346
|
+
avatarUrl?: string;
|
|
347
|
+
charId?: string;
|
|
348
|
+
metadata?: Record<string, unknown>;
|
|
349
|
+
}): SessionContext;
|
|
350
|
+
/**
|
|
351
|
+
* Extract the `choices[0].message` whole object — preserving `tool_calls`
|
|
352
|
+
* / `reasoning_content` / `refusal` etc. — for appending to the running
|
|
353
|
+
* history. Falls back to a minimal placeholder when the response is
|
|
354
|
+
* malformed so the hook still gets a chance to react via
|
|
355
|
+
* `llmOutputText === ''`.
|
|
356
|
+
*
|
|
357
|
+
* Critically, we keep the entire message object (not just
|
|
358
|
+
* `{role, content}`): the next round may need to forward a `tool_calls`
|
|
359
|
+
* array to OpenAI alongside the matching tool-result messages, and
|
|
360
|
+
* stripping the field would make the API reject the request.
|
|
361
|
+
*
|
|
362
|
+
* @param {unknown} llmResponse
|
|
363
|
+
* @returns {ChatMessage}
|
|
364
|
+
*/
|
|
365
|
+
export function extractAssistantMessage(llmResponse: unknown): ChatMessage;
|
|
366
|
+
/**
|
|
367
|
+
* Assert that an onLLMOutput hook returned a structurally valid decision.
|
|
368
|
+
* TypeScript discriminated unions don't survive into runtime, and a
|
|
369
|
+
* misbehaving hook can easily return `null` / `{ decision: 'idk' }` /
|
|
370
|
+
* `undefined` — treat any of those as a hook contract violation.
|
|
371
|
+
*
|
|
372
|
+
* Flavors:
|
|
373
|
+
* - default (amsg-instant): 'tool-request' must carry pushPayloads — the
|
|
374
|
+
* tool_request push goes to the client, which executes the tools and
|
|
375
|
+
* POSTs /continue.
|
|
376
|
+
* - `{ inlineToolCalls: true }` (amsg-server fire-time loop): the host
|
|
377
|
+
* executes tools in-process, so 'tool-request' may instead carry a
|
|
378
|
+
* non-empty `toolCalls` array directly; pushPayloads then become
|
|
379
|
+
* optional. pushPayloads-shaped tool-requests stay valid so a
|
|
380
|
+
* classifier written for instant drops in unchanged.
|
|
381
|
+
*
|
|
382
|
+
* @param {unknown} decision
|
|
383
|
+
* @param {{ inlineToolCalls?: boolean }} [options]
|
|
384
|
+
*/
|
|
385
|
+
export function assertValidDecision(decision: unknown, options?: {
|
|
386
|
+
inlineToolCalls?: boolean;
|
|
387
|
+
}): void;
|
|
388
|
+
/**
|
|
389
|
+
* Pull the toolCalls out of a 'tool-request' decision, whichever shape it
|
|
390
|
+
* came in: `decision.toolCalls` directly (server flavor), or embedded in
|
|
391
|
+
* tool_request pushPayloads (instant classifier flavor).
|
|
392
|
+
*
|
|
393
|
+
* @param {unknown} decision
|
|
394
|
+
* @returns {Array<Record<string, unknown>>}
|
|
395
|
+
*/
|
|
396
|
+
export function extractToolCallsFromDecision(decision: unknown): Array<Record<string, unknown>>;
|
|
300
397
|
/**
|
|
301
398
|
* @rei-standard/amsg-shared
|
|
302
399
|
*
|
|
@@ -554,6 +651,43 @@ export type ErrorPush = AmsgPushCommon & {
|
|
|
554
651
|
* `switch` on `messageKind` and the compiler narrows automatically.
|
|
555
652
|
*/
|
|
556
653
|
export type AmsgPush = ContentPush | ReasoningPush | ToolRequestPush | ErrorPush;
|
|
654
|
+
export type ChatMessage = {
|
|
655
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
656
|
+
content?: string | unknown[] | null;
|
|
657
|
+
tool_calls?: Array<{
|
|
658
|
+
id: string;
|
|
659
|
+
type: "function";
|
|
660
|
+
function: {
|
|
661
|
+
name: string;
|
|
662
|
+
arguments: string;
|
|
663
|
+
};
|
|
664
|
+
}>;
|
|
665
|
+
tool_call_id?: string;
|
|
666
|
+
name?: string;
|
|
667
|
+
};
|
|
668
|
+
export type SessionContext = {
|
|
669
|
+
sessionId: string;
|
|
670
|
+
charId?: string;
|
|
671
|
+
/**
|
|
672
|
+
* - Including the just-appended assistant turn.
|
|
673
|
+
*/
|
|
674
|
+
messages: ChatMessage[];
|
|
675
|
+
/**
|
|
676
|
+
* - Full LLM response (choices, usage, …).
|
|
677
|
+
*/
|
|
678
|
+
llmResponse: unknown;
|
|
679
|
+
/**
|
|
680
|
+
* - May be '' for pure tool-call responses.
|
|
681
|
+
*/
|
|
682
|
+
llmOutputText: string;
|
|
683
|
+
/**
|
|
684
|
+
* - 0-indexed: the round that just finished.
|
|
685
|
+
*/
|
|
686
|
+
iteration: number;
|
|
687
|
+
metadata: Record<string, unknown>;
|
|
688
|
+
contactName: string;
|
|
689
|
+
avatarUrl?: string;
|
|
690
|
+
};
|
|
557
691
|
/**
|
|
558
692
|
* What the push carries. Fixed enum — packages must not add values.
|
|
559
693
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -290,18 +290,187 @@ function stripReasoningTags(content) {
|
|
|
290
290
|
if (typeof content !== "string" || !content.includes("<")) return content;
|
|
291
291
|
return content.replace(REASONING_TAG_RE_G, "").trim();
|
|
292
292
|
}
|
|
293
|
+
function buildSessionContext({
|
|
294
|
+
sessionId,
|
|
295
|
+
messages,
|
|
296
|
+
llmResponse,
|
|
297
|
+
iteration,
|
|
298
|
+
contactName,
|
|
299
|
+
avatarUrl,
|
|
300
|
+
charId,
|
|
301
|
+
metadata
|
|
302
|
+
}) {
|
|
303
|
+
const llmOutputText = readLlmOutputText(llmResponse);
|
|
304
|
+
const ctx = {
|
|
305
|
+
sessionId,
|
|
306
|
+
charId,
|
|
307
|
+
messages,
|
|
308
|
+
llmResponse,
|
|
309
|
+
llmOutputText,
|
|
310
|
+
iteration,
|
|
311
|
+
metadata: metadata && typeof metadata === "object" ? metadata : {},
|
|
312
|
+
contactName,
|
|
313
|
+
avatarUrl: avatarUrl || void 0
|
|
314
|
+
};
|
|
315
|
+
return Object.freeze(ctx);
|
|
316
|
+
}
|
|
317
|
+
function readLlmOutputText(llmResponse) {
|
|
318
|
+
if (!llmResponse || typeof llmResponse !== "object") return "";
|
|
319
|
+
const choices = (
|
|
320
|
+
/** @type {{ choices?: unknown }} */
|
|
321
|
+
llmResponse.choices
|
|
322
|
+
);
|
|
323
|
+
if (!Array.isArray(choices) || choices.length === 0) return "";
|
|
324
|
+
const message = (
|
|
325
|
+
/** @type {{ message?: { content?: unknown } }} */
|
|
326
|
+
choices[0]?.message
|
|
327
|
+
);
|
|
328
|
+
const content = message?.content;
|
|
329
|
+
return typeof content === "string" ? content : "";
|
|
330
|
+
}
|
|
331
|
+
function extractAssistantMessage(llmResponse) {
|
|
332
|
+
const message = llmResponse && typeof llmResponse === "object" && Array.isArray(
|
|
333
|
+
/** @type {{ choices?: unknown }} */
|
|
334
|
+
llmResponse.choices
|
|
335
|
+
) && /** @type {{ choices: Array<{ message?: unknown }> }} */
|
|
336
|
+
llmResponse.choices[0]?.message;
|
|
337
|
+
if (message && typeof message === "object") {
|
|
338
|
+
return (
|
|
339
|
+
/** @type {ChatMessage} */
|
|
340
|
+
message
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return { role: "assistant", content: "" };
|
|
344
|
+
}
|
|
345
|
+
var VALID_DECISIONS = /* @__PURE__ */ new Set(["finish", "tool-request", "continue", "skip-push"]);
|
|
346
|
+
function assertValidDecision(decision, options = {}) {
|
|
347
|
+
const inlineToolCalls = options.inlineToolCalls === true;
|
|
348
|
+
if (!decision || typeof decision !== "object") {
|
|
349
|
+
throw new TypeError(`onLLMOutput returned invalid decision: ${stringifyDecisionForError(decision)}`);
|
|
350
|
+
}
|
|
351
|
+
const tag = (
|
|
352
|
+
/** @type {{ decision?: unknown }} */
|
|
353
|
+
decision.decision
|
|
354
|
+
);
|
|
355
|
+
if (typeof tag !== "string" || !VALID_DECISIONS.has(tag)) {
|
|
356
|
+
throw new TypeError(`onLLMOutput returned invalid decision tag: ${stringifyDecisionForError(tag)}`);
|
|
357
|
+
}
|
|
358
|
+
const hasSingular = Object.prototype.hasOwnProperty.call(decision, "pushPayload");
|
|
359
|
+
const hasPlural = Object.prototype.hasOwnProperty.call(decision, "pushPayloads");
|
|
360
|
+
if (hasSingular) {
|
|
361
|
+
throw new TypeError(
|
|
362
|
+
hasPlural ? "pushPayload (singular) is removed in 0.8.0, use pushPayloads" : "pushPayload (singular) is removed in 0.8.0, use pushPayloads: [yourPayload]"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (tag === "continue") {
|
|
366
|
+
if (!Array.isArray(
|
|
367
|
+
/** @type {{ nextHistory?: unknown }} */
|
|
368
|
+
decision.nextHistory
|
|
369
|
+
)) {
|
|
370
|
+
throw new TypeError('decision:"continue" requires a nextHistory array');
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (tag === "skip-push") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (tag === "tool-request" && inlineToolCalls && Object.prototype.hasOwnProperty.call(decision, "toolCalls")) {
|
|
378
|
+
const toolCalls = (
|
|
379
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
380
|
+
decision.toolCalls
|
|
381
|
+
);
|
|
382
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
383
|
+
throw new TypeError('decision:"tool-request" toolCalls must be a non-empty array when set');
|
|
384
|
+
}
|
|
385
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
386
|
+
const t = toolCalls[i];
|
|
387
|
+
if (!t || typeof t !== "object" || Array.isArray(t)) {
|
|
388
|
+
throw new TypeError(`toolCalls[${i}] must be a plain object, got ${stringifyDecisionForError(t)}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!hasPlural) return;
|
|
392
|
+
}
|
|
393
|
+
if (!hasPlural || !Array.isArray(
|
|
394
|
+
/** @type {{ pushPayloads?: unknown }} */
|
|
395
|
+
decision.pushPayloads
|
|
396
|
+
)) {
|
|
397
|
+
throw new TypeError(`decision:"${tag}" requires a pushPayloads array`);
|
|
398
|
+
}
|
|
399
|
+
const pushes = (
|
|
400
|
+
/** @type {Array<unknown>} */
|
|
401
|
+
decision.pushPayloads
|
|
402
|
+
);
|
|
403
|
+
if (pushes.length === 0) {
|
|
404
|
+
throw new TypeError("pushPayloads: [] \u2014 use decision: skip-push to skip notification entirely");
|
|
405
|
+
}
|
|
406
|
+
for (let i = 0; i < pushes.length; i++) {
|
|
407
|
+
const p = pushes[i];
|
|
408
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
409
|
+
throw new TypeError(`pushPayloads[${i}] must be a plain object, got ${stringifyDecisionForError(p)}`);
|
|
410
|
+
}
|
|
411
|
+
if (Object.prototype.hasOwnProperty.call(p, "splitPattern")) {
|
|
412
|
+
throw new TypeError(`pushPayloads[${i}].splitPattern is removed in 0.8.0; caller is responsible for splitting`);
|
|
413
|
+
}
|
|
414
|
+
if (Object.prototype.hasOwnProperty.call(p, "messageId")) {
|
|
415
|
+
const id = (
|
|
416
|
+
/** @type {{ messageId?: unknown }} */
|
|
417
|
+
p.messageId
|
|
418
|
+
);
|
|
419
|
+
if (typeof id !== "string" || id === "") {
|
|
420
|
+
throw new TypeError(`pushPayloads[${i}].messageId must be a non-empty string when set, got ${stringifyDecisionForError(id)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function extractToolCallsFromDecision(decision) {
|
|
426
|
+
if (!decision || typeof decision !== "object") return [];
|
|
427
|
+
const direct = (
|
|
428
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
429
|
+
decision.toolCalls
|
|
430
|
+
);
|
|
431
|
+
if (Array.isArray(direct) && direct.length > 0) {
|
|
432
|
+
return direct;
|
|
433
|
+
}
|
|
434
|
+
const pushPayloads = (
|
|
435
|
+
/** @type {{ pushPayloads?: unknown }} */
|
|
436
|
+
decision.pushPayloads
|
|
437
|
+
);
|
|
438
|
+
if (!Array.isArray(pushPayloads)) return [];
|
|
439
|
+
const out = [];
|
|
440
|
+
for (const push of pushPayloads) {
|
|
441
|
+
if (push && typeof push === "object" && Array.isArray(
|
|
442
|
+
/** @type {{ toolCalls?: unknown }} */
|
|
443
|
+
push.toolCalls
|
|
444
|
+
)) {
|
|
445
|
+
out.push(.../** @type {{ toolCalls: unknown[] }} */
|
|
446
|
+
push.toolCalls);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
function stringifyDecisionForError(value) {
|
|
452
|
+
try {
|
|
453
|
+
return JSON.stringify(value);
|
|
454
|
+
} catch {
|
|
455
|
+
return String(value);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
293
458
|
export {
|
|
294
459
|
AVATAR_URL_MAX_LENGTH,
|
|
295
460
|
MESSAGE_KIND,
|
|
296
461
|
MESSAGE_TYPE,
|
|
297
462
|
PUSH_SOURCE,
|
|
463
|
+
assertValidDecision,
|
|
298
464
|
base64UrlToBytes,
|
|
299
465
|
buildContentPush,
|
|
300
466
|
buildErrorPush,
|
|
301
467
|
buildReasoningPush,
|
|
468
|
+
buildSessionContext,
|
|
302
469
|
buildToolRequestPush,
|
|
303
470
|
chunkReasoningByUtf8Bytes,
|
|
304
471
|
concatBytes,
|
|
472
|
+
extractAssistantMessage,
|
|
473
|
+
extractToolCallsFromDecision,
|
|
305
474
|
isContentPush,
|
|
306
475
|
isErrorPush,
|
|
307
476
|
isReasoningPush,
|
package/package.json
CHANGED