@columenlabs/lumen-ai 0.1.180 → 0.1.182

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
@@ -230,6 +230,10 @@ An app with an `Assistant` gets the whole plan from it: `assistant.agentTurn(cal
230
230
 
231
231
  Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agent's SQLite). A write the person's access only drafts makes an open card and changes nothing. `saveCard(id)` runs it through the app's `applyDraft`, and `dismissCard(id)` drops it. A write made at once is kept for `undoCard(id)` through the app's `undoWrite`. `cards()` lists the cards. The next answer is told how each card ended and what was undone. With an `Assistant`, `applyDraft` is `assistant.runDraft(caller, card)` and `undoWrite` is `assistant.undoChange(caller, card)`. Think compacts the chat past `COMPACT_AT` (150k tokens) on the last answer's model and key, and that spend is added to the next answer.
232
232
 
233
+ **Connectors.** A person adds an MCP server to their chat with `addConnector(name, url)`. The address must be https. The server signs in with its own OAuth through Think's MCP client, and the answer carries `authUrl` while it asks. `connectors()` lists them and `removeConnector(id)` drops one. Think never merges their tools on its own (`includeMcpTools` is false). A turn opens the ones `plan.connectors` lists, at the access the app gives each, as group `mcp_<id>`. Read keeps the tools the server marks read-only. Read + draft makes the rest cards, which `saveCard` runs on the server itself. Read + write runs them at once with no Undo. A connector's text is untrusted data.
234
+
235
+ **Lazy tools.** Past `LAZY_AT` (30) tools, a turn sends the model its core tools and `tool_search`, with one catalog line per group in the instructions. By default the core tools are the apps' finds, the hand off and the advisor. `plan.lazy` sets `{ at, core }`. A search matches the words in a tool's name and description and turns on up to 8 tools from the next step on.
236
+
233
237
  ```ts
