@columenlabs/lumen-ai 0.1.135 → 0.1.136

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
@@ -141,3 +141,18 @@ The panel's composer lists what can follow three keys as they are typed. The kit
141
141
  `target` lights the thing on screen while its row is hovered. A page's source replaces a standing one with the same `id`.
142
142
 
143
143
  Text the person highlights on the page is offered first under `#` (Selected text), with where it sits from the nearest `data-assistant-where` attribute. To hand a highlight over from the page, an Ask button calls `composeAssistant({ refs: [selectionRef({ text, where })], text: '' })` (or any refs and text): the panel opens with the chips in the composer, focused for the person to finish.
144
+
145
+ ### Bearings
146
+
147
+ Each message the model gets starts with a few lines of bearings: the time now in the person's zone (the panel sends the browser's), UTC, how long since the chat's last message and the app's name. They go in the message, not the system prompt, so the prompt cache holds. An app adds its own lines with `bearings`; each call gets 300 ms, and when it is late or throws its lines are left out.
148
+
149
+ ```ts
150
+ createAssistant({
151
+ // ...
152
+ app: 'Notes',
153
+ timeZone: 'America/Los_Angeles', // when the browser sends none
154
+ bearings: ({ now, at, span }) => `Next backup: ${at(nextBackup(now))} (in ${span(nextBackup(now) - now)})`,
155
+ });
156
+ ```
157
+
158
+ An app with its own assistant loop builds the same block with `bearings({ timeZone, lastAt, app, extras })` and puts it before the message.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Bearings: a few lines at the top of each message telling the model when and where it is (the time
3
+ * in the person's zone, how long since the chat's last message, the app), so it never guesses
4
+ * "today". Kept out of the system prompt so the prompt cache holds. An app adds its own lines (a
5
+ * market's session, a billing day) through `extras`; each gets `extrasMs` and is left out when it is
6
+ * late or throws, so bearings never hold a turn.
7
+ */
8
+ /** What an app's extra lines are worked out from. */
9
+ export interface BearingsNow {
10
+ /** The moment, ms since the epoch. */
11
+ now: number;
12
+ /** The person's IANA zone (checked). */
13
+ timeZone: string;
14
+ /** A moment in the person's zone: `Tue, Oct 27, 2026, 3:05 PM PDT`. */
15
+ at: (ms: number) => string;
16
+ /** A span as the model reads it: `1h 55m`, `2d 3h`. */
17
+ span: (ms: number) => string;
18
+ }
19
+ /** An app's own bearings: lines (or one line) to add, nothing to add none. */
20
+ export type BearingsExtra = (b: BearingsNow) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>;
21
+ export interface BearingsInput {
22
+ /** The moment; now by default. */
23
+ now?: number;
24
+ /** The person's IANA zone (the browser's); `fallbackZone` when missing or unknown. */
25
+ timeZone?: string;
26
+ /** UTC by default. */
27
+ fallbackZone?: string;
28
+ /** When this chat's last message was, ms; left out on a new chat. */
29
+ lastAt?: number;
30
+ /** The app's name (`App: …`). */
31
+ app?: string;
32
+ extras?: BearingsExtra | BearingsExtra[];
33
+ /** How long each extra may take; 300 ms. */
34
+ extrasMs?: number;
35
+ }
36
+ /** `zone` when Intl knows it, else undefined. */
37
+ export declare function knownZone(zone: string | undefined): string | undefined;
38
+ /** A moment in `timeZone`: `Tue, Oct 27, 2026, 3:05 PM PDT`. */
39
+ export declare function bearingTime(ms: number, timeZone: string): string;
40
+ /** A span: `45s`, `12m`, `1h 55m`, `2d 3h`. */
41
+ export declare function bearingSpan(ms: number): string;
42
+ /** The bearings block, ending in a blank line, to put before the person's message. */
43
+ export declare function bearings(input?: BearingsInput): Promise<string>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Bearings: a few lines at the top of each message telling the model when and where it is (the time
3
+ * in the person's zone, how long since the chat's last message, the app), so it never guesses
4
+ * "today". Kept out of the system prompt so the prompt cache holds. An app adds its own lines (a
5
+ * market's session, a billing day) through `extras`; each gets `extrasMs` and is left out when it is
6
+ * late or throws, so bearings never hold a turn.
7
+ */
8
+ const EXTRAS_MS = 300;
9
+ /** `zone` when Intl knows it, else undefined. */
10
+ export function knownZone(zone) {
11
+ if (!zone || zone.length > 64)
12
+ return undefined;
13
+ try {
14
+ new Intl.DateTimeFormat('en-US', { timeZone: zone });
15
+ return zone;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ /** A moment in `timeZone`: `Tue, Oct 27, 2026, 3:05 PM PDT`. */
22
+ export function bearingTime(ms, timeZone) {
23
+ return new Intl.DateTimeFormat('en-US', {
24
+ timeZone,
25
+ weekday: 'short',
26
+ month: 'short',
27
+ day: 'numeric',
28
+ year: 'numeric',
29
+ hour: 'numeric',
30
+ minute: '2-digit',
31
+ timeZoneName: 'short',
32
+ }).format(ms);
33
+ }
34
+ /** A span: `45s`, `12m`, `1h 55m`, `2d 3h`. */
35
+ export function bearingSpan(ms) {
36
+ const m = Math.round(Math.abs(ms) / 60_000);
37
+ if (m < 1)
38
+ return `${Math.round(Math.abs(ms) / 1000)}s`;
39
+ if (m < 60)
40
+ return `${m}m`;
41
+ const h = Math.floor(m / 60);
42
+ if (h < 24)
43
+ return m % 60 ? `${h}h ${m % 60}m` : `${h}h`;
44
+ const d = Math.floor(h / 24);
45
+ return h % 24 ? `${d}d ${h % 24}h` : `${d}d`;
46
+ }
47
+ function ago(ms) {
48
+ const m = ms / 60_000;
49
+ if (m < 1)
50
+ return 'just now';
51
+ if (m < 60)
52
+ return `${Math.round(m)} min ago`;
53
+ const h = m / 60;
54
+ if (h < 24)
55
+ return `${Math.round(h)} h ago`;
56
+ const d = Math.round(h / 24);
57
+ return `${d} day${d === 1 ? '' : 's'} ago`;
58
+ }
59
+ async function within(extra, b, ms) {
60
+ let timer;
61
+ const late = new Promise((done) => {
62
+ timer = setTimeout(() => done(undefined), ms);
63
+ });
64
+ try {
65
+ const got = await Promise.race([Promise.resolve().then(() => extra(b)), late]);
66
+ if (!got)
67
+ return [];
68
+ return (Array.isArray(got) ? got : [got]).filter((l) => typeof l === 'string' && l.trim());
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+ /** The bearings block, ending in a blank line, to put before the person's message. */
78
+ export async function bearings(input = {}) {
79
+ const now = input.now ?? Date.now();
80
+ const timeZone = knownZone(input.timeZone) ?? knownZone(input.fallbackZone) ?? 'UTC';
81
+ const b = {
82
+ now,
83
+ timeZone,
84
+ at: (ms) => bearingTime(ms, timeZone),
85
+ span: bearingSpan,
86
+ };
87
+ const utc = new Date(now).toISOString().slice(11, 16);
88
+ const lines = [
89
+ "Bearings (the server's clock: use it for today, now and how long ago; never guess the date):",
90
+ `Now: ${b.at(now)} (${timeZone}) · ${utc} UTC`,
91
+ ];
92
+ if (input.lastAt && input.lastAt < now)
93
+ lines.push(`Last message in this chat: ${ago(now - input.lastAt)} (${b.at(input.lastAt)})`);
94
+ if (input.app)
95
+ lines.push(`App: ${input.app}`);
96
+ const extras = input.extras ? (Array.isArray(input.extras) ? input.extras : [input.extras]) : [];
97
+ const more = await Promise.all(extras.map((e) => within(e, b, input.extrasMs ?? EXTRAS_MS)));
98
+ for (const l of more.flat())
99
+ lines.push(l.slice(0, 400));
100
+ return `${lines.join('\n')}\n\n`;
101
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
2
+ import { type BearingsExtra, type BearingsNow } from './bearings.js';
2
3
  import { type ModelDef, type TaskDef, type TierDef } from './models.js';
3
4
  import { State } from './state.js';
4
5
  import { type AssistantStore, type Sealer } from './store.js';
@@ -140,8 +141,15 @@ export interface AssistantOptions {
140
141
  defaultCap?: number;
141
142
  /** An allowance on the shared keys a person can ask to raise (Request more). */
142
143
  allowance?: AllowanceOptions;
143
- /** Months and dates in this zone; UTC by default. */
144
+ /** Months and dates in this zone, and a person's zone when their browser sends none; UTC by default. */
144
145
  timeZone?: string;
146
+ /** The app's name, told to the model in its bearings (`App: …`). */
147
+ app?: string;
148
+ /**
149
+ * The app's own lines for the bearings at the top of each message (a market's session, a billing
150
+ * day). Given 300 ms; late or failing, its lines are left out.
151
+ */
152
+ bearings?: (b: BearingsNow, caller: Caller) => ReturnType<BearingsExtra>;
145
153
  /** Chats are deleted this many days after their last message (`purge`); 30. */
146
154
  keepDays?: number;
147
155
  /** The fetch the model providers and ScrapeQL calls go through. */
@@ -19,6 +19,7 @@
19
19
  import Anthropic from '@anthropic-ai/sdk';
20
20
  import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
21
21
  import OpenAI from 'openai';
22
+ import { bearings } from './bearings.js';
22
23
  import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
23
24
  import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, effortParams, } from './models.js';
24
25
  import { DEFAULT_CAP, State, keyDocName, keyState, planDocName, waitText, } from './state.js';
@@ -711,7 +712,15 @@ export class Assistant {
711
712
  return;
712
713
  }
713
714
  }
714
- const page = this.cardNews(doc) +
715
+ const told = await bearings({
716
+ timeZone: req.time_zone,
717
+ fallbackZone: this.o.timeZone,
718
+ lastAt: prev?.at,
719
+ app: this.o.app,
720
+ ...(this.o.bearings ? { extras: (b) => this.o.bearings?.(b, caller) } : {}),
721
+ });
722
+ const page = told +
723
+ this.cardNews(doc) +
715
724
  (req.refs?.length
716
725
  ? `The person points at these parts: they are what "this" means, and when the person asks for a change they are the only parts to change. Read anything else you need (the rest of the page, other records, your tools) for context, but leave it as it is unless they ask.\n${JSON.stringify(req.refs).slice(0, 16000)}\n\n`
717
726
  : '') +
@@ -834,7 +843,7 @@ export class Assistant {
834
843
  /** What the kit adds to the app's instructions: tools, drafts, hand off, advice, limits. */
835
844
  rules(main, policy, advisor, fns, failed) {
836
845
  const lines = [
837
- `You are ${this.name}. Today is ${new Date().toISOString().slice(0, 10)}.`,
846
+ `You are ${this.name}. Each message starts with Bearings: the time now in the person's zone and where they are. Take today, now and how long ago from it; never guess the date.`,
838
847
  'Answer in short plain sentences. Use the tools for facts; never guess a number a tool can give you. Call several tools at once when they do not depend on each other.',
839
848
  'A tool marked DRAFT ONLY makes a card the person saves or dismisses; it changes nothing by itself. Say what the card will do; never claim it is done. The next message tells you how each card ended.',
840
849
  'A tool marked WRITES AT ONCE makes its change when you call it; the person can undo it from the chat. Change only what they asked for, and say what you changed.',
@@ -12,6 +12,7 @@
12
12
  * The ChatGPT plan sign-in's parts are exported on their own too, for an app that keeps the tokens and
13
13
  * calls OpenAI itself (`LumenChatgptSignin` is the panel's part for it).
14
14
  */
15
+ export * from './bearings.js';
15
16
  export * from './chatgpt.js';
16
17
  export * from './engine.js';
17
18
  export * from './models.js';
@@ -12,6 +12,7 @@
12
12
  * The ChatGPT plan sign-in's parts are exported on their own too, for an app that keeps the tokens and
13
13
  * calls OpenAI itself (`LumenChatgptSignin` is the panel's part for it).
14
14
  */
15
+ export * from './bearings.js';
15
16
  export * from './chatgpt.js';
16
17
  export * from './engine.js';
17
18
  export * from './models.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
4
4
  "description": "Lumen's assistant and agent parts: a chat assistant's server (Claude tiers, tools over MCP, the web, hand off, spend caps), serving an MCP server from a Worker, read-only tool sets and metered tool calls.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@anthropic-ai/sdk": "0.133.0",
44
- "@columenlabs/lumen-ops": "0.1.135",
45
- "@columenlabs/lumen-ui-types": "0.1.135",
44
+ "@columenlabs/lumen-ops": "0.1.136",
45
+ "@columenlabs/lumen-ui-types": "0.1.136",
46
46
  "openai": "7.32.0",
47
47
  "zod": "^4.3.6"
48
48
  },