@columenlabs/lumen-ai 0.1.253 → 0.1.254

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
@@ -254,7 +254,7 @@ Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agen
254
254
 
255
255
  **The panel.** `LumenAssistant` reads a Think chat with no change. `ask(input, sink)` runs one answer and sends the panel's own events (`AssistantEvent`: the chat, the text, each tool line, cards, hand-off sections, advice, the use) to `sink`. The app's `POST /chat` returns `askResponse((sink) => chat.ask(input, sink), { signal, stop: () => chat.stop() })`, the same server-sent events `assistantRoutes` sends. `view()` gives a stored chat as the panel's items, and `usage()` gives the chat's use so far. A card's id is its tool call's, so the panel's Save, Dismiss and Undo go to `saveCard`, `dismissCard` and `undoCard`. The app keeps the list of chats, and each chat is its own agent, such as a facet of the person's store. `chunkEvents` and `itemsOf` are the mapping, for an app that streams some other way.
256
256
 
257
- **Tool lines.** Each tool call shows as a line with the tool's own `title` (an MCP tool's `title`, a plain tool's `title`), kept in the chat's storage so a chat read back words its lines the same; a tool with no title shows its name. An app that keeps its insides off the screen overrides `plainSteps()` to return `true`: every line reads its tool's title, else a word for what it did (Look something up, Make a change, Draft a change), with no tool id, row count or error text (a failed line says `didn't work`), and a hand-off section reads Take a closer look with no tier or task. The details go to the app's log instead: `stepFailed({ tool, error, ms })` hears each failed call with its whole error (override it to log it with the chat's ids), and `onStep` carries `error` and `caller` on a failed tool or hand-off. `stepTitleOf(name, kind)` gives a line's title for an app's own card answers.
257
+ **Tool lines.** Each tool call shows as a line with the tool's own `title` (an MCP tool's `title`, a plain tool's `title`), kept in the chat's storage so a chat read back words its lines the same; a tool with no title shows its name. An app that keeps its insides off the screen overrides `plainSteps()` to return `true`: every line reads its tool's title, else a word for what it did (Look something up, Make a change, Draft a change), with no tool id, row count or error text (a failed line says `didn't work`), and a hand-off section reads Take a closer look with no tier or task. The details go to the app's log instead: `stepFailed({ tool, error, ms })` hears each failed call with its whole error (override it to log it with the chat's ids), and `onStep` carries `error` and `caller` on a failed tool or hand-off. `stepTitleOf(name, kind)` gives a line's title for an app's own card answers. **Step lines say what a call did** (`stepLine(c)`): override it to word each call as a verb and what it acted on, from the call's own arguments and answer (`c.input`, `c.output`): present tense while it runs (`state: 'running'`, sent again once the arguments are in), past tense with a short result when it answered (`'done'`), and "Couldn't …" when it failed (`'failed'`, which then shows no `didn't work`). Return `undefined` to keep the tool's title. The kit words its own web lookup (Looking up / Looked up the question). A finished line is kept with the chat, so a chat read back reads the same. In a plain chat the tool search that turns more tools on shows no line.
258
258
 
259
259
  **A write in progress.** A tool line starts (`step_start`) as soon as the model starts the call, while it is still writing the arguments. For a Read + write tool it carries `write: true`, and `useAssistantChat` marks that running step `writing` until its result lands or the answer ends, so an app can show what the tool changes as being changed now (a pending edit is not ready to accept while the model rewrites it).
260
260
 
@@ -17,6 +17,7 @@ import { type ModelMessage, type ToolSet } from 'ai';
17
17
  import type { ModelDef } from '../assistant/models.js';
18
18
  import { type AgentConnector, type ConnectorAt } from './connectors.js';
19
19
  import { type ModelKey } from './models.js';
20
+ import { type StepCall } from './stream.js';
20
21
  import { type AiToolsOptions, type OpenedTools, type SourceAt } from './tools.js';
21
22
  import { UsageMeter } from './usage.js';
22
23
  /** What a plan's extra tools get: the open tools, a meter of their own and the cap. */
@@ -197,6 +198,16 @@ export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
197
198
  * (`false`) unless the app overrides it; the app logs the details from `onStep`.
198
199
  */
199
200
  protected plainSteps(): boolean;
201
+ /**
202
+ * A tool call's line in the person's words: a verb and what it acted on, from the call's own
203
+ * arguments and answer. `running` is present tense ("Editing Lid"), `done` past tense with a short
204
+ * result when it helps ("Edited Lid: wall 2 → 3 mm"), `failed` says it did not ("Couldn't edit
205
+ * Lid"). Return `undefined` to keep the tool's title. A plain chat's line names no tool id, model,
206
+ * engine or code. The kit words its own web lookup; the app words its tools by overriding this
207
+ * (and may call `super.stepLine(c)` for the kit's). A finished line is kept with the chat, so a
208
+ * chat read back shows the same words.
209
+ */
210
+ protected stepLine(c: StepCall): string | undefined;
200
211
  /**
201
212
  * A tool call this answer failed: its name and whole error (a plain line shows neither). Does
202
213
  * nothing unless the app overrides it, e.g. to log it with the chat's ids.
@@ -201,12 +201,45 @@ export class LumenAgent extends Think {
201
201
  plainSteps() {
202
202
  return false;
203
203
  }
204
+ /**
205
+ * A tool call's line in the person's words: a verb and what it acted on, from the call's own
206
+ * arguments and answer. `running` is present tense ("Editing Lid"), `done` past tense with a short
207
+ * result when it helps ("Edited Lid: wall 2 → 3 mm"), `failed` says it did not ("Couldn't edit
208
+ * Lid"). Return `undefined` to keep the tool's title. A plain chat's line names no tool id, model,
209
+ * engine or code. The kit words its own web lookup; the app words its tools by overriding this
210
+ * (and may call `super.stepLine(c)` for the kit's). A finished line is kept with the chat, so a
211
+ * chat read back shows the same words.
212
+ */
213
+ stepLine(c) {
214
+ if (c.tool !== 'web__lookup')
215
+ return undefined;
216
+ const q = c.input?.question;
217
+ const question = typeof q === 'string' ? q.trim() : '';
218
+ if (!question)
219
+ return undefined;
220
+ return c.state === 'running'
221
+ ? `Looking up ${question}`
222
+ : c.state === 'failed'
223
+ ? `Couldn't look up ${question}`
224
+ : `Looked up ${question}`;
225
+ }
204
226
  /** How this chat words its tool lines: plain or not, with the titles of every tool it offered. */
205
227
  #names() {
206
228
  return {
207
229
  plain: this.plainSteps(),
208
230
  titleOf: (n) => this.#turn?.tools.info.find((i) => i.name === n)?.title ??
209
231
  this.#get('titles')?.[n],
232
+ lineOf: (c) => {
233
+ if (c.state === 'running')
234
+ return this.stepLine(c);
235
+ const kept = this.#get(`line:${c.id}`);
236
+ if (kept)
237
+ return kept;
238
+ const line = this.stepLine(c);
239
+ if (line)
240
+ this.#put(`line:${c.id}`, line.slice(0, 200));
241
+ return line;
242
+ },
210
243
  };
211
244
  }
