@columenlabs/lumen-ai 0.1.223 → 0.1.225

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
@@ -103,7 +103,7 @@ 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. 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.
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). `effort` sets how hard the model thinks on that one call (`low` to `max`; on an OpenAI or xAI model it is the request's reasoning effort), over what the person saved for the tier. `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. The month's spend also adds up per task and model (`spend:YYYY-MM` `tasks`: answers, calls, tokens, cost and time, a chat answer as `chat`), so an app's admin page can show what each kind of work costs; `tag` counts one call under another name when a task does more than one kind of work (a first try, a repair).
107
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`).
108
108
 
109
109
  ### Commands, `@` and `#`
@@ -1,4 +1,4 @@
1
- import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
1
+ import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantEffort, 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';
@@ -306,6 +306,12 @@ export declare class Assistant {
306
306
  images?: CompleteImage[];
307
307
  /** The answer's token cap, thinking included (default 1,000, at most COMPLETE_MAX_TOKENS). */
308
308
  maxTokens?: number;
309
+ /**
310
+ * How hard the model thinks on this one call, whatever the person saved for the tier. On an
311
+ * OpenAI or xAI model it is the request's reasoning effort (`max` asks for `high`); left out,
312
+ * those models run at their own default.
313
+ */
314
+ effort?: AssistantEffort;
309
315
  signal?: AbortSignal;
310
316
  /**
311
317
  * The run this call belongs to, as one line of the person's usage history (`GET /usage`):
@@ -316,6 +322,11 @@ export declare class Assistant {
316
322
  id: string;
317
323
  title: string;
318
324
  };
325
+ /**
326
+ * What this call counts as in the month's spend per task (`spend:YYYY-MM` `tasks`), when one
327
+ * task does more than one kind of work (a first try, a repair). Left out, it is the task.
328
+ */
329
+ tag?: string;
319
330
  }): Promise<{
320
331
  text: string;
321
332
  usd: number;
@@ -332,7 +332,9 @@ export class Assistant {
332
332
  : {},
333
333
  };
334
334
  const settle = async (end) => {
335
- await this.state.addSpend(caller.id, paying.payer, end.parts).catch(() => undefined);
335
+ await this.state
336
+ .addSpend(caller.id, paying.payer, end.parts, { task: 'chat', ms: end.ms })
337
+ .catch(() => undefined);
336
338
  if (input.chat)
337
339
  await this.state
338
340
  .addUsage(caller.id, { id: `chat:${input.chat}`, kind: 'chat', title: message.split('\n')[0] || 'Chat' }, end.parts)
@@ -831,7 +833,7 @@ export class Assistant {
831
833
  * events like the chat loop, because a ChatGPT plan sign-in answers only streamed calls, takes no
832
834
  * output cap and may send its words only as deltas.
833
835
  */
834
- async openaiOnce(clients, s, instructions, input, max, signal, images) {
836
+ async openaiOnce(clients, s, instructions, input, max, signal, images, effort) {
835
837
  // xAI's API is OpenAI's Responses API at another address.
836
838
  const xai = s.model.provider === 'xai';
837
839
  const client = (xai ? clients.xai : clients.openai);
@@ -861,6 +863,8 @@ export class Assistant {
861
863
  store: false,
862
864
  stream: true,
863
865
  ...(plan ? {} : { max_output_tokens: max }),
866
+ ...(effort &&
867
+ effort !== 'default' && { reasoning: { effort: effort === 'max' ? 'high' : effort } }),
864
868
  }, signal ? { signal } : {});
865
869
  for await (const e of stream) {
866
870
  if (e.type === 'response.output_text.delta' && typeof e.delta === 'string')
@@ -907,6 +911,7 @@ export class Assistant {
907
911
  * `not_connected`, `app_cap` or `own_cap` when nobody can pay, or `upstream` when the model fails.
908
912
  */
909
913
  async complete(caller, o) {
914
+ const began = Date.now();
910
915
  const s = await this.session(caller, await this.mainTier(caller, o.task));
911
916
  const { paying, wait } = await this.state.paying(caller.id, s.model.provider);
912
917
  if (!paying) {
@@ -921,7 +926,7 @@ export class Assistant {
921
926
  try {
922
927
  if ((s.model.provider === 'openai' && clients.openai) ||
923
928
  (s.model.provider === 'xai' && clients.xai)) {
924
- ({ text, cut } = await this.openaiOnce(clients, s, o.system, o.prompt, max, o.signal, o.images));
929
+ ({ text, cut } = await this.openaiOnce(clients, s, o.system, o.prompt, max, o.signal, o.images, o.effort));
925
930
  }
926
931
  else if (clients.anthropic) {
927
932
  // Streamed and read whole: a long answer (minutes at a high cap) keeps its connection alive,
@@ -949,7 +954,7 @@ export class Assistant {
949
954
  : o.prompt,
950
955
  },
951
956
  ],
952
- ...effortParams(s.effort),
957
+ ...effortParams(o.effort ?? s.effort),
953
958
  }, o.signal ? { signal: o.signal } : undefined)
954
959
  .finalMessage();
955
960
  s.meter.add(msg.usage);
@@ -969,7 +974,10 @@ export class Assistant {
969
974
  }
970
975
  finally {
971
976
  const part = s.meter.part();
972
- await this.state.addSpend(caller.id, paying.payer, [part]);
977
+ await this.state.addSpend(caller.id, paying.payer, [part], {
978
+ task: o.tag ?? o.task,
979
+ ms: Date.now() - began,
980
+ });
973
981
  const task = this.tasks.find((t) => t.id === o.task);
974
982
  await this.state
975
983
  .addUsage(caller.id, {
@@ -19,7 +19,7 @@ export * from './find.js';
19
19
  export * from './meaning.js';
20
20
  export * from './models.js';
21
21
  export * from './routes.js';
22
- export { DEFAULT_CAP, type KeyDoc, keyDocName, type KeyTest, monthIn, type Paying, type PersonDoc, type SpendDoc, State, type TokenDoc, waitText, } from './state.js';
22
+ export { DEFAULT_CAP, type KeyDoc, keyDocName, type KeyTest, monthIn, type Paying, type PersonDoc, type SpendDoc, type TaskSpend, State, type TokenDoc, waitText, } from './state.js';
23
23
  export * from './store.js';
24
24
  export * from './tools.js';
25
25
  export * from './web.js';
@@ -97,8 +97,30 @@ export interface RequestsDoc {
97
97
  export type SpendRow = UsagePart & {
98
98
  payer: AssistantPayer;
99
99
  };
100
+ /**
101
+ * One task's use of one model this month: what a kind of work cost (a chat answer, a background
102
+ * task), beside `rows`, which stay per tier and payer.
103
+ */
104
+ export interface TaskSpend {
105
+ /** The task's id (`chat`, or a `complete` call's `tag` or `task`). */
106
+ task: string;
107
+ /** The model's id. */
108
+ model: string;
109
+ /** Answers: chat turns, or `complete` calls. */
110
+ runs: number;
111
+ /** Model calls. */
112
+ calls: number;
113
+ in: number;
114
+ read: number;
115
+ out: number;
116
+ usd: number;
117
+ /** Time the answers took in all, in ms (`ms / runs` is the average). */
118
+ ms: number;
119
+ }
100
120
  export interface SpendDoc {
101
121
  rows: SpendRow[];
122
+ /** Per task and model, counted since the kit began tagging (older spend is only in `rows`). */
123
+ tasks?: TaskSpend[];
102
124
  }
103
125
  /** Chats and runs kept in one month's usage history: the newest this many. */
104
126
  export declare const USAGE_KEPT = 300;
@@ -210,8 +232,14 @@ export declare class State {
210
232
  } | null>;
211
233
  appCap(): Promise<number>;
212
234
  spend(id: string, month?: string): Promise<SpendDoc>;
213
- /** Adds an answer's use to this month's spend, by label and payer. */
214
- addSpend(id: string, payer: AssistantPayer, parts: UsagePart[]): Promise<void>;
235
+ /**
236
+ * Adds an answer's use to this month's spend, by label and payer. `at` names the task the answer
237
+ * was for and how long it took, so the month also adds up per task and model (`tasks`).
238
+ */
239
+ addSpend(id: string, payer: AssistantPayer, parts: UsagePart[], at?: {
240
+ task: string;
241
+ ms?: number;
242
+ }): Promise<void>;
215
243
  /**
216
244
  * Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
217
245
  * chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only.
@@ -227,12 +227,43 @@ export class State {
227
227
  spend(id, month = this.month()) {
228
228
  return this.docs.read(id, `spend:${month}`).then((d) => d ?? { rows: [] });
229
229
  }
230
- /** Adds an answer's use to this month's spend, by label and payer. */
231
- async addSpend(id, payer, parts) {
230
+ /**
231
+ * Adds an answer's use to this month's spend, by label and payer. `at` names the task the answer
232
+ * was for and how long it took, so the month also adds up per task and model (`tasks`).
233
+ */
234
+ async addSpend(id, payer, parts, at) {
232
235
  if (!parts.some((p) => p.calls))
233
236
  return;
234
237
  await this.docs.update(id, `spend:${this.month()}`, (d) => {
235
238
  const rows = [...(d?.rows ?? [])];
239
+ const tasks = [...(d?.tasks ?? [])];
240
+ if (at) {
241
+ // The answer and its time count once, on its first model.
242
+ let first = true;
243
+ for (const p of parts) {
244
+ if (!p.calls)
245
+ continue;
246
+ const model = p.model ?? p.label;
247
+ const i = tasks.findIndex((t) => t.task === at.task && t.model === model);
248
+ const prev = i >= 0 ? tasks[i] : undefined;
249
+ const row = {
250
+ task: at.task,
251
+ model,
252
+ runs: (prev?.runs ?? 0) + (first ? 1 : 0),
253
+ calls: (prev?.calls ?? 0) + p.calls,
254
+ in: (prev?.in ?? 0) + p.in,
255
+ read: (prev?.read ?? 0) + p.read,
256
+ out: (prev?.out ?? 0) + p.out,
257
+ usd: (prev?.usd ?? 0) + p.usd,
258
+ ms: (prev?.ms ?? 0) + (first ? (at.ms ?? 0) : 0),
259
+ };
260
+ first = false;
261
+ if (i >= 0)
262
+ tasks[i] = row;
263
+ else
264
+ tasks.push(row);
265
+ }
266
+ }
236
267
  for (const p of parts) {
237
268
  if (!p.calls)
238
269
  continue;
@@ -253,7 +284,7 @@ export class State {
253
284
  else
254
285
  rows.push(row);
255
286
  }
256
- return { rows };
287
+ return tasks.length ? { rows, tasks } : { rows };
257
288
  });
258
289
  if (payer === 'app' && this.o.allowance?.period === 'once') {
259
290
  const usd = parts.reduce((a, p) => a + p.usd, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.223",
3
+ "version": "0.1.225",
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.223",
56
- "@columenlabs/lumen-ui-types": "0.1.223",
55
+ "@columenlabs/lumen-ops": "0.1.225",
56
+ "@columenlabs/lumen-ui-types": "0.1.225",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",