@columenlabs/lumen-ai 0.1.250 → 0.1.252

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
@@ -111,6 +111,7 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
111
111
  - **Who pays.** The person's pick (the shared keys by default) while it has a key for the tier's provider and money left this month under its cap (an admin sets the shared keys' cap per person, `PUT /app`), else the other; with neither the panel says why.
112
112
  - **Tools.** Each group is Off, Read, Read + draft or Read + write per person. A tool is a read when its MCP `readOnlyHint` is set. On Read + draft a write only drafts a card, and runs as the person when they press Save, after access is checked again. A group with `readOnly: true` (read tools only) is offered Off or Read alone. Read + write is offered only for a group with `writes: true`: its writes run at once, and with `undo` set each one's line in the chat has Undo, which calls `undo(caller, { tool, args, result })`; the model is told what was undone. Cards drafted one after another show as one stack with Save all. Use `mcpServerTools` (an SDK `McpServer`), `mcpFetchTools` (a server served as a fetch handler, any SDK), `remoteMcpTools` or `plainTools`.
113
113
  - **Web.** `fetch` is a plain GET from the Worker (10 s, 2 MB, never the app's own hosts or private addresses). ScrapeQL's own tools join over its MCP API once an admin pastes a Cloudflare Access service token (`PUT /web/scrapeql`). A Worker on ScrapeQL's own Cloudflare account passes its service binding instead, `scrapeql: { url: 'https://scrapeql/mcp', fetch: env.SCRAPEQL.fetch.bind(env.SCRAPEQL) }`: no token is stored, the tools are always on and Settings shows the link with a Test. Each web step carries `web` (`StepWeb`: a search's `query` and `results`, or the page it `read`), so the panel folds a search's results under its line and numbers each page read under the answer as Sources chips; the model marks facts with those numbers ([1]).
114
+ - **Web lookup (a subagent).** `web: { scrapeql, lookup: true }` swaps fetch and ScrapeQL's own tools for one tool, `web__lookup` (`webLookupTools` from `@columenlabs/lumen-ai/subagents`): given a question (and `urls` when the pages are known), it searches with ScrapeQL, has ScrapeQL's `ask` read the best pages (3 by default, `pages` up to 5) and answer in a few sentences each (`answerTokens`, default 300), and returns `{question, sources: [{url, title, answer}], failed}`: about a thousand tokens, never whole pages, so the assistant's context stays small. No model runs in the kit for it; ScrapeQL picks its own cheapest model. Its step carries `web.lookup` and `web.reads`, which the panel folds under a Looked up line and numbers as Sources chips. `fetch.blockHosts` still applies.
114
115
  - **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. A hand-off gives up after `HAND_OFF_MS` (4 minutes) and the turn goes on without it.
115
116
  - **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.
116
117
  - **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.
@@ -1,5 +1,6 @@
1
1
  import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantEffort, AssistantMessage, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantStatus, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
2
2
  import type { AgentTurnEnd, AgentTurnPlan } from '../agent/agent.js';
3
+ import { type WebLookupOptions } from '../subagents/web.js';
3
4
  import { type BearingsExtra, type BearingsNow } from './bearings.js';
4
5
  import { type ModelDef, type TaskDef, type TierDef } from './models.js';
5
6
  import { type OwnWays, State } from './state.js';
@@ -174,6 +175,12 @@ export interface AssistantOptions {
174
175
  url: string;
175
176
  fetch?: typeof fetch;
176
177
  };
178
+ /**
179
+ * One `web__lookup` tool in place of fetch and ScrapeQL's own tools: it searches, has ScrapeQL
180
+ * read the best pages and answer the question, and returns a short answer per page, so whole
181
+ * pages never reach the model. Needs `scrapeql`; without it the web is off.
182
+ */
183
+ lookup?: boolean | WebLookupOptions;
177
184
  };
178
185
  store: AssistantStore;
179
186
  sealer?: Sealer;
@@ -12,10 +12,11 @@ import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
12
12
  import { generateText, jsonSchema, stepCountIs, tool } from 'ai';
13
13
  import OpenAI, { toFile } from 'openai';
14
14
  import { aiModel, turnSettings } from '../agent/models.js';
15
+ import { webLookupTools } from '../subagents/web.js';
15
16
  import { bearings } from './bearings.js';
16
17
  import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
17
18
  import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, XAI_BASE_URL, effortParams, } from './models.js';