234
238
  class Chat extends LumenAgent<Env> {
235
239
  #settle?: (end: AgentTurnEnd) => Promise<void>;
@@ -15,6 +15,7 @@ import { type ChatResponseResult, type PrepareStepContext, type Session, type St
15
15
  import type { AssistantEffort, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
16
16
  import { type ModelMessage, type ToolSet } from 'ai';
17
17
  import type { ModelDef } from '../assistant/models.js';
18
+ import { type AgentConnector, type ConnectorAt } from './connectors.js';
18
19
  import { type ModelKey } from './models.js';
19
20
  import { type AiToolsOptions, type OpenedTools, type SourceAt } from './tools.js';
20
21
  import { UsageMeter } from './usage.js';
@@ -46,6 +47,13 @@ export interface AgentTurnPlan {
46
47
  more?: (t: TurnTools) => ToolSet;
47
48
  /** Call this tool first (a message handed to a higher tier). */
48
49
  first?: string;
50
+ /** The person's connectors to open, at the access they gave each. */
51
+ connectors?: ConnectorAt[];
52
+ /** Past `at` tools (`LAZY_AT`) the model starts with `core` and `tool_search` only. */
53
+ lazy?: {
54
+ at?: number;
55
+ core?: string[];
56
+ };
49
57
  /** Claude's advisor: a bigger Claude reads the chat and sends a plan. */
50
58
  advisor?: {
51
59
  model: ModelDef;
@@ -104,6 +112,17 @@ export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
104
112
  protected applyDraft(_card: AgentCard): Promise<unknown>;
105
113
  /** Takes back a write made at once (Undo). */
106
114
  protected undoWrite(_card: AgentCard): Promise<void>;
115
+ /** Connectors' tools join a turn through `plan.connectors`, at the person's access. */
116
+ includeMcpTools: boolean;
117
+ /** The person's connectors, with their state and tool counts. */
118
+ connectors(): AgentConnector[];
119
+ /** Adds a connector by its https address; the answer says where to sign in when it asks. */
120
+ addConnector(name: string, url: string, o?: {
121
+ callbackHost?: string;
122
+ callbackPath?: string;
123
+ }): Promise<AgentConnector>;
124
+ /** Removes a connector and its sign-in. */
125
+ removeConnector(id: string): Promise<void>;
107
126
  /** The chat's cards, newest last: drafts and writes made at once (at most 200). */
108
127
  cards(): AgentCard[];
109
128
  /** The person saved a draft: its write runs now. */
@@ -15,7 +15,9 @@ import { anthropic } from '@ai-sdk/anthropic';
15
15
  import { Think, } from '@cloudflare/think';
16
16
  import { createCompactFunction } from 'agents/sessions';
17
17
  import { generateText } from 'ai';
18
+ import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
18
19
  import { aiModel, turnSettings } from './models.js';
20
+ import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
19
21
  import { openAiTools, } from './tools.js';
20
22
  import { UsageMeter } from './usage.js';
21
23
  export const CAP_NOTE = 'The spend cap for this month is reached: answer now from what you have, in a sentence or two. You can call no more tools.';
@@ -71,6 +73,8 @@ export class LumenAgent extends Think {
71
73
  undoWrite(_card) {
72
74
  return Promise.reject(new Error('This change cannot be undone here.'));
73
75
  }
76
+ /** Connectors' tools join a turn through `plan.connectors`, at the person's access. */
77
+ includeMcpTools = false;
74
78
  #turn = null;
75
79
  /** The last turn's model and key, for compaction between turns. */
76
80
  #last = null;
@@ -100,6 +104,43 @@ export class LumenAgent extends Think {
100
104
  this
101
105
  .sql `UPDATE lumen_cards SET status = ${status}, result = COALESCE(${o.result === undefined ? null : JSON.stringify(o.result).slice(0, 20_000)}, result), error = ${o.error ?? null}, told = 0 WHERE id = ${id}`;
102
106
  }
107
+ #connectorClient() {
108
+ return {
109
+ listTools: () => this.getMcpServers().tools,
110
+ callTool: (p) => this.mcp.callTool(p),
111
+ };
112
+ }
113
+ /** The person's connectors, with their state and tool counts. */
114
+ connectors() {
115
+ const { servers, tools } = this.getMcpServers();
116
+ return Object.entries(servers).map(([id, s]) => {
117
+ const own = tools.filter((t) => t.serverId === id);
118
+ return {
119
+ id,
120
+ name: s.name,
121
+ url: s.server_url,
122
+ state: s.state,
123
+ ...(s.auth_url ? { authUrl: s.auth_url } : {}),
124
+ ...(s.error ? { error: s.error.slice(0, 300) } : {}),
125
+ tools: own.length,
126
+ reads: own.filter((t) => t.annotations?.readOnlyHint === true).length,
127
+ };
128
+ });
129
+ }
130
+ /** Adds a connector by its https address; the answer says where to sign in when it asks. */
131
+ async addConnector(name, url, o = {}) {
132
+ const u = checkConnectorUrl(url);
133
+ const id = connectorId(name);
134
+ await this.addMcpServer(name.slice(0, 60), u.href, { id, ...o });
135
+ const c = this.connectors().find((x) => x.id === id);
136
+ if (!c)
137
+ throw new Error('The connector did not stay.');
138
+ return c;
139
+ }
140
+ /** Removes a connector and its sign-in. */
141
+ async removeConnector(id) {
142
+ await this.removeMcpServer(id);
143
+ }
103
144
  /** The chat's cards, newest last: drafts and writes made at once (at most 200). */
104
145
  cards() {
105
146
  this.#cards();
@@ -115,7 +156,13 @@ export class LumenAgent extends Think {
115
156
  if (c.status !== 'open')
116
157
  throw new Error(`That card is already ${c.status}.`);
117
158
  try {
118
- this.#setCard(id, 'saved', { result: await this.applyDraft(c) });
159
+ const server = c.group.startsWith(CONNECTOR_GROUP)
160
+ ? c.group.slice(CONNECTOR_GROUP.length)
161
+ : null;
162
+ const result = server
163
+ ? await (await connectorSource(this.#connectorClient(), server)()).call(c.tool, c.args)
164
+ : await this.applyDraft(c);
165
+ this.#setCard(id, 'saved', { result });
119
166
  }
120
167
  catch (err) {
121
168
  this.#setCard(id, 'failed', { error: String(err?.message ?? err).slice(0, 300) });
@@ -186,9 +233,23 @@ export class LumenAgent extends Think {
186
233
  await this.#close();
187
234
  const plan = await this.plan(ctx);
188
235
  this.#last = { plan };
189
- const opened = await openAiTools(plan.sources, {
236
+ const linked = (plan.connectors ?? []).filter((c) => c.access !== 'off');
237
+ if (linked.length)
238
+ await this.mcp.waitForConnections({ timeout: 1500 }).catch(() => undefined);
239
+ const client = this.#connectorClient();
240
+ const sources = [
241
+ ...plan.sources,
242
+ ...linked.map((c) => ({
243
+ group: connectorGroup(c.id),
244
+ access: c.access,
245
+ source: connectorSource(client, c.id),
246
+ writes: true,
247
+ })),
248
+ ];
249
+ const opened = await openAiTools(sources, {
190
250
  draft: async (d) => ({ card: this.#addCard('draft', d), note: CARD_NOTE }),
191
- wrote: async (w) => this.#addCard('write', w, w.result),
251
+ // A connector's change cannot be taken back, so it gets no Undo card.
252
+ wrote: async (w) => w.group.startsWith(CONNECTOR_GROUP) ? undefined : this.#addCard('write', w, w.result),
192
253
  ...plan.tools,
193
254
  });
194
255
  const free = !!plan.key.plan;
@@ -200,7 +261,7 @@ export class LumenAgent extends Think {
200
261
  : undefined;
201
262
  if (advice)
202
263
  meters.push(advice);
203
- const system = typeof plan.system === 'string' ? plan.system : plan.system(opened);
264
+ let system = typeof plan.system === 'string' ? plan.system : plan.system(opened);
204
265
  this.#turn = {
205
266
  plan,
206
267
  system,
@@ -228,6 +289,23 @@ export class LumenAgent extends Think {
228
289
  maxUses: plan.advisor.maxUses,
229
290
  maxTokens: plan.advisor.maxTokens,
230
291
  });
292
+ const tools = { ...opened.tools, ...more, ...advisor };
293
+ let lazy;
294
+ if (Object.keys(tools).length > (plan.lazy?.at ?? LAZY_AT)) {
295
+ const groups = new Map(opened.info.map((i) => [i.name, i.group]));
296
+ const core = plan.lazy?.core ?? [
297
+ ...opened.info.filter((i) => i.find).map((i) => i.name),
298
+ ...Object.keys(more ?? {}),
299
+ ...Object.keys(advisor),
300
+ ];
301
+ if (plan.first && !core.includes(plan.first))
302
+ core.push(plan.first);
303
+ lazy = lazyTools(tools, core, (n) => groups.get(n) ?? 'other');
304
+ tools[TOOL_SEARCH] = lazy.search;
305
+ system = `${system}\n\n${lazy.catalog}`;
306
+ this.#turn.system = system;
307
+ this.#turn.lazy = lazy;
308
+ }
231
309
  return {
232
310
  model: aiModel(plan.model, plan.key, {
233
311
  ...(plan.fetch ? { fetch: plan.fetch } : {}),
@@ -235,7 +313,8 @@ export class LumenAgent extends Think {
235
313
  }),
236
314
  instructions: system,
237
315
  messages: withLead(ctx.messages, `${this.#news()}${plan.lead ?? ''}`),
238
- tools: { ...opened.tools, ...more, ...advisor },
316
+ tools,
317
+ ...(lazy ? { activeTools: lazy.active() } : {}),
239
318
  ...(plan.first ? { toolChoice: { type: 'tool', toolName: plan.first } } : {}),
240
319
  maxSteps: plan.maxSteps ?? 8,
241
320
  ...turnSettings(plan.model, {
@@ -254,10 +333,14 @@ export class LumenAgent extends Think {
254
333
  t.capped = true;
255
334
  return { activeTools: [], instructions: `${t.system}\n\n${CAP_NOTE}` };
256
335
  }
257
- // A forced first call is for the first step only.
258
- if (t.plan.first && ctx.stepNumber > 0)
259
- return { toolChoice: 'auto' };
260
- return undefined;
336
+ // A forced first call is for the first step only; tools a search found join each step.
337
+ const auto = t.plan.first && ctx.stepNumber > 0;
338
+ if (!auto && !t.lazy)
339
+ return undefined;
340
+ return {
341
+ ...(auto ? { toolChoice: 'auto' } : {}),
342
+ ...(t.lazy ? { activeTools: t.lazy.active() } : {}),
343
+ };
261
344
  }
262
345
  onStepFinish(ctx) {
263
346
  const t = this.#turn;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Connectors: MCP servers a person adds to their chat (a URL, signed in with the server's own
3
+ * OAuth through Think's MCP client). Each is a tool group like the app's own, at the access the
4
+ * app gives it: Read keeps the tools the server marks read-only, Read + draft turns the rest into
5
+ * cards, Read + write runs them at once. A server's text is untrusted data, never instructions.
6
+ */
7
+ import type { AssistantAccess } from '@columenlabs/lumen-ui-types/assistant';
8
+ import { type ToolSource } from '../assistant/tools.js';
9
+ /** A connector's tool group: `mcp_` and its id. */
10
+ export declare const CONNECTOR_GROUP = "mcp_";
11
+ export declare const connectorGroup: (id: string) => string;
12
+ /** One connector the app opens for a turn, at the access the person gave it. */
13
+ export interface ConnectorAt {
14
+ id: string;
15
+ access: AssistantAccess;
16
+ }
17
+ /** A connector as the app's settings list it. */
18
+ export interface AgentConnector {
19
+ id: string;
20
+ name: string;
21
+ url: string;
22
+ state: string;
23
+ /** Where the person signs in, while the server asks for it. */
24
+ authUrl?: string;
25
+ error?: string;
26
+ /** Its tools, and how many are marked read-only. */
27
+ tools: number;
28
+ reads: number;
29
+ }
30
+ interface McpTool {
31
+ name: string;
32
+ serverId: string;
33
+ title?: string;
34
+ description?: string;
35
+ inputSchema: unknown;
36
+ annotations?: {
37
+ readOnlyHint?: boolean;
38
+ title?: string;
39
+ };
40
+ }
41
+ /** The part of Think's MCP client a connector needs. */
42
+ export interface ConnectorClient {
43
+ listTools(): McpTool[];
44
+ callTool(p: {
45
+ serverId: string;
46
+ name: string;
47
+ arguments: Record<string, unknown>;
48
+ }): Promise<{
49
+ content?: unknown;
50
+ isError?: boolean;
51
+ }>;
52
+ }
53
+ /** What a connector's call answered: its text as JSON when it parses, else the text. */
54
+ export declare function connectorResult(r: {
55
+ content?: unknown;
56
+ isError?: boolean;
57
+ }, name: string): unknown;
58
+ /** One connector's tools as a source: listed from the client's catalog, called through it. */
59
+ export declare function connectorSource(mcp: ConnectorClient, serverId: string): ToolSource;
60
+ /** A connector's id from its name: a letter first, then letters, digits and `_`, at most 24. */
61
+ export declare function connectorId(name: string): string;
62
+ /** A connector's address must be https (localhost only while developing). */
63
+ export declare function checkConnectorUrl(url: string): URL;
64
+ export {};
@@ -0,0 +1,71 @@
1
+ import { ToolError, withDeadline } from '../assistant/tools.js';
2
+ /** A connector's tool group: `mcp_` and its id. */
3
+ export const CONNECTOR_GROUP = 'mcp_';
4
+ export const connectorGroup = (id) => `${CONNECTOR_GROUP}${id}`;
5
+ /** What a connector's call answered: its text as JSON when it parses, else the text. */
6
+ export function connectorResult(r, name) {
7
+ const parts = r.content ?? [];
8
+ const text = parts
9
+ .filter((c) => c.type === 'text' && c.text)
10
+ .map((c) => c.text)
11
+ .join('\n');
12
+ if (r.isError)
13
+ throw new ToolError(text.slice(0, 500) || `${name} failed`);
14
+ try {
15
+ return JSON.parse(text);
16
+ }
17
+ catch {
18
+ return text;
19
+ }
20
+ }
21
+ /** One connector's tools as a source: listed from the client's catalog, called through it. */
22
+ export function connectorSource(mcp, serverId) {
23
+ return async () => ({
24
+ tools: mcp
25
+ .listTools()
26
+ .filter((t) => t.serverId === serverId)
27
+ .map((t) => {
28
+ const title = t.title ?? t.annotations?.title;
29
+ return {
30
+ name: t.name,
31
+ ...(title ? { title } : {}),
32
+ ...(t.description ? { description: t.description } : {}),
33
+ inputSchema: (t.inputSchema ?? { type: 'object' }),
34
+ read: t.annotations?.readOnlyHint === true,
35
+ };
36
+ }),
37
+ call: async (name, args) => connectorResult(await withDeadline(`${name}`, 60_000, mcp.callTool({
38
+ serverId,
39
+ name,
40
+ arguments: (args ?? {}),
41
+ })), name),
42
+ close: async () => undefined,
43
+ });
44
+ }
45
+ /** A connector's id from its name: a letter first, then letters, digits and `_`, at most 24. */
46
+ export function connectorId(name) {
47
+ const id = name
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, '_')
50
+ .replace(/^_+|_+$/g, '')
51
+ .slice(0, 24);
52
+ if (!id)
53
+ throw new Error('Give the connector a name.');
54
+ return /^[a-z]/.test(id) ? id : `c_${id}`.slice(0, 24);
55
+ }
56
+ /** A connector's address must be https (localhost only while developing). */
57
+ export function checkConnectorUrl(url) {
58
+ let u;
59
+ try {
60
+ u = new URL(url);
61
+ }
62
+ catch {
63
+ throw new Error('That is not a web address.');
64
+ }
65
+ const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1';
66
+ if (u.protocol !== 'https:' && !(local && u.protocol === 'http:'))
67
+ throw new Error('A connector needs an https address.');
68
+ if (u.username || u.password)
69
+ throw new Error('Leave sign-in details out of the address.');
70
+ return u;
71
+ }
@@ -4,6 +4,8 @@
4
4
  * payer rules; Think keeps the loop, the messages, streaming, resume and compaction.
5
5
  */
6
6
  export * from './agent.js';
7
+ export * from './connectors.js';
7
8
  export * from './models.js';
9
+ export * from './search.js';
8
10
  export * from './tools.js';
9
11
  export * from './usage.js';
@@ -4,6 +4,8 @@
4
4
  * payer rules; Think keeps the loop, the messages, streaming, resume and compaction.
5
5
  */
6
6
  export * from './agent.js';
7
+ export * from './connectors.js';
7
8
  export * from './models.js';
9
+ export * from './search.js';
8
10
  export * from './tools.js';
9
11
  export * from './usage.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lazy tools: past `LAZY_AT` tools a turn sends the model only its core tools and `tool_search`,
3
+ * with one catalog line per group in the instructions. A search finds tools by their name and
4
+ * description and turns them on from the next step, so a big set (many connectors) costs a short
5
+ * prompt instead of every schema on every call.
6
+ */
7
+ import { type Tool, type ToolSet } from 'ai';
8
+ /** Past this many tools a turn starts with `tool_search` and its core tools only. */
9
+ export declare const LAZY_AT = 30;
10
+ export declare const TOOL_SEARCH = "tool_search";
11
+ export interface LazyTools {
12
+ /** The search tool, to add to the turn's tools. */
13
+ search: Tool;
14
+ /** One line per group, for the instructions. */
15
+ catalog: string;
16
+ /** The names to keep active now: the core tools, the search and what it found. */
17
+ active(): string[];
18
+ }
19
+ /** How well a tool's name and description match the query's words. */
20
+ export declare function toolScore(query: string, name: string, description?: string): number;
21
+ /**
22
+ * A search over `tools` whose finds stay on for the rest of the turn. `core` names are always on;
23
+ * `groupOf` names each tool's group for the catalog.
24
+ */
25
+ export declare function lazyTools(tools: ToolSet, core: string[], groupOf: (name: string) => string): LazyTools;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Lazy tools: past `LAZY_AT` tools a turn sends the model only its core tools and `tool_search`,
3
+ * with one catalog line per group in the instructions. A search finds tools by their name and
4
+ * description and turns them on from the next step, so a big set (many connectors) costs a short
5
+ * prompt instead of every schema on every call.
6
+ */
7
+ import { jsonSchema, tool } from 'ai';
8
+ /** Past this many tools a turn starts with `tool_search` and its core tools only. */
9
+ export const LAZY_AT = 30;
10
+ export const TOOL_SEARCH = 'tool_search';
11
+ /** Most tools one search turns on. */
12
+ const FOUND_MAX = 8;
13
+ const words = (s) => s
14
+ .toLowerCase()
15
+ .split(/[^a-z0-9]+/)
16
+ .filter((w) => w.length > 1);
17
+ /** How well a tool's name and description match the query's words. */
18
+ export function toolScore(query, name, description = '') {
19
+ const q = words(query);
20
+ if (!q.length)
21
+ return 0;
22
+ const n = new Set(words(name));
23
+ const d = new Set(words(description));
24
+ let score = 0;
25
+ for (const w of q) {
26
+ if (n.has(w))
27
+ score += 3;
28
+ else if ([...n].some((x) => x.startsWith(w) || w.startsWith(x)))
29
+ score += 2;
30
+ if (d.has(w))
31
+ score += 1;
32
+ }
33
+ return score;
34
+ }
35
+ /**
36
+ * A search over `tools` whose finds stay on for the rest of the turn. `core` names are always on;
37
+ * `groupOf` names each tool's group for the catalog.
38
+ */
39
+ export function lazyTools(tools, core, groupOf) {
40
+ const found = new Set();
41
+ const about = (n) => {
42
+ const d = tools[n]?.description;
43
+ return typeof d === 'string' ? d : '';
44
+ };
45
+ const names = Object.keys(tools);
46
+ const groups = new Map();
47
+ for (const n of names) {
48
+ if (core.includes(n))
49
+ continue;
50
+ const g = groupOf(n);
51
+ groups.set(g, (groups.get(g) ?? 0) + 1);
52
+ }
53
+ const catalog = `More tools are off until you find them: call ${TOOL_SEARCH} with a few words of what you need, then call what it finds. Groups: ${[
54
+ ...groups,
55
+ ]
56
+ .map(([g, n]) => `${g} (${n})`)
57
+ .join(', ')}.`;
58
+ const search = tool({
59
+ description: 'Finds tools by what they do and turns them on for your next call. Give a few words (e.g. "create calendar event"), and a group to look in only it.',
60
+ inputSchema: jsonSchema({
61
+ type: 'object',
62
+ properties: {
63
+ query: { type: 'string', description: 'A few words of what the tool does.' },
64
+ group: { type: 'string', description: 'Look in this group only.' },
65
+ },
66
+ required: ['query'],
67
+ }),
68
+ execute: async ({ query, group }) => {
69
+ const hits = names
70
+ .filter((n) => !core.includes(n) && (!group || groupOf(n) === group))
71
+ .map((n) => ({ n, s: toolScore(query, n, about(n)) }))
72
+ .filter((h) => h.s > 0)
73
+ .sort((a, b) => b.s - a.s)
74
+ .slice(0, FOUND_MAX);
75
+ for (const h of hits)
76
+ found.add(h.n);
77
+ if (!hits.length)
78
+ return { found: [], note: 'Nothing matched. Try other words or a group.' };
79
+ return {
80
+ found: hits.map((h) => ({
81
+ name: h.n,
82
+ description: about(h.n).slice(0, 300),
83
+ })),
84
+ note: 'These are on from your next call.',
85
+ };
86
+ },
87
+ });
88
+ return {
89
+ search,
90
+ catalog,
91
+ active: () => [...core, TOOL_SEARCH, ...found],
92
+ };
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.180",
3
+ "version": "0.1.182",
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",
@@ -48,8 +48,8 @@
48
48
  "@ai-sdk/openai": "4.0.108",
49
49
  "@anthropic-ai/sdk": "0.133.0",
50
50
  "@cloudflare/think": "0.23.0",
51
- "@columenlabs/lumen-ops": "0.1.180",
52
- "@columenlabs/lumen-ui-types": "0.1.180",
51
+ "@columenlabs/lumen-ops": "0.1.182",
52
+ "@columenlabs/lumen-ui-types": "0.1.182",
53
53
  "agents": "0.32.0",
54
54
  "ai": "7.0.162",
55
55
  "just-bash": "3.6.0",