@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 +15 -0
- package/dist/assistant/bearings.d.ts +43 -0
- package/dist/assistant/bearings.js +101 -0
- package/dist/assistant/engine.d.ts +9 -1
- package/dist/assistant/engine.js +11 -2
- package/dist/assistant/index.d.ts +1 -0
- package/dist/assistant/index.js +1 -0
- package/package.json +3 -3
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. */
|
package/dist/assistant/engine.js
CHANGED
|
@@ -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
|
|
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}.
|
|
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';
|
package/dist/assistant/index.js
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
45
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
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
|
},
|