@columenlabs/lumen-ai 0.1.185 → 0.1.187
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 +2 -1
- package/dist/agent/agent.d.ts +9 -0
- package/dist/agent/agent.js +24 -8
- package/dist/agent/models.d.ts +14 -2
- package/dist/agent/models.js +159 -9
- package/dist/assistant/engine.js +8 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -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
|
@@ -15,8 +15,11 @@ export interface AiModelOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
|
|
17
17
|
* forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
|
|
18
|
-
* limitations).
|
|
19
|
-
*
|
|
18
|
+
* limitations). It also keeps nothing (`store: false`, so earlier reasoning rides along encrypted)
|
|
19
|
+
* and takes the system prompt as `instructions`, as the assistant's own OpenAI loop sends it (the
|
|
20
|
+
* AI SDK's request without these came back 400 Bad Request on a plan, 2026-11-04). This fetch
|
|
21
|
+
* rewrites each Responses request to those rules, so the AI SDK's OpenAI provider runs on a plan
|
|
22
|
+
* unchanged.
|
|
20
23
|
*/
|
|
21
24
|
export declare function planFetch(base?: typeof fetch, name?: string): typeof fetch;
|
|
22
25
|
/** The AI SDK model a tier runs: its provider's client on the payer's key. */
|
|
@@ -30,3 +33,12 @@ export declare function turnSettings(model: ModelDef, o: {
|
|
|
30
33
|
maxOutputTokens?: number;
|
|
31
34
|
providerOptions?: Record<string, Record<string, JSONValue>>;
|
|
32
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
|
@@ -8,8 +8,11 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|
|
8
8
|
/**
|
|
9
9
|
* A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
|
|
10
10
|
* forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
|
|
11
|
-
* limitations).
|
|
12
|
-
*
|
|
11
|
+
* limitations). It also keeps nothing (`store: false`, so earlier reasoning rides along encrypted)
|
|
12
|
+
* and takes the system prompt as `instructions`, as the assistant's own OpenAI loop sends it (the
|
|
13
|
+
* AI SDK's request without these came back 400 Bad Request on a plan, 2026-11-04). This fetch
|
|
14
|
+
* rewrites each Responses request to those rules, so the AI SDK's OpenAI provider runs on a plan
|
|
15
|
+
* unchanged.
|
|
13
16
|
*/
|
|
14
17
|
export function planFetch(base = fetch, name = 'Assistant') {
|
|
15
18
|
return async (input, init) => {
|
|
@@ -17,22 +20,66 @@ export function planFetch(base = fetch, name = 'Assistant') {
|
|
|
17
20
|
if (!url.endsWith('/responses') || typeof init?.body !== 'string')
|
|
18
21
|
return base(input, init);
|
|
19
22
|
const { max_output_tokens: _cap, ...body } = JSON.parse(init.body);
|
|
23
|
+
body.store = false;
|
|
24
|
+
const include = Array.isArray(body.include) ? body.include : [];
|
|
25
|
+
if (!include.includes('reasoning.encrypted_content'))
|
|
26
|
+
body.include = [...include, 'reasoning.encrypted_content'];
|
|
27
|
+
// System and developer messages become the request's instructions.
|
|
28
|
+
const items = Array.isArray(body.input) ? body.input : [];
|
|
29
|
+
const lead = items.filter((m) => m.role === 'system' || m.role === 'developer');
|
|
30
|
+
if (lead.length) {
|
|
31
|
+
const said = lead.map((m) => textOf(m.content)).filter(Boolean);
|
|
32
|
+
if (typeof body.instructions === 'string' && body.instructions)
|
|
33
|
+
said.unshift(body.instructions);
|
|
34
|
+
body.instructions = said.join('\n\n');
|
|
35
|
+
body.input = items.filter((m) => !lead.includes(m));
|
|
36
|
+
}
|
|
20
37
|
let tools = body.tools ?? [];
|
|
21
38
|
const choice = body.tool_choice;
|
|
22
39
|
if (choice && typeof choice === 'object' && choice.type === 'function' && choice.name) {
|
|
23
40
|
tools = tools.filter((t) => t.name === choice.name);
|
|
24
41
|
body.tool_choice = 'required';
|
|
25
42
|
}
|
|
26
|
-
|
|
43
|
+
// Each function as the assistant's own loop sends it: no `strict`, no JSON Schema dialect tag.
|
|
44
|
+
const fns = tools
|
|
45
|
+
.filter((t) => t.type === 'function')
|
|
46
|
+
.map(({ strict: _, ...t }) => {
|
|
47
|
+
const p = t.parameters;
|
|
48
|
+
if (!p || !('$schema' in p))
|
|
49
|
+
return t;
|
|
50
|
+
const { $schema: _s, ...parameters } = p;
|
|
51
|
+
return { ...t, parameters };
|
|
52
|
+
});
|
|
27
53
|
const rest = tools.filter((t) => t.type !== 'function');
|
|
28
54
|
if (fns.length)
|
|
29
55
|
body.tools = [
|
|
30
56
|
...rest,
|
|
31
57
|
{ type: 'namespace', name: 'app', description: `${name}'s tools`, tools: fns },
|
|
32
58
|
];
|
|
33
|
-
|
|
59
|
+
const res = await base(input, { ...init, body: JSON.stringify(body) });
|
|
60
|
+
// A refusal is logged with OpenAI's own reason; the person sees a plain sentence.
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
console.error(JSON.stringify({
|
|
63
|
+
event: 'assistant_plan_refused',
|
|
64
|
+
status: res.status,
|
|
65
|
+
body: (await res.clone().text()).slice(0, 500),
|
|
66
|
+
}));
|
|
67
|
+
return res;
|
|
34
68
|
};
|
|
35
69
|
}
|
|
70
|
+
/** A message's text, whether a string or a list of text parts. */
|
|
71
|
+
function textOf(content) {
|
|
72
|
+
if (typeof content === 'string')
|
|
73
|
+
return content;
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return '';
|
|
76
|
+
return content
|
|
77
|
+
.map((p) => p && typeof p === 'object' && typeof p.text === 'string'
|
|
78
|
+
? p.text
|
|
79
|
+
: '')
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join('\n');
|
|
82
|
+
}
|
|
36
83
|
/** The AI SDK model a tier runs: its provider's client on the payer's key. */
|
|
37
84
|
export function aiModel(model, key, o = {}) {
|
|
38
85
|
if (model.provider === 'anthropic')
|
|
@@ -43,9 +90,112 @@ export function aiModel(model, key, o = {}) {
|
|
|
43
90
|
/** A tier's effort and output cap as a turn's settings; nothing at the model's own default. */
|
|
44
91
|
export function turnSettings(model, o) {
|
|
45
92
|
const out = o.plan || !o.out ? {} : { maxOutputTokens: o.out };
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
// A plan keeps nothing, so earlier turns go back as items, not references to stored ones.
|
|
94
|
+
const openai = o.plan ? { store: false } : {};
|
|
95
|
+
if (o.effort !== 'default') {
|
|
96
|
+
if (model.provider === 'anthropic')
|
|
97
|
+
return { ...out, providerOptions: { anthropic: { effort: o.effort } } };
|
|
98
|
+
openai.reasoningEffort = o.effort;
|
|
99
|
+
}
|
|
100
|
+
return model.provider === 'openai' && Object.keys(openai).length
|
|
101
|
+
? { ...out, providerOptions: { openai } }
|
|
102
|
+
: out;
|
|
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;
|
|
51
201
|
}
|
package/dist/assistant/engine.js
CHANGED
|
@@ -388,6 +388,13 @@ export class Assistant {
|
|
|
388
388
|
return apiKey ? { apiKey, ...(paying.plans[provider] ? { plan: true } : {}) } : null;
|
|
389
389
|
};
|
|
390
390
|
const key = keyOf(main.model.provider) ?? { apiKey: paying.apiKey };
|
|
391
|
+
// A ChatGPT plan used up for now: the app's first Claude (its smallest) answers instead, on
|
|
392
|
+
// the same payer's Anthropic key, rather than the turn ending in the limit's error.
|
|
393
|
+
const claude = key.plan ? this.models.find((m) => m.provider === 'anthropic') : undefined;
|
|
394
|
+
const claudeKey = claude ? keyOf('anthropic') : null;
|
|
395
|
+
const fallback = claude && claudeKey
|
|
396
|
+
? { model: claude, key: claudeKey, label: `${main.tier.label} · ${claude.label}` }
|
|
397
|
+
: undefined;
|
|
391
398
|
const p = await this.state.person(caller.id);
|
|
392
399
|
const policy = p.hand_off ?? 'next';
|
|
393
400
|
const advisor = await this.advisorFor(caller, p.advice, main);
|
|
@@ -423,6 +430,7 @@ export class Assistant {
|
|
|
423
430
|
? `The person handed this message to ${handTo.label}: call escalate {tier: "${handTo.id}", why: "asked"} now, its task the message with every fact from this chat it needs, then answer from its memo.\n\n`
|
|
424
431
|
: ''}${command}`,
|
|
425
432
|
sources: await this.sourcesOf(caller, p),
|
|
433
|
+
...(fallback ? { fallback } : {}),
|
|
426
434
|
left: paying.left,
|
|
427
435
|
maxSteps: MAX_ROUNDS,
|
|
428
436
|
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.187",
|
|
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.
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
51
|
+
"@columenlabs/lumen-ops": "0.1.187",
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.187",
|
|
53
53
|
"agents": "0.32.0",
|
|
54
54
|
"ai": "7.0.162",
|
|
55
55
|
"just-bash": "3.6.0",
|