Skip to content
Source
Sign inSign up

userMessage.ts

139 lines5.3 KB

1import {
2 type ChatAttachment,
3 type ChatFileAttachment,
4 type GuestMessageAttribution,
5 type OrchestrationMessageContext,
6 type OrchestrationV2Actor,
7 type OrchestrationV2CreationSource,
8 type OrchestrationV2IdleCompaction,
9 type OrchestrationV2UserMessageInputIntent,
10 ScheduledTaskId,
11} from "@t3tools/contracts";
12import {
13 isAttachmentOnlyBootstrapPrompt,
14 replaceComposerContextReferences,
15} from "@t3tools/shared/composerContextReferences";
16import { filePreviewKind } from "@t3tools/shared/filePreview";
17import { imageMimeType } from "@t3tools/shared/image";
18import { videoMimeType } from "@t3tools/shared/video";
19​
20/** The working row while T3 Code's own compaction runs. */
21export const IDLE_COMPACTING_LABELS = {
22 beforeCacheExpires: "Compacting before the cache expires…",
23 onReturn: "Compacting…",
24} satisfies Record<OrchestrationV2IdleCompaction, string>;
25​
26/** Leads the finished compaction's token counts, in place of "Context compacted". */
27export const IDLE_COMPACTION_TITLES = {
28 beforeCacheExpires: "Context automatically compacted before cache expiry",
29 onReturn: "Context compacted before sending",
30} satisfies Record<OrchestrationV2IdleCompaction, string>;
31​
32const LEGACY_AUTOMATION_PREFIX = /^\[Triggered by schedule task: [^\r\n]+\]\r?\n\r?\n/;
33const LEGACY_AUTOMATION_MESSAGE_ID = /^scheduled-task-message:(.+):\d+:(?:scheduled|manual)$/;
34​
35export function resolveUserMessageFileCards(message: {
36 readonly text: string;
37 readonly attachments: ReadonlyArray<ChatAttachment>;
38 readonly context?: OrchestrationMessageContext | undefined;
39}): ReadonlyArray<ChatFileAttachment> | null {
40 const files = message.attachments.filter(
41 (attachment): attachment is ChatFileAttachment =>
42 attachment.type === "file" &&
43 imageMimeType(attachment) === null &&
44 videoMimeType(attachment) === null &&
45 filePreviewKind(attachment) !== "audio",
46 );
47 if (files.length === 0 || files.length !== message.attachments.length) return null;
48 const fileIds = new Set(files.map((file) => file.id));
49 const remaining = replaceComposerContextReferences(message.text, (reference) => {
50 const record = message.context?.records.find((item) => item.contextId === reference.contextId);
51 return reference.kind === "file" &&
52 record?.kind === "file" &&
53 "attachmentId" in record &&
54 fileIds.has(record.attachmentId)
55 ? ""
56 : reference.label || reference.kind;
57 });
58 return remaining.trim() === "" ? files : null;
60​
61export function resolveUserMessagePresentation(message: {
62 readonly id?: string;
63 readonly role: string;
64 readonly text: string;
65 readonly createdBy?: OrchestrationV2Actor;
66 readonly creationSource?: OrchestrationV2CreationSource;
67 readonly scheduledTaskId?: ScheduledTaskId | undefined;
68 readonly context?: { readonly guest?: GuestMessageAttribution | undefined } | undefined;
69}): {
70 readonly text: string;
71 readonly attribution: "automation" | "agent" | "t3code" | "guest" | null;
72 readonly scheduledTaskId: ScheduledTaskId | undefined;
73 /** Set when `attribution` is "guest". */
74 readonly guestName?: string;
75} {
76 if (message.role !== "user") {
77 return { text: message.text, attribution: null, scheduledTaskId: undefined };
78 }
79 // The stored text names the guest for the agent; people see the guest's own words.
80 const guest = message.context?.guest;
81 if (guest !== undefined) {
82 return {
83 text: guest.text,
84 attribution: "guest",
85 scheduledTaskId: undefined,
86 guestName: guest.name,
87 };
88 }
89 if (message.scheduledTaskId !== undefined) {
90 return {
91 text: message.text,
92 attribution: "automation",
93 scheduledTaskId: message.scheduledTaskId,
94 };
95 }
96 const legacyPrefix = LEGACY_AUTOMATION_PREFIX.exec(message.text);
97 const legacyTaskId = legacyPrefix
98 ? LEGACY_AUTOMATION_MESSAGE_ID.exec(message.id ?? "")?.[1]
99 : undefined;
100 if (legacyPrefix !== null && (legacyTaskId !== undefined || message.createdBy === "agent")) {
101 return {
102 text: message.text.slice(legacyPrefix[0].length),
103 attribution: "automation",
104 scheduledTaskId: legacyTaskId === undefined ? undefined : ScheduledTaskId.make(legacyTaskId),
105 };
106 }
107 return {
108 // The agent needs words to start a turn; people sent only the attachments.
109 text: isAttachmentOnlyBootstrapPrompt(message.text) ? "" : message.text,
110 attribution:
111 message.createdBy !== "agent"
112 ? null
113 : message.creationSource === "server"
114 ? "t3code"
115 : "agent",
116 scheduledTaskId: undefined,
117 };
119​
120type GroupableUserMessage = Parameters<typeof resolveUserMessagePresentation>[0] & {
121 readonly inputIntent?: OrchestrationV2UserMessageInputIntent;
122};
123​
124export function resolveUserMessageGrouping(
125 message: GroupableUserMessage | undefined,
126 previous: GroupableUserMessage | undefined,
127 next: GroupableUserMessage | undefined,
128) {
129 const isOwnMessage = (value: GroupableUserMessage | undefined) =>
130 value?.role === "user" && resolveUserMessagePresentation(value).attribution === null;
131 const isSteer = (value: GroupableUserMessage | undefined) =>
132 value?.inputIntent === "steer" || value?.inputIntent === "promoted_queued_to_steer";
133 const groupedWithPrevious = isOwnMessage(message) && isOwnMessage(previous);
134 return {
135 groupedWithPrevious,
136 groupedWithNext: isOwnMessage(message) && isOwnMessage(next),
137 continuesSteer: groupedWithPrevious && isSteer(message) && isSteer(previous),
138 };