Skip to content
81 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * What a message may hold and how pages of them are read. Pure, so it is
3 * tested apart from the service.
4 */
5
6/** The longest message body, in characters. */
7export const MAX_BODY = 40_000;
8
9/** Messages on one page when none is asked for, and the most on one page. */
10export const DEFAULT_PAGE = 50;
11export const MAX_PAGE = 200;
12
13/** How long "is typing" shows after a person's keystroke and while an agent works. */
14export const PERSON_TYPING_MS = 5_000;
15export const AGENT_TYPING_MS = 8_000;
16
17/** A body as kept, or why it cannot be: something to say, and not too much. */
18export function messageBody(body: unknown, hasCard = false): { ok: true; body: string } | { ok: false; message: string } {
19 const text = typeof body === "string" ? body.replace(/\s+$/, "") : "";
20 if (!text.trim() && !hasCard) return { ok: false, message: "A message needs something in it." };
21 if (text.length > MAX_BODY) return { ok: false, message: `A message is at most ${MAX_BODY.toLocaleString("en-US")} characters.` };
22 return { ok: true, body: text };
23}
24
25/** How many messages a page holds: the default, or what was asked within 1 to the most. */
26export function pageSize(limit: unknown): number {
27 const n = typeof limit === "number" && Number.isFinite(limit) ? Math.floor(limit) : DEFAULT_PAGE;
28 return Math.min(MAX_PAGE, Math.max(1, n));
29}
30
31/**
32 * A page read one past its size, newest first: the page, and the cursor
33 * for the next one back (its oldest id), or null when this is the start.
34 */
35export function pageOf<T extends { id: string }>(rows: T[], size: number): { rows: T[]; older: string | null } {
36 if (rows.length <= size) return { rows, older: null };
37 const page = rows.slice(0, size);
38 return { rows: page, older: page[page.length - 1].id };
39}
40
41/** Messages an agent reads before replying when none is asked for, and the most. */
42export const DEFAULT_HISTORY = 30;
43export const MAX_HISTORY = 100;
44
45/** How many messages an agent's history holds: the default, or what was asked within 1 to the most. */
46export function historySize(limit: unknown): number {
47 const n = typeof limit === "number" && Number.isFinite(limit) ? Math.floor(limit) : DEFAULT_HISTORY;
48 return Math.min(MAX_HISTORY, Math.max(1, n));
49}
50
51/**
52 * An agent's history, oldest first, from rows read newest first: a
53 * thread's root (when there is one) leads, then the rest in order.
54 */
55export function historyOf<T extends { id: string }>(newestFirst: T[], root: T | null = null): T[] {
56 const rest = [...newestFirst].reverse().filter((row) => row.id !== root?.id);
57 return root ? [root, ...rest] : rest;
58}
59
60/**
61 * Whether a frame from a client's socket says it is typing in `channelId`:
62 * `{"type":"typing"}`, optionally naming the channel. A frame naming
63 * another channel is ignored.
64 */
65export function isTypingFrame(frame: unknown, channelId: string): boolean {
66 if (typeof frame !== "string" || frame.length > 1024) return false;
67 let said: unknown;
68 try {
69 said = JSON.parse(frame);
70 } catch {
71 return false;
72 }
73 if (!said || typeof said !== "object") return false;
74 const { type, channel_id } = said as { type?: unknown; channel_id?: unknown };
75 return type === "typing" && (channel_id === undefined || channel_id === null || channel_id === channelId);
76}
77
78/** The UTC day a time falls on, as the meter keys it. */
79export function meterDay(iso: string): string {
80 return iso.slice(0, 10);
81}