212
245
  /**
@@ -17,7 +17,28 @@ export interface StepNames {
17
17
  plain?: boolean;
18
18
  /** The tool's own title, when it has one. */
19
19
  titleOf?: (name: string) => string | undefined;
20
+ /**
21
+ * A line that says what this call did and to what (`LumenAgent.stepLine`): present tense while it
22
+ * runs, past tense with a short result when it answered, "Couldn't …" when it failed. Left out
23
+ * (or `undefined`), the line reads the tool's title.
24
+ */
25
+ lineOf?: (c: StepCall) => string | undefined;
20
26
  }
27
+ /** One tool call as its line is worded: its name, arguments, answer and where it stands. */
28
+ export interface StepCall {
29
+ id: string;
30
+ /** The tool's name as the model called it (`group__tool`). */
31
+ tool: string;
32
+ input?: unknown;
33
+ /** Its answer, once it answered. */
34
+ output?: unknown;
35
+ state: 'running' | 'done' | 'failed';
36
+ }
37
+ /**
38
+ * Whether a call shows no line at all: in a plain chat, the tool search that turns more tools on is
39
+ * the assistant finding its own tools, not something it did for the person.
40
+ */
41
+ export declare function hiddenStep(name: string, names?: StepNames): boolean;
21
42
  /** What a plain line says when a call failed: the assistant reads the error and goes on. */
22
43
  export declare const PLAIN_FAILED = "didn't work";
23
44
  /** A plain hand-off's heading: a closer look, never the tier it went to. */
@@ -1,10 +1,32 @@
1
1
  import { rowCount } from '../assistant/tools.js';
2
2
  import { webStep } from '../assistant/web.js';
3
+ import { TOOL_SEARCH } from './search.js';
3
4
  /** A tool's name as a line's title: `notes__find_notes` → `find notes`. */
4
5
  export function stepTitle(name) {
5
6
  const own = name.includes('__') ? name.slice(name.indexOf('__') + 2) : name;
6
7
  return own.replace(/_/g, ' ');
7
8
  }
