@columenlabs/lumen-ai 0.1.186 → 0.1.188
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 -3
- package/dist/agent/agent.d.ts +9 -0
- package/dist/agent/agent.js +24 -8
- package/dist/agent/models.d.ts +9 -0
- package/dist/agent/models.js +98 -0
- package/dist/assistant/engine.d.ts +1 -30
- package/dist/assistant/engine.js +13 -1288
- package/dist/assistant/routes.js +3 -29
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -98,10 +98,10 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
|
|
|
98
98
|
- **Keys.** Each provider has its own keys (`?provider=anthropic|openai` on the key routes). An admin pastes the shared key everyone uses (`PUT /key`); it is tested with one free call before it is kept, then sealed, and no route returns it. A person may add their own (`PUT /my-key`), with one monthly cap on their own keys (`PUT /my-key/cap`).
|
|
99
99
|
- **ChatGPT sign-in.** In place of their own OpenAI key a person may sign in with their ChatGPT plan (`POST /my-signin`, then `POST /my-signin/finish {url}`): the sign-in opens in a new tab and ends on `http://127.0.0.1:1455/auth/callback`, a page that does not load, whose address the person pastes back. The tokens are sealed as their own key and refreshed before they run out; answers on the plan count tokens and no dollars.
|
|
100
100
|
- **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.
|
|
101
|
-
- **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 })
|
|
101
|
+
- **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`.
|
|
102
102
|
- **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.
|
|
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
|
-
- **Chats**
|
|
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
|
- **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. It throws `AssistantError` `not_connected`, `app_cap` or `own_cap` when nobody can pay, so the caller waits and tries later.
|
|
106
106
|
|
|
107
107
|
### Commands, `@` and `#`
|
|
@@ -220,11 +220,12 @@ ctx.waitUntil(meaningStep({ def: notesFind, sql: d1Sql(env.DB), embed, index: en
|
|
|
220
220
|
| `LumenAgent` | An abstract Think agent. The app answers `plan(turn)` (model, key, effort, instructions, tool sources, money left) and hears `settle(end)` (outcome, usage parts, context size, time) when the turn ends |
|
|
221
221
|
| `aiModel(model, key, opts?)` | The AI SDK model for a tier on an API key, or on a ChatGPT plan sign-in (`key.plan`, through `planFetch`) |
|
|
222
222
|
| `planFetch(fetch?, name?)` | Rewrites Responses calls to a plan sign-in's rules: tools in one namespace, no output cap, a forced call as the only tool |
|
|
223
|
+
| `planFallback(primary, fallback, onSwitch)` | A plan's model that hands the call, and the rest of the turn, to `fallback` when the plan reaches its usage limit (a 429 or a usage code, before any output) |
|
|
223
224
|
| `turnSettings(model, opts)` | Output cap and effort as the provider takes them |
|
|
224
225
|
| `openAiTools(sources, opts)` | The app's tool groups as AI SDK tools: reads always, drafts and writes by access, results cut at 16k characters |
|
|
225
226
|
| `UsageMeter`, `tokenUsage(u)` | Prices each step's tokens (cache reads and writes, reasoning) into one usage part |
|
|
226
227
|
|
|
227
|
-
A ChatGPT plan sign-in is free to the app (`priced` with `free`). Nothing here holds a key or a token: the app passes them per turn.
|
|
228
|
+
A ChatGPT plan sign-in is free to the app (`priced` with `free`). When the plan is used up for now, a turn with `fallback` (`agentTurn` sets the app's first Claude on the same payer's Anthropic key) answers on Claude instead of ending in the limit's error, its use its own part. Nothing here holds a key or a token: the app passes them per turn.
|
|
228
229
|
|
|
229
230
|
An app with an `Assistant` gets the whole plan from it: `assistant.agentTurn(caller, { message, tier?, context?, refs?, time_zone? })` returns `{ plan, settle }` with the person's tier and model on whoever pays, the tool groups at their access, the kit's rules, the bearings and the page before the message, `escalate` to a higher tier (its own usage part, inside the same cap) and Claude's advisor. `settle` adds the spend to the payer's month and reports the turn through `onStep`. It throws `AssistantError` when nobody can pay.
|
|
230
231
|
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -61,6 +61,15 @@ export interface AgentTurnPlan {
|
|
|
61
61
|
maxUses: number;
|
|
62
62
|
maxTokens: number;
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Who answers when the ChatGPT plan `key` signs in with reaches its usage limit: a Claude model
|
|
66
|
+
* on the payer's own key, for the rest of the turn. Its use is its own part (`label`).
|
|
67
|
+
*/
|
|
68
|
+
fallback?: {
|
|
69
|
+
model: ModelDef;
|
|
70
|
+
key: ModelKey;
|
|
71
|
+
label: string;
|
|
72
|
+
};
|
|
64
73
|
/** Dollars the payer has left; at or past it the model gets no more tools. */
|
|
65
74
|
left?: number;
|
|
66
75
|
/** Most model calls in one answer; 8. */
|
package/dist/agent/agent.js
CHANGED
|
@@ -16,7 +16,7 @@ import { Think, } from '@cloudflare/think';
|
|
|
16
16
|
import { createCompactFunction } from 'agents/sessions';
|
|
17
17
|
import { generateText } from 'ai';
|
|
18
18
|
import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
|
|
19
|
-
import { aiModel, turnSettings } from './models.js';
|
|
19
|
+
import { aiModel, planFallback, turnSettings } from './models.js';
|
|
20
20
|
import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
|
|
21
21
|
import { chunkEvents, itemsOf } from './stream.js';
|
|
22
22
|
import { openAiTools, } from './tools.js';
|
|
@@ -348,12 +348,15 @@ export class LumenAgent extends Think {
|
|
|
348
348
|
if (!last)
|
|
349
349
|
throw new Error('No model to compact with until the next answer.');
|
|
350
350
|
const { plan } = last;
|
|
351
|
-
|
|
351
|
+
let meter = new UsageMeter(plan.model, `${plan.label} · compact`, !!plan.key.plan);
|
|
352
352
|
this.#owed.push(meter);
|
|
353
353
|
const r = await generateText({
|
|
354
|
-
model:
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
model: this.#model(plan, () => {
|
|
355
|
+
const fb = plan.fallback;
|
|
356
|
+
if (!fb)
|
|
357
|
+
return;
|
|
358
|
+
meter = new UsageMeter(fb.model, `${fb.label} · compact`, false);
|
|
359
|
+
this.#owed.push(meter);
|
|
357
360
|
}),
|
|
358
361
|
prompt,
|
|
359
362
|
...turnSettings(plan.model, { effort: 'default', out: 4000, plan: !!plan.key.plan }),
|
|
@@ -361,6 +364,14 @@ export class LumenAgent extends Think {
|
|
|
361
364
|
meter.add(r.usage);
|
|
362
365
|
return r.text;
|
|
363
366
|
}
|
|
367
|
+
/** The turn's model; on a ChatGPT plan with a fallback, Claude once the plan's limit is reached. */
|
|
368
|
+
#model(plan, onSwitch) {
|
|
369
|
+
const o = { ...(plan.fetch ? { fetch: plan.fetch } : {}), name: this.constructor.name };
|
|
370
|
+
const model = aiModel(plan.model, plan.key, o);
|
|
371
|
+
if (!plan.key.plan || !plan.fallback)
|
|
372
|
+
return model;
|
|
373
|
+
return planFallback(model, aiModel(plan.fallback.model, plan.fallback.key, o), onSwitch);
|
|
374
|
+
}
|
|
364
375
|
/** Never read: `beforeTurn` gives every turn its own model. */
|
|
365
376
|
getModel() {
|
|
366
377
|
return '@cf/none';
|
|
@@ -442,10 +453,15 @@ export class LumenAgent extends Think {
|
|
|
442
453
|
this.#turn.system = system;
|
|
443
454
|
this.#turn.lazy = lazy;
|
|
444
455
|
}
|
|
456
|
+
const turn = this.#turn;
|
|
445
457
|
return {
|
|
446
|
-
model:
|
|
447
|
-
|
|
448
|
-
|
|
458
|
+
model: this.#model(plan, () => {
|
|
459
|
+
const fb = plan.fallback;
|
|
460
|
+
if (!fb)
|
|
461
|
+
return;
|
|
462
|
+
// Claude answers the rest of the turn on the payer's key: its use is its own part.
|
|
463
|
+
turn.main = new UsageMeter(fb.model, fb.label, false);
|
|
464
|
+
turn.meters.push(turn.main);
|
|
449
465
|
}),
|
|
450
466
|
instructions: system,
|
|
451
467
|
messages: withLead(ctx.messages, `${this.#news()}${plan.lead ?? ''}`),
|
package/dist/agent/models.d.ts
CHANGED
|
@@ -33,3 +33,12 @@ export declare function turnSettings(model: ModelDef, o: {
|
|
|
33
33
|
maxOutputTokens?: number;
|
|
34
34
|
providerOptions?: Record<string, Record<string, JSONValue>>;
|
|
35
35
|
};
|
|
36
|
+
/** A ChatGPT plan used up for now: a 429 or a usage code, thrown or sent before any output. */
|
|
37
|
+
export declare function planLimited(err: unknown): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* `primary` until its ChatGPT plan reaches its usage limit, then `fallback` for that call and
|
|
40
|
+
* every later one on this model (the rest of the turn). The limit shows either as a thrown error
|
|
41
|
+
* or as an error part before any output; once output has started nothing is swapped. `onSwitch`
|
|
42
|
+
* runs once, at the swap.
|
|
43
|
+
*/
|
|
44
|
+
export declare function planFallback(primary: LanguageModel, fallback: LanguageModel, onSwitch: () => void): LanguageModel;
|
package/dist/agent/models.js
CHANGED
|
@@ -101,3 +101,101 @@ export function turnSettings(model, o) {
|
|
|
101
101
|
? { ...out, providerOptions: { openai } }
|
|
102
102
|
: out;
|
|
103
103
|
}
|
|
104
|
+
/** A ChatGPT plan used up for now: a 429 or a usage code, thrown or sent before any output. */
|
|
105
|
+
export function planLimited(err) {
|
|
106
|
+
const e = err;
|
|
107
|
+
if (!e)
|
|
108
|
+
return false;
|
|
109
|
+
if (e.statusCode === 429 || e.status === 429)
|
|
110
|
+
return true;
|
|
111
|
+
const text = `${e.code ?? ''} ${e.message ?? ''} ${e.responseBody ?? ''} ${typeof err === 'string' ? err : ''}`;
|
|
112
|
+
return /usage_limit|usage limit|usage_unavailable|insufficient_quota/i.test(text);
|
|
113
|
+
}
|
|
114
|
+
/** Parts that come before an answer starts; an error among them means nothing was said yet. */
|
|
115
|
+
const PREAMBLE = new Set(['stream-start', 'response-metadata']);
|
|
116
|
+
/**
|
|
117
|
+
* `primary` until its ChatGPT plan reaches its usage limit, then `fallback` for that call and
|
|
118
|
+
* every later one on this model (the rest of the turn). The limit shows either as a thrown error
|
|
119
|
+
* or as an error part before any output; once output has started nothing is swapped. `onSwitch`
|
|
120
|
+
* runs once, at the swap.
|
|
121
|
+
*/
|
|
122
|
+
export function planFallback(primary, fallback, onSwitch) {
|
|
123
|
+
const p = primary;
|
|
124
|
+
const f = fallback;
|
|
125
|
+
let on = p;
|
|
126
|
+
const swap = () => {
|
|
127
|
+
if (on === f)
|
|
128
|
+
return;
|
|
129
|
+
on = f;
|
|
130
|
+
onSwitch();
|
|
131
|
+
};
|
|
132
|
+
const model = {
|
|
133
|
+
specificationVersion: p.specificationVersion,
|
|
134
|
+
provider: p.provider,
|
|
135
|
+
modelId: p.modelId,
|
|
136
|
+
get supportedUrls() {
|
|
137
|
+
return on.supportedUrls;
|
|
138
|
+
},
|
|
139
|
+
async doGenerate(o) {
|
|
140
|
+
if (on === f)
|
|
141
|
+
return f.doGenerate(o);
|
|
142
|
+
try {
|
|
143
|
+
return await p.doGenerate(o);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (!planLimited(err))
|
|
147
|
+
throw err;
|
|
148
|
+
swap();
|
|
149
|
+
return f.doGenerate(o);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
async doStream(o) {
|
|
153
|
+
if (on === f)
|
|
154
|
+
return f.doStream(o);
|
|
155
|
+
let r;
|
|
156
|
+
try {
|
|
157
|
+
r = await p.doStream(o);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
if (!planLimited(err))
|
|
161
|
+
throw err;
|
|
162
|
+
swap();
|
|
163
|
+
return f.doStream(o);
|
|
164
|
+
}
|
|
165
|
+
// Hold the preamble until the first real part shows whether the plan answered.
|
|
166
|
+
const reader = r.stream.getReader();
|
|
167
|
+
const held = [];
|
|
168
|
+
for (;;) {
|
|
169
|
+
const { done, value } = await reader.read();
|
|
170
|
+
if (done)
|
|
171
|
+
break;
|
|
172
|
+
if (value.type === 'error' && planLimited(value.error)) {
|
|
173
|
+
await reader.cancel().catch(() => undefined);
|
|
174
|
+
swap();
|
|
175
|
+
return f.doStream(o);
|
|
176
|
+
}
|
|
177
|
+
held.push(value);
|
|
178
|
+
if (!PREAMBLE.has(value.type))
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const stream = new ReadableStream({
|
|
182
|
+
start(c) {
|
|
183
|
+
for (const v of held)
|
|
184
|
+
c.enqueue(v);
|
|
185
|
+
},
|
|
186
|
+
async pull(c) {
|
|
187
|
+
const { done, value } = await reader.read();
|
|
188
|
+
if (done)
|
|
189
|
+
c.close();
|
|
190
|
+
else
|
|
191
|
+
c.enqueue(value);
|
|
192
|
+
},
|
|
193
|
+
cancel(reason) {
|
|
194
|
+
return reader.cancel(reason);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return { ...r, stream };
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
return model;
|
|
201
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus
|
|
1
|
+
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus } 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';
|
|
@@ -173,7 +173,6 @@ export declare class AssistantError extends Error {
|
|
|
173
173
|
readonly status: number;
|
|
174
174
|
constructor(message: string, code: string, status?: number);
|
|
175
175
|
}
|
|
176
|
-
type Emit = (event: string, data: unknown) => void;
|
|
177
176
|
/** What a Think turn calls this turn: what one turn asks besides the message itself. */
|
|
178
177
|
export interface AgentTurnInput {
|
|
179
178
|
message: string;
|
|
@@ -262,37 +261,17 @@ export declare class Assistant {
|
|
|
262
261
|
removeChat(caller: Caller, id: string): Promise<void>;
|
|
263
262
|
/** Deletes chats past `keepDays` since their last message; run it once a day. */
|
|
264
263
|
purge(): Promise<number>;
|
|
265
|
-
private writeChat;
|
|
266
264
|
/** Opens the tools the person allows: each group's, then the web's. */
|
|
267
265
|
/** The tool groups (and the web) at the person's access, not opened yet. */
|
|
268
266
|
private sourcesOf;
|
|
269
|
-
private openTools;
|
|
270
|
-
private toolsFor;
|
|
271
|
-
/** `batch`: any of the reads, up to BATCH_MAX at once, as one step. Null with no reads. */
|
|
272
|
-
private batchTool;
|
|
273
|
-
/** One answer as server-sent events. Abort `signal` to stop it. */
|
|
274
|
-
turnStream(caller: Caller, req: AssistantMessage, signal?: AbortSignal): ReadableStream<Uint8Array>;
|
|
275
|
-
turn(caller: Caller, req: AssistantMessage, emit: Emit, signal?: AbortSignal): Promise<void>;
|
|
276
267
|
/** What the kit adds to the app's instructions: tools, drafts, hand off, advice, limits. */
|
|
277
268
|
private rules;
|
|
278
269
|
/** The advisor for this answer, or null when off. Never weaker than the model it advises. */
|
|
279
270
|
private advisorFor;
|
|
280
|
-
private escalateTool;
|
|
281
271
|
/** What the model is asked: an app command's prompt, else the message as typed. */
|
|
282
272
|
private expand;
|
|
283
273
|
/** The tiers the person's message asks for by name ("go deeper" is the next one up). */
|
|
284
274
|
private askedFor;
|
|
285
|
-
private handOff;
|
|
286
|
-
/** One tool call: a draft card for a write, else the call itself, timed. */
|
|
287
|
-
private runTool;
|
|
288
|
-
/** A batch of reads: each runs as its own call (the same read twice runs once), in parallel, shown as one step. */
|
|
289
|
-
private runBatch;
|
|
290
|
-
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
291
|
-
private loop;
|
|
292
|
-
/** The loop on OpenAI's Responses API: the same tools, Stop, caps and rounds as Claude's. */
|
|
293
|
-
private loopOpenAI;
|
|
294
|
-
/** The loop on Anthropic's Messages API, with the advisor and prompt caching. */
|
|
295
|
-
private loopClaude;
|
|
296
275
|
/** Swaps the history for a short summary. False with nothing to compact. */
|
|
297
276
|
/**
|
|
298
277
|
* One plain answer from OpenAI, metered on `s`: no tools, no chat. Streamed and read from the raw
|
|
@@ -300,7 +279,6 @@ export declare class Assistant {
|
|
|
300
279
|
* output cap and may send its words only as deltas.
|
|
301
280
|
*/
|
|
302
281
|
private openaiOnce;
|
|
303
|
-
private compact;
|
|
304
282
|
/**
|
|
305
283
|
* One prompt answered on a task's tier as the person: no chat, no tools. It is for background
|
|
306
284
|
* work an app runs for someone (a job's fit score, a summary), billed to their payer and capped
|
|
@@ -320,8 +298,6 @@ export declare class Assistant {
|
|
|
320
298
|
usd: number;
|
|
321
299
|
model: string;
|
|
322
300
|
}>;
|
|
323
|
-
/** How the person's cards ended since the last answer, once each. */
|
|
324
|
-
private cardNews;
|
|
325
301
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
326
302
|
startSignin(caller: Caller, provider: AssistantProvider): Promise<{
|
|
327
303
|
url: string;
|
|
@@ -353,10 +329,5 @@ export declare class Assistant {
|
|
|
353
329
|
detail: string;
|
|
354
330
|
at: number;
|
|
355
331
|
}>;
|
|
356
|
-
/** Saves (runs, as the person) or dismisses a drafted card. Access is checked again first. */
|
|
357
|
-
settleDraft(caller: Caller, chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
|
358
|
-
/** Takes back one change a write made at once, through its group's `undo`. */
|
|
359
|
-
undoWrite(caller: Caller, chatId: string, stepId: string): Promise<StepItem>;
|
|
360
332
|
}
|
|
361
333
|
export declare function createAssistant(o: AssistantOptions): Assistant;
|
|
362
|
-
export {};
|