18
- import { DEFAULT_CAP, State, keyDocName, keyState, ownKeyUsd, planDocName, waitText, withProvider, } from './state.js';
19
+ import { DEFAULT_CAP, State, keyDocName, keyState, ownKeyUsd, ownPlanCalls, planDocName, waitText, withProvider, } from './state.js';
19
20
  import { Docs } from './store.js';
20
21
  import { toolName, withDeadline } from './tools.js';
21
22
  import { scrapeqlTools, webFetchTools } from './web.js';
@@ -408,6 +409,7 @@ export class Assistant {
408
409
  kind: 'chat',
409
410
  title: message.split('\n')[0] || 'Chat',
410
411
  own_usd: ownKeyUsd(paying, onPayer),
412
+ own_plan_calls: ownPlanCalls(paying, onPayer),
411
413
  }, end.parts)
412
414
  .catch(() => undefined);
413
415
  const total = end.parts.reduce((a, m) => ({
@@ -824,7 +826,22 @@ export class Assistant {
824
826
  for (const source of Array.isArray(s) ? s : [s])
825
827
  sources.push({ group: g.id, access, source, ...(g.writes ? { writes: true } : {}) });
826
828
  }
827
- if (p.web ?? true) {
829
+ const lookup = this.o.web?.lookup;
830
+ const blockHosts = this.o.web?.fetch ? this.o.web.fetch.blockHosts : undefined;
831
+ if ((p.web ?? true) && lookup) {
832
+ const sq = this.o.web?.scrapeql;
833
+ const token = sq && !sq.fetch ? await this.state.scrapeql() : null;
834
+ if (sq && (sq.fetch || token))
835
+ sources.push({
836
+ group: 'web',
837
+ access: 'read',
838
+ source: webLookupTools(scrapeqlTools(sq.url, token, sq.fetch ?? this.o.fetch), {
839
+ ...(blockHosts ? { blockHosts } : {}),
840
+ ...(typeof lookup === 'object' ? lookup : {}),
841
+ }),
842
+ });
843
+ }
844
+ else if (p.web ?? true) {
828
845
  if (this.o.web?.fetch !== false)
829
846
  sources.push({
830
847
  group: 'web',
@@ -869,7 +886,9 @@ export class Assistant {
869
886
  lines.push('No tools are on now: the person can turn them on in Settings ▸ Tools.');
870
887
  if (failed)
871
888
  lines.push(`${failed} tool source${failed === 1 ? '' : 's'} could not be reached this turn.`);
872
- if (fns.web)
889
+ if (fns.web && this.o.web?.lookup)
890
+ lines.push("Web: web__lookup answers one question from the web: give the full question (and urls when you already know the pages); it returns a short answer from each page it read. Look up again with a sharper question rather than asking for whole pages. The person sees each page a lookup read numbered under your answer, in the order returned (the first lookup's sources first, a later lookup's after them): mark a fact with its number, like [1], and never cite a page that is not among the sources.");
891
+ else if (fns.web)
873
892
  lines.push('Web: use web__fetch first for APIs and plain pages; the browser tools only for pages that need JavaScript or after a fetch fails. To find pages, search, then read the one to three that answer the question. The person sees each page you read numbered under your answer, in the order you read them (1 for the first page read in this answer): mark a fact you took from a page with its number, like [1], and never cite a page you did not read.');
874
893
  if (main) {
875
894
  const reach = this.above(main.tier);
@@ -1076,6 +1095,7 @@ export class Assistant {
1076
1095
  task: o.task,
1077
1096
  title: o.run?.title ?? task?.label ?? o.task,
1078
1097
  own_usd: ownKeyUsd(paying, [part]),
1098
+ own_plan_calls: ownPlanCalls(paying, [part]),
1079
1099
  }, [part])
1080
1100
  .catch(() => undefined);
1081
1101
  }
@@ -1164,6 +1184,7 @@ export class Assistant {
1164
1184
  task: o.task,
1165
1185
  title: o.run?.title ?? task?.label ?? o.task,
1166
1186
  own_usd: ownKeyUsd(paying, [part]),
1187
+ own_plan_calls: ownPlanCalls(paying, [part]),
1167
1188
  }, [part])
1168
1189
  .catch(() => undefined);
1169
1190
  }
@@ -111,6 +111,11 @@ export declare function ownKeyUsd(paying: {
111
111
  payer: AssistantPayer;
112
112
  plans: Paying['plans'];
113
113
  }, parts: UsagePart[]): number;
114
+ /** Calls in `parts` a plan sign-in of the person's answered (their own payer, a plan provider). */
115
+ export declare function ownPlanCalls(paying: {
116
+ payer: AssistantPayer;
117
+ plans: Paying['plans'];
118
+ }, parts: UsagePart[]): number;
114
119
  /**
115
120
  * What counts toward the person's own cap: own rows paid by an API key (a plan sign-in costs
116
121
  * nothing per call), on a provider they can still pay with. A row from before ways were recorded
@@ -276,7 +281,8 @@ export declare class State {
276
281
  /**
277
282
  * Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
278
283
  * chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only;
279
- * `own_usd` is what of this answer the person's own key or plan paid, added to the entry's.
284
+ * `own_usd` is what of this answer the person's own key or plan paid, added to the entry's;
285
+ * `own_plan_calls` the calls their plan sign-in answered (free to them, so counted, not priced).
280
286
  */
281
287
  addUsage(id: string, e: {
282
288
  id: string;
@@ -284,6 +290,7 @@ export declare class State {
284
290
  task?: string;
285
291
  title: string;
286
292
  own_usd?: number;
293
+ own_plan_calls?: number;
287
294
  }, parts: UsagePart[]): Promise<void>;
288
295
  /** One month's history (this month by default) beside its spend, and the months that have one. */
289
296
  usage(id: string, month?: string): Promise<UsageHistory>;
@@ -32,6 +32,17 @@ export function ownKeyUsd(paying, parts) {
32
32
  })
33
33
  .reduce((a, p) => a + p.usd, 0);
34
34
  }
35
+ /** Calls in `parts` a plan sign-in of the person's answered (their own payer, a plan provider). */
36
+ export function ownPlanCalls(paying, parts) {
37
+ if (paying.payer !== 'own')
38
+ return 0;
39
+ return parts
40
+ .filter((p) => {
41
+ const prov = providerOfPart(p);
42
+ return !!(prov && paying.plans[prov]);
43
+ })
44
+ .reduce((a, p) => a + p.calls, 0);
45
+ }
35
46
  /**
36
47
  * What counts toward the person's own cap: own rows paid by an API key (a plan sign-in costs
37
48
  * nothing per call), on a provider they can still pay with. A row from before ways were recorded
@@ -352,7 +363,8 @@ export class State {
352
363
  /**
353
364
  * Adds one answer's use to a chat or run in this month's history (`usage:YYYY-MM`): one entry a
354
365
  * chat or run, its parts merged, the newest `USAGE_KEPT` kept. `title` names a new entry only;
355
- * `own_usd` is what of this answer the person's own key or plan paid, added to the entry's.
366
+ * `own_usd` is what of this answer the person's own key or plan paid, added to the entry's;
367
+ * `own_plan_calls` the calls their plan sign-in answered (free to them, so counted, not priced).
356
368
  */
357
369
  async addUsage(id, e, parts) {
358
370
  if (!parts.some((p) => p.calls))
@@ -365,6 +377,7 @@ export class State {
365
377
  const merged = mergeParts(prev?.parts ?? [], parts);
366
378
  const sum = (k) => merged.reduce((a, p) => a + p[k], 0);
367
379
  const own = (prev?.own_usd ?? 0) + (e.own_usd ?? 0);
380
+ const plan = (prev?.own_plan_calls ?? 0) + (e.own_plan_calls ?? 0);
368
381
  entries.unshift({
369
382
  id: e.id,
370
383
  kind: e.kind,
@@ -378,6 +391,7 @@ export class State {
378
391
  out: sum('out'),
379
392
  usd: sum('usd'),
380
393
  ...(own > 0 ? { own_usd: own } : {}),
394
+ ...(plan > 0 ? { own_plan_calls: plan } : {}),
381
395
  parts: merged,
382
396
  });
383
397
  return { entries: entries.slice(0, USAGE_KEPT) };
@@ -12,6 +12,8 @@ export interface WebFetchOptions {
12
12
  userAgent?: string;
13
13
  fetch?: typeof fetch;
14
14
  }
15
+ /** Why a URL is refused, or null. */
16
+ export declare function refused(url: URL, block: string[]): string | null;
15
17
  /** One GET, never thrown: what it got, or why not. */
16
18
  export declare function webFetch(raw: string, headers?: Record<string, string>, o?: WebFetchOptions): Promise<{
17
19
  ok: boolean;
@@ -6,7 +6,7 @@ const TIMEOUT_MS = 10_000;
6
6
  const MAX_BYTES = 2_000_000;
7
7
  const DESCRIPTION = `Plain HTTP GET of one URL from the server, no browser: fast and cheap, so use it first for APIs, JSON, CSV, RSS and plain HTML. Returns {ok, status, url (after redirects), content_type, json (when the body is JSON) or text, truncated}; a failed fetch (refused, timed out, not 2xx) returns ok false with status and error. Bodies over ${MAX_BYTES / 1_000_000} MB are cut; ${TIMEOUT_MS / 1000} s per fetch. Use the browser tools (scrape, extract, ask) only for pages built by JavaScript, bot walls, or after fetch fails.`;
8
8
  /** Why a URL is refused, or null. */
9
- function refused(url, block) {
9
+ export function refused(url, block) {
10
10
  if (url.protocol !== 'https:' && url.protocol !== 'http:')
11
11
  return 'only http and https';
12
12
  const host = url.hostname.toLowerCase();
@@ -212,6 +212,31 @@ export function webStep(name, input, output, failed) {
212
212
  }
213
213
  return { ...(query ? { query } : {}), results };
214
214
  }
215
+ if (tool === 'lookup') {
216
+ if (failed)
217
+ return undefined;
218
+ let o = output;
219
+ if (typeof o === 'string') {
220
+ try {
221
+ o = JSON.parse(o);
222
+ }
223
+ catch {
224
+ o = {};
225
+ }
226
+ }
227
+ const r = (o && typeof o === 'object' ? o : {});
228
+ const reads = [];
229
+ for (const x of Array.isArray(r.sources) ? r.sources : []) {
230
+ const u = (x ?? {});
231
+ if (typeof u.url !== 'string' || !/^https?:\/\//.test(u.url))
232
+ continue;
233
+ if (reads.some((p) => p.url === u.url))
234
+ continue;
235
+ reads.push(typeof u.title === 'string' && u.title ? { url: u.url, title: u.title } : { url: u.url });
236
+ }
237
+ const q = typeof args.question === 'string' ? args.question : r.question;
238
+ return { lookup: true, ...(typeof q === 'string' ? { query: q.slice(0, 300) } : {}), reads };
239
+ }
215
240
  if (failed) {
216
241
  const url = typeof args.url === 'string' ? args.url : '';
217
242
  return /^https?:\/\//.test(url) ? { tried: { url } } : undefined;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Subagents: tools that do a small job on a cheap path and hand the assistant a short result, so
3
+ * the assistant's own context stays small. The first is web lookup.
4
+ */
5
+ export * from './web.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Subagents: tools that do a small job on a cheap path and hand the assistant a short result, so
3
+ * the assistant's own context stays small. The first is web lookup.
4
+ */
5
+ export * from './web.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Web lookup: one tool that answers a question from the web without putting whole pages in the
3
+ * assistant's context. It searches (ScrapeQL `search`, no model), then has ScrapeQL `ask` read each
4
+ * of the best pages and answer the question in a few sentences (ScrapeQL runs that on its cheapest
5
+ * model). The assistant gets back a short answer per page and its address, about a thousand tokens
6
+ * in all, instead of the pages themselves.
7
+ */
8
+ import { type ToolSource } from '../assistant/tools.js';
9
+ export interface WebLookupOptions {
10
+ /** Pages read for one question; default 3, at most 5. */
11
+ pages?: number;
12
+ /** Tokens each page's answer may use; default 300 (ScrapeQL's floor is 200). */
13
+ answerTokens?: number;
14
+ /** Hosts never read (the app's own, behind sign-in), each with its subdomains. */
15
+ blockHosts?: string[];
16
+ }
17
+ /** One page a lookup read: its address, its title when the search gave one, and its answer. */
18
+ export interface LookupSource {
19
+ url: string;
20
+ title?: string;
21
+ answer: string;
22
+ }
23
+ /** What `web__lookup` returns. */
24
+ export interface LookupResult {
25
+ question: string;
26
+ /** The search words, when it searched. */
27
+ query?: string;
28
+ sources: LookupSource[];
29
+ /** Pages it could not read, and why. */
30
+ failed?: {
31
+ url: string;
32
+ error: string;
33
+ }[];
34
+ }
35
+ /**
36
+ * The `lookup` tool (in the `web` group, `web__lookup`) over ScrapeQL's tools (`scrapeqlTools`).
37
+ * ScrapeQL is opened on the first lookup of an answer and closed with it.
38
+ */
39
+ export declare function webLookupTools(scrapeql: ToolSource, o?: WebLookupOptions): ToolSource;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Web lookup: one tool that answers a question from the web without putting whole pages in the
3
+ * assistant's context. It searches (ScrapeQL `search`, no model), then has ScrapeQL `ask` read each
4
+ * of the best pages and answer the question in a few sentences (ScrapeQL runs that on its cheapest
5
+ * model). The assistant gets back a short answer per page and its address, about a thousand tokens
6
+ * in all, instead of the pages themselves.
7
+ */
8
+ import { ToolError, plainTools } from '../assistant/tools.js';
9
+ import { refused } from '../assistant/web.js';
10
+ const LINK = /\[([^\]\n]{0,300})\]\((https?:\/\/[^)\s]{1,2000})\)/g;
11
+ /** ScrapeQL notes after an answer (`\n\n---\n_…_`): for an agent, not for this one. */
12
+ const FOOTER = /\n\n---\n_[\s\S]*$/;
13
+ const ANSWER_MAX = 2000;
14
+ const DESCRIPTION = 'Answer a question from the web. It searches, reads the best pages and returns a short answer from each with its address: about a thousand tokens, never whole pages. Ask one specific question (a part number, a standard, a size); pass urls to read pages you already know instead of searching. Returns {question, sources: [{url, title, answer}], failed}.';
15
+ const clamp = (n, lo, hi, d) => Math.min(hi, Math.max(lo, Math.round(n ?? d)));
16
+ /** Text of a tool's answer: a string, or JSON of anything else. */
17
+ const textOf = (v) => (typeof v === 'string' ? v : JSON.stringify(v ?? ''));
18
+ /** True when `raw` is an http(s) address this lookup may read. */
19
+ function readable(raw, block) {
20
+ try {
21
+ return refused(new URL(raw), block) === null;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * The `lookup` tool (in the `web` group, `web__lookup`) over ScrapeQL's tools (`scrapeqlTools`).
29
+ * ScrapeQL is opened on the first lookup of an answer and closed with it.
30
+ */
31
+ export function webLookupTools(scrapeql, o = {}) {
32
+ const pages = clamp(o.pages, 1, 5, 3);
33
+ const answerTokens = clamp(o.answerTokens, 200, 2000, 300);
34
+ const block = (o.blockHosts ?? []).map((h) => h.toLowerCase());
35
+ return async () => {
36
+ let open;
37
+ const sq = () => {
38
+ open ??= scrapeql();
39
+ return open;
40
+ };
41
+ const tools = await plainTools([
42
+ {
43
+ name: 'lookup',
44
+ title: 'Look it up on the web',
45
+ description: DESCRIPTION,
46
+ read: true,
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ question: {
51
+ type: 'string',
52
+ maxLength: 500,
53
+ description: 'The question each page should answer, in full.',
54
+ },
55
+ search: {
56
+ type: 'string',
57
+ maxLength: 300,
58
+ description: 'Search words, when they differ from the question.',
59
+ },
60
+ urls: {
61
+ type: 'array',
62
+ items: { type: 'string', maxLength: 2000 },
63
+ maxItems: pages,
64
+ description: 'Pages to read instead of searching.',
65
+ },
66
+ },
67
+ required: ['question'],
68
+ },
69
+ async run(args) {
70
+ const a = (args ?? {});
71
+ const question = String(a.question ?? '')
72
+ .trim()
73
+ .slice(0, 500);
74
+ if (!question)
75
+ throw new ToolError('Give the question to look up.');
76
+ const given = Array.isArray(a.urls) ? a.urls.map(String) : [];
77
+ const t = await sq();
78
+ let query;
79
+ let picks;
80
+ if (given.length) {
81
+ picks = given.filter((u) => readable(u, block)).map((url) => ({ url }));
82
+ if (!picks.length)
83
+ throw new ToolError('None of those addresses can be read.');
84
+ }
85
+ else {
86
+ query =
87
+ String(a.search ?? '')
88
+ .trim()
89
+ .slice(0, 300) || question;
90
+ const found = textOf(await t.call('search', { query, limit: pages + 3 }));
91
+ picks = [];
92
+ for (const m of found.matchAll(LINK)) {
93
+ const url = m[2];
94
+ if (!readable(url, block) || picks.some((p) => p.url === url))
95
+ continue;
96
+ const title = m[1].trim();
97
+ picks.push(title && title !== url ? { url, title } : { url });
98
+ }
99
+ }
100
+ picks = picks.slice(0, pages);
101
+ const out = { question, ...(query ? { query } : {}), sources: [] };
102
+ if (!picks.length)
103
+ return out;
104
+ const read = await Promise.all(picks.map(async (p) => {
105
+ try {
106
+ const r = textOf(await t.call('ask', { url: p.url, question, budgetTokens: answerTokens }));
107
+ return { ...p, answer: r.replace(FOOTER, '').trim().slice(0, ANSWER_MAX) };
108
+ }
109
+ catch (err) {
110
+ return { url: p.url, error: String(err?.message ?? err).slice(0, 200) };
111
+ }
112
+ }));
113
+ const failed = [];
114
+ for (const r of read)
115
+ if ('answer' in r) {
116
+ if (r.answer)
117
+ out.sources.push(r);
118
+ }
119
+ else
120
+ failed.push(r);
121
+ return failed.length ? { ...out, failed } : out;
122
+ },
123
+ },
124
+ ])();
125
+ return {
126
+ ...tools,
127
+ async close() {
128
+ if (open)
129
+ await (await open.catch(() => null))?.close();
130
+ },
131
+ };
132
+ };
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.250",
3
+ "version": "0.1.252",
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",
@@ -26,6 +26,10 @@
26
26
  "./workspaces": {
27
27
  "types": "./dist/workspaces/index.d.ts",
28
28
  "default": "./dist/workspaces/index.js"
29
+ },
30
+ "./subagents": {
31
+ "types": "./dist/subagents/index.d.ts",
32
+ "default": "./dist/subagents/index.js"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -52,8 +56,8 @@
52
56
  "@ai-sdk/openai": "4.0.108",
53
57
  "@anthropic-ai/sdk": "0.133.0",
54
58
  "@cloudflare/think": "0.23.0",
55
- "@columenlabs/lumen-ops": "0.1.250",
56
- "@columenlabs/lumen-ui-types": "0.1.250",
59
+ "@columenlabs/lumen-ops": "0.1.252",
60
+ "@columenlabs/lumen-ui-types": "0.1.252",
57
61
  "agents": "0.32.0",
58
62
  "ai": "7.0.162",
59
63
  "just-bash": "3.6.0",