9
+ /** A line's text kept to one short line. */
10
+ const oneLine = (t) => {
11
+ const s = (t ?? '').replace(/\s+/g, ' ').trim();
12
+ return s ? s.slice(0, 120) : undefined;
13
+ };
14
+ /** The line the app gave for this call, when it gave one; never throws. */
15
+ function lineFor(names, c) {
16
+ try {
17
+ return oneLine(names.lineOf?.(c));
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ /**
24
+ * Whether a call shows no line at all: in a plain chat, the tool search that turns more tools on is
25
+ * the assistant finding its own tools, not something it did for the person.
26
+ */
27
+ export function hiddenStep(name, names = {}) {
28
+ return !!names.plain && name === TOOL_SEARCH;
29
+ }
8
30
  /** What a plain line says when a call failed: the assistant reads the error and goes on. */
9
31
  export const PLAIN_FAILED = "didn't work";
10
32
  /** A plain hand-off's heading: a closer look, never the tier it went to. */
@@ -88,15 +110,27 @@ function settledCall(id, c, out, ms, cards, screens = new Map(), names = {}) {
88
110
  const image = stepImage(o?.image);
89
111
  const web = webStep(c.name, c.input, out.output, !!error || obj(out.output)?.ok === false);
90
112
  const wrote = card?.kind === 'write' || (o && typeof o.change === 'string');
113
+ // The app's own line says what the call did; a failed one already says it did not ("Couldn't …").
114
+ const line = lineFor(names, {
115
+ id,
116
+ tool: c.name,
117
+ input: c.input,
118
+ ...(error
119
+ ? {}
120
+ : { output: o && 'result' in o && o.change !== undefined ? o.result : out.output }),
121
+ state: error ? 'failed' : 'done',
122
+ });
91
123
  const step = {
92
124
  kind: 'step',
93
125
  id,
94
126
  tool: c.name,
95
- title: shownTitle(c.name, names, wrote ? 'write' : 'read'),
127
+ title: line ?? shownTitle(c.name, names, wrote ? 'write' : 'read'),
96
128
  ok: !error,
97
129
  ms,
98
130
  ...(rows !== undefined && !error && !names.plain ? { rows } : {}),
99
- ...(error ? { error: names.plain ? PLAIN_FAILED : error.slice(0, 300) } : {}),
131
+ ...(error && !(line && names.plain)
132
+ ? { error: names.plain ? PLAIN_FAILED : error.slice(0, 300) }
133
+ : {}),
100
134
  ...(wrote
101
135
  ? { write: { undo: true, status: card?.status === 'undone' ? 'undone' : 'done' } }
102
136
  : {}),
@@ -129,7 +163,10 @@ export function chunkEvents(o = {}) {
129
163
  advising = true;
130
164
  return [{ event: 'advice', data: { tier: 'advisor', label: o.advisor ?? 'Advice' } }];
131
165
  }
132
- if (name === 'escalate' || name === 'screen' || o.kindOf?.(name) === 'draft')
166
+ if (name === 'escalate' ||
167
+ name === 'screen' ||
168
+ o.kindOf?.(name) === 'draft' ||
169
+ hiddenStep(name, names))
133
170
  return [];
134
171
  // A tool that makes a change at once says so as it starts, so an app can show that what it
135
172
  // changes is being changed now, before the model has finished writing the call.
@@ -150,7 +187,23 @@ export function chunkEvents(o = {}) {
150
187
  case 'tool-input-available': {
151
188
  const c = calls.get(id) ?? { name: String(chunk.toolName ?? ''), t0: Date.now() };
152
189
  calls.set(id, { ...c, input: chunk.input });
153
- return [];
190
+ // Its arguments are in: the running line can say what it is doing and to what.
191
+ if (c.name === 'advisor' ||
192
+ c.name === 'escalate' ||
193
+ c.name === 'screen' ||
194
+ o.kindOf?.(c.name) === 'draft' ||
195
+ hiddenStep(c.name, names))
196
+ return [];
197
+ const line = lineFor(names, { id, tool: c.name, input: chunk.input, state: 'running' });
198
+ if (!line)
199
+ return [];
200
+ const write = o.kindOf?.(c.name) === 'write';
201
+ return [
202
+ {
203
+ event: 'step_start',
204
+ data: { id, tool: c.name, title: line, ...(write ? { write } : {}) },
205
+ },
206
+ ];
154
207
  }
155
208
  case 'tool-output-available':
156
209
  case 'tool-output-error': {
@@ -164,6 +217,8 @@ export function chunkEvents(o = {}) {
164
217
  return [{ event: 'advice_done', data: error ? { error } : {} }];
165
218
  }
166
219
  const ms = Date.now() - c.t0;
220
+ if (hiddenStep(c.name, names))
221
+ return [];
167
222
  const out = obj(chunk.output);
168
223
  const said = error ?? (out && typeof out.error === 'string' ? out.error : undefined);
169
224
  if (said !== undefined) {
@@ -256,6 +311,8 @@ export function itemsOf(messages, cards, screens = new Map(), steers = {}, names
256
311
  items.push({ kind: 'advice', tier: 'advisor', label: 'Advice' });
257
312
  continue;
258
313
  }
314
+ if (hiddenStep(tool, names))
315
+ continue;
259
316
  const ended = p.state === 'output-available'
260
317
  ? { output: p.output }
261
318
  : p.state === 'output-error'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.253",
3
+ "version": "0.1.254",
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",
@@ -56,8 +56,8 @@
56
56
  "@ai-sdk/openai": "4.0.108",
57
57
  "@anthropic-ai/sdk": "0.133.0",
58
58
  "@cloudflare/think": "0.23.0",
59
- "@columenlabs/lumen-ops": "0.1.253",
60
- "@columenlabs/lumen-ui-types": "0.1.253",
59
+ "@columenlabs/lumen-ops": "0.1.254",
60
+ "@columenlabs/lumen-ui-types": "0.1.254",
61
61
  "agents": "0.32.0",
62
62
  "ai": "7.0.162",
63
63
  "just-bash": "3.6.0",