@columenlabs/lumen-ai 0.1.209 → 0.1.210
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 +4 -2
- package/dist/assistant/engine.d.ts +16 -1
- package/dist/assistant/engine.js +16 -2
- package/dist/assistant/models.js +1 -1
- package/dist/assistant/routes.js +8 -0
- package/dist/assistant/state.d.ts +29 -1
- package/dist/assistant/state.js +74 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
|
|
|
103
103
|
- **Hand off and advice.** The `escalate` tool hands one task to a higher tier (the person sets next, any or only when asked); Anthropic's advisor tool lets a higher tier send a plan, at most two an answer.
|
|
104
104
|
- **Chats** run on the agent (`./agent`, `LumenAgent`, below): the app routes `POST /chat` and each chat's reads, cards and Undo to its own agent, and `assistantRoutes` answers `POST /chat` with 410 `moved`. Chats saved by the old turn loop stay readable (`GET /chats`, `GET|DELETE /chats/:id`) until `assistant.purge()` drops them 30 days after their last message.
|
|
105
105
|
- **Screen moves**: while the person's Settings ▸ Screen is Move (the default), the panel sends its page map (`AssistantMessage.screen`: routes, nav pages, Settings sections, cards on screen, rows to open) and `LumenAgent` gives the model a `screen` tool (`./agent` `screen.ts`). Each call's moves (go, settings, chart, open, point) are checked against the map, kept in the chat and streamed as a `screen` event; the panel runs them through `AssistantScreenProvider` (mounted by `LumenShell`) with an Undo chip, and their outcomes go back with the next message (`outcomes`). Nothing is saved by a move.
|
|
106
|
-
- **Background work** an app runs for a person (a score, a summary) is `assistant.complete(caller, { task, system, prompt, images?, maxTokens })`: one answer on the task's tier (`tasks`, so the person picks its model in Settings ▸ Models), no chat and no tools (`images`, base64 with their media type, are read with the prompt: a scanned page, a screenshot), billed to their payer and counted in their spend and allowance like a chat turn. `maxTokens` (default 1,000) goes up to `COMPLETE_MAX_TOKENS` (32,000, thinking included); the answer is streamed, so a long one keeps its connection, and `cut: true` says it stopped at the cap before it ended (the app can ask again, shorter, or continue). It throws `AssistantError` `not_connected`, `app_cap` or `own_cap` when nobody can pay, so the caller waits and tries later.
|
|
106
|
+
- **Background work** an app runs for a person (a score, a summary) is `assistant.complete(caller, { task, system, prompt, images?, maxTokens })`: one answer on the task's tier (`tasks`, so the person picks its model in Settings ▸ Models), no chat and no tools (`images`, base64 with their media type, are read with the prompt: a scanned page, a screenshot), billed to their payer and counted in their spend and allowance like a chat turn. `maxTokens` (default 1,000) goes up to `COMPLETE_MAX_TOKENS` (32,000, thinking included); the answer is streamed, so a long one keeps its connection, and `cut: true` says it stopped at the cap before it ended (the app can ask again, shorter, or continue). It throws `AssistantError` `not_connected`, `app_cap` or `own_cap` when nobody can pay, so the caller waits and tries later. It returns the answer's `usd`, `model` and `usage` (tokens in, cache reads, out, per model). `run: { id, title }` makes the calls that share an `id` one line of the person's usage history (every round of one rebuild); without it a task's calls that day share a line.
|
|
107
|
+
- **Usage history.** Each chat (`agentTurn` with `chat`) and each run of background work is one line of the person's month (`usage:YYYY-MM`, the newest 300): its title, first and last use, tokens, cost and its parts per tier and model. `GET /usage?month=YYYY-MM` returns it beside that month's spend and the months that have any; `LumenAssistant`'s footer shows this month's total and opens it (`UsageHistoryView`).
|
|
107
108
|
|
|
108
109
|
### Commands, `@` and `#`
|
|
109
110
|
|
|
@@ -243,7 +244,8 @@ class Chat extends LumenAgent<Env> {
|
|
|
243
244
|
#settle?: (end: AgentTurnEnd) => Promise<void>;
|
|
244
245
|
async plan(turn: TurnContext) {
|
|
245
246
|
// this.input: what the person sent (message, page, refs, tier, time zone).
|
|
246
|
-
|
|
247
|
+
// chat: this chat's id, so its use is one line of the person's usage history.
|
|
248
|
+
const { plan, settle } = await assistant.agentTurn(caller, { ...(this.input ?? { message: '' }), chat: this.name });
|
|
247
249
|
this.#settle = settle;
|
|
248
250
|
return plan;
|
|
249
251
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import type { AgentTurnEnd, AgentTurnPlan } from '../agent/agent.js';
|
|
3
3
|
import { type BearingsExtra, type BearingsNow } from './bearings.js';
|
|
4
4
|
import { type ModelDef, type TaskDef, type TierDef } from './models.js';
|
|
@@ -186,6 +186,11 @@ export interface AgentTurnInput {
|
|
|
186
186
|
time_zone?: string;
|
|
187
187
|
/** When the chat's last message was, for "since you last asked". */
|
|
188
188
|
last_at?: number;
|
|
189
|
+
/**
|
|
190
|
+
* The chat this turn is in (the agent's name), so its use shows as one line of the person's
|
|
191
|
+
* usage history (`GET /usage`). Left out, the turn counts toward spend only.
|
|
192
|
+
*/
|
|
193
|
+
chat?: string;
|
|
189
194
|
}
|
|
190
195
|
export declare class Assistant {
|
|
191
196
|
private readonly o;
|
|
@@ -297,11 +302,21 @@ export declare class Assistant {
|
|
|
297
302
|
/** The answer's token cap, thinking included (default 1,000, at most COMPLETE_MAX_TOKENS). */
|
|
298
303
|
maxTokens?: number;
|
|
299
304
|
signal?: AbortSignal;
|
|
305
|
+
/**
|
|
306
|
+
* The run this call belongs to, as one line of the person's usage history (`GET /usage`):
|
|
307
|
+
* calls with the same `id` add up (every round of one rebuild). Left out, the task's calls
|
|
308
|
+
* that day share one line named after the task.
|
|
309
|
+
*/
|
|
310
|
+
run?: {
|
|
311
|
+
id: string;
|
|
312
|
+
title: string;
|
|
313
|
+
};
|
|
300
314
|
}): Promise<{
|
|
301
315
|
text: string;
|
|
302
316
|
usd: number;
|
|
303
317
|
model: string;
|
|
304
318
|
cut: boolean;
|
|
319
|
+
usage: UsagePart;
|
|
305
320
|
}>;
|
|
306
321
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
307
322
|
startSignin(caller: Caller, provider: AssistantProvider): Promise<{
|
package/dist/assistant/engine.js
CHANGED
|
@@ -332,6 +332,10 @@ export class Assistant {
|
|
|
332
332
|
};
|
|
333
333
|
const settle = async (end) => {
|
|
334
334
|
await this.state.addSpend(caller.id, paying.payer, end.parts).catch(() => undefined);
|
|
335
|
+
if (input.chat)
|
|
336
|
+
await this.state
|
|
337
|
+
.addUsage(caller.id, { id: `chat:${input.chat}`, kind: 'chat', title: message.split('\n')[0] || 'Chat' }, end.parts)
|
|
338
|
+
.catch(() => undefined);
|
|
335
339
|
const total = end.parts.reduce((a, m) => ({
|
|
336
340
|
calls: a.calls + m.calls,
|
|
337
341
|
in: a.in + m.in,
|
|
@@ -963,9 +967,19 @@ export class Assistant {
|
|
|
963
967
|
throw new AssistantError(`${s.model.label} did not answer: ${String(e?.message ?? e).slice(0, 200)}`, 'upstream', 502);
|
|
964
968
|
}
|
|
965
969
|
finally {
|
|
966
|
-
|
|
970
|
+
const part = s.meter.part();
|
|
971
|
+
await this.state.addSpend(caller.id, paying.payer, [part]);
|
|
972
|
+
const task = this.tasks.find((t) => t.id === o.task);
|
|
973
|
+
await this.state
|
|
974
|
+
.addUsage(caller.id, {
|
|
975
|
+
id: `task:${o.task}:${o.run?.id ?? new Date().toISOString().slice(0, 10)}`,
|
|
976
|
+
kind: 'task',
|
|
977
|
+
task: o.task,
|
|
978
|
+
title: o.run?.title ?? task?.label ?? o.task,
|
|
979
|
+
}, [part])
|
|
980
|
+
.catch(() => undefined);
|
|
967
981
|
}
|
|
968
|
-
return { text, usd: s.meter.usd, model: s.model.id, cut };
|
|
982
|
+
return { text, usd: s.meter.usd, model: s.model.id, cut, usage: s.meter.part() };
|
|
969
983
|
}
|
|
970
984
|
// ---------------------------------------------------------------- keys
|
|
971
985
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
package/dist/assistant/models.js
CHANGED
|
@@ -126,7 +126,7 @@ export class Meter {
|
|
|
126
126
|
}
|
|
127
127
|
part() {
|
|
128
128
|
const { label, calls, read, out, usd } = this;
|
|
129
|
-
return { label, calls, in: this.in, read, out, usd };
|
|
129
|
+
return { label, calls, in: this.in, read, out, usd, model: this.model.id };
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
/** The request's effort setting: nothing at the model's own default. */
|
package/dist/assistant/routes.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* PUT /web/scrapeql {client_id, client_secret}, DELETE /web/scrapeql, POST /web/test admin
|
|
21
21
|
* POST /chat one answer as server-sent events; closing the request stops it
|
|
22
22
|
* GET /chats, GET|DELETE /chats/:id
|
|
23
|
+
* GET /usage?month=YYYY-MM the person's use that month: total, per tier and model, each chat and run
|
|
23
24
|
* POST /chats/:id/drafts/:draft {action: save | dismiss}
|
|
24
25
|
* POST /chats/:id/writes/:step/undo
|
|
25
26
|
* POST /allowance/request {amount, why} asks for more allowance (one open at a time)
|
|
@@ -321,6 +322,13 @@ export function assistantRoutes(app, o) {
|
|
|
321
322
|
return fail('Chats run on the agent now. Start a new chat.', 410, 'moved');
|
|
322
323
|
case 'GET /chats':
|
|
323
324
|
return json(await a.listChats(caller));
|
|
325
|
+
// The person's own use: one month's total, per tier and model, and each chat and run.
|
|
326
|
+
case 'GET /usage': {
|
|
327
|
+
const month = url.searchParams.get('month') ?? undefined;
|
|
328
|
+
if (month !== undefined && !/^\d{4}-\d{2}$/.test(month))
|
|
329
|
+
return fail('month is YYYY-MM.');
|
|
330
|
+
return json(await a.state.usage(caller.id, month));
|
|
331
|
+
}
|
|
324
332
|
case 'POST /allowance/request': {
|
|
325
333
|
const b = await body(req);
|
|
326
334
|
const amount = typeof b.amount === 'number' && Number.isFinite(b.amount) ? b.amount : 0;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tier runs on and money left this month, else the other; with neither the assistant waits and says
|
|
20
20
|
* why. A cap covers every provider of its payer.
|
|
21
21
|
*/
|
|
22
|
-
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantScreenSetting, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
22
|
+
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantScreenSetting, AssistantWait, HandOffPolicy, UsageEntry, UsageHistory, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
23
23
|
import { type ChatgptTokens } from './chatgpt.js';
|
|
24
24
|
import { type Docs } from './store.js';
|
|
25
25
|
export interface KeyTest {
|
|
@@ -98,6 +98,22 @@ export type SpendRow = UsagePart & {
|
|
|
98
98
|
export interface SpendDoc {
|
|
99
99
|
rows: SpendRow[];
|
|
100
100
|
}
|
|
101
|
+
/** Chats and runs kept in one month's usage history: the newest this many. */
|
|
102
|
+
export declare const USAGE_KEPT = 300;
|
|
103
|
+
export interface UsageDoc {
|
|
104
|
+
entries: UsageEntry[];
|
|
105
|
+
}
|
|
106
|
+
/** Adds `more` to `parts`, merged by label (the model kept from the first). */
|
|
107
|
+
export declare function mergeParts(parts: UsagePart[], more: UsagePart[]): {
|
|
108
|
+
label: string;
|
|
109
|
+
calls: number;
|
|
110
|
+
in: number;
|
|
111
|
+
read: number;
|
|
112
|
+
out: number;
|
|
113
|
+
usd: number;
|
|
114
|
+
runs?: number;
|
|
115
|
+
model?: string;
|
|
116
|
+
}[];
|
|
101
117
|
/** USD a month when nobody set a cap. */
|
|
102
118
|
export declare const DEFAULT_CAP = 10;
|
|
103
119
|
/** `2026-10` in the app's time zone. */
|
|
@@ -191,6 +207,18 @@ export declare class State {
|
|
|
191
207
|
spend(id: string, month?: string): Promise<SpendDoc>;
|
|
192
208
|
/** Adds an answer's use to this month's spend, by label and payer. */
|
|
193
209
|
addSpend(id: string, payer: AssistantPayer, parts: UsagePart[]): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
|
|
212
|
+
* chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only.
|
|
213
|
+
*/
|
|
214
|
+
addUsage(id: string, e: {
|
|
215
|
+
id: string;
|
|
216
|
+
kind: UsageEntry['kind'];
|
|
217
|
+
task?: string;
|
|
218
|
+
title: string;
|
|
219
|
+
}, parts: UsagePart[]): Promise<void>;
|
|
220
|
+
/** One month's history (this month by default) beside its spend, and the months that have one. */
|
|
221
|
+
usage(id: string, month?: string): Promise<UsageHistory>;
|
|
194
222
|
/** The payer's key of each provider, those set, and which are plan sign-ins. */
|
|
195
223
|
keysOf(id: string, payer: AssistantPayer): Promise<{
|
|
196
224
|
keys: Partial<Record<"anthropic" | "openai" | "xai", string>>;
|
package/dist/assistant/state.js
CHANGED
|
@@ -7,6 +7,33 @@ export const keyDocName = (provider) => provider === 'anthropic' ? 'key' : `key:
|
|
|
7
7
|
export const planDocName = (provider) => `plan:${provider}`;
|
|
8
8
|
/** Requests kept for approvers: the newest this many. */
|
|
9
9
|
const REQUESTS_KEPT = 300;
|
|
10
|
+
/** Chats and runs kept in one month's usage history: the newest this many. */
|
|
11
|
+
export const USAGE_KEPT = 300;
|
|
12
|
+
/** Months of usage history offered (`usage:YYYY-MM` documents are listed this far back). */
|
|
13
|
+
const USAGE_MONTHS = 24;
|
|
14
|
+
/** Adds `more` to `parts`, merged by label (the model kept from the first). */
|
|
15
|
+
export function mergeParts(parts, more) {
|
|
16
|
+
const out = parts.map((p) => ({ ...p }));
|
|
17
|
+
for (const p of more) {
|
|
18
|
+
if (!p.calls)
|
|
19
|
+
continue;
|
|
20
|
+
const prev = out.find((r) => r.label === p.label);
|
|
21
|
+
if (!prev)
|
|
22
|
+
out.push({ ...p });
|
|
23
|
+
else {
|
|
24
|
+
prev.calls += p.calls;
|
|
25
|
+
prev.in += p.in;
|
|
26
|
+
prev.read += p.read;
|
|
27
|
+
prev.out += p.out;
|
|
28
|
+
prev.usd += p.usd;
|
|
29
|
+
if (p.runs)
|
|
30
|
+
prev.runs = (prev.runs ?? 0) + p.runs;
|
|
31
|
+
if (!prev.model && p.model)
|
|
32
|
+
prev.model = p.model;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
10
37
|
/** USD a month when nobody set a cap. */
|
|
11
38
|
export const DEFAULT_CAP = 10;
|
|
12
39
|
/** `2026-10` in the app's time zone. */
|
|
@@ -227,6 +254,53 @@ export class State {
|
|
|
227
254
|
await this.setAllowance(id, (d) => ({ ...d, spent_usd: (d.spent_usd ?? 0) + usd }));
|
|
228
255
|
}
|
|
229
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
|
|
259
|
+
* chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only.
|
|
260
|
+
*/
|
|
261
|
+
async addUsage(id, e, parts) {
|
|
262
|
+
if (!parts.some((p) => p.calls))
|
|
263
|
+
return;
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
await this.docs.update(id, `usage:${this.month()}`, (d) => {
|
|
266
|
+
const entries = [...(d?.entries ?? [])];
|
|
267
|
+
const i = entries.findIndex((x) => x.id === e.id);
|
|
268
|
+
const prev = i >= 0 ? entries.splice(i, 1)[0] : undefined;
|
|
269
|
+
const merged = mergeParts(prev?.parts ?? [], parts);
|
|
270
|
+
const sum = (k) => merged.reduce((a, p) => a + p[k], 0);
|
|
271
|
+
entries.unshift({
|
|
272
|
+
id: e.id,
|
|
273
|
+
kind: e.kind,
|
|
274
|
+
...(e.task ? { task: e.task } : {}),
|
|
275
|
+
title: prev?.title || e.title.slice(0, 120),
|
|
276
|
+
first: prev?.first ?? now,
|
|
277
|
+
at: now,
|
|
278
|
+
calls: sum('calls'),
|
|
279
|
+
in: sum('in'),
|
|
280
|
+
read: sum('read'),
|
|
281
|
+
out: sum('out'),
|
|
282
|
+
usd: sum('usd'),
|
|
283
|
+
parts: merged,
|
|
284
|
+
});
|
|
285
|
+
return { entries: entries.slice(0, USAGE_KEPT) };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/** One month's history (this month by default) beside its spend, and the months that have one. */
|
|
289
|
+
async usage(id, month = this.month()) {
|
|
290
|
+
const [doc, spend, listed] = await Promise.all([
|
|
291
|
+
this.docs.read(id, `usage:${month}`),
|
|
292
|
+
this.spend(id, month),
|
|
293
|
+
this.docs.list(id, 'usage:', USAGE_MONTHS),
|
|
294
|
+
]);
|
|
295
|
+
const months = [...new Set([this.month(), ...listed.map((r) => r.key.slice(6))])].sort((a, b) => b.localeCompare(a));
|
|
296
|
+
return {
|
|
297
|
+
month,
|
|
298
|
+
months,
|
|
299
|
+
usd: spend.rows.reduce((a, r) => a + r.usd, 0),
|
|
300
|
+
rows: spend.rows,
|
|
301
|
+
entries: doc?.entries ?? [],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
230
304
|
/** The payer's key of each provider, those set, and which are plan sign-ins. */
|
|
231
305
|
async keysOf(id, payer) {
|
|
232
306
|
const ids = Object.keys(PROVIDERS);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.210",
|
|
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",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@ai-sdk/openai": "4.0.108",
|
|
53
53
|
"@anthropic-ai/sdk": "0.133.0",
|
|
54
54
|
"@cloudflare/think": "0.23.0",
|
|
55
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
56
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
55
|
+
"@columenlabs/lumen-ops": "0.1.210",
|
|
56
|
+
"@columenlabs/lumen-ui-types": "0.1.210",
|
|
57
57
|
"agents": "0.32.0",
|
|
58
58
|
"ai": "7.0.162",
|
|
59
59
|
"just-bash": "3.6.0",
|