@columenlabs/lumen-ai 0.1.276 → 0.1.278
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/dist/agent/agent.d.ts +2 -0
- package/dist/agent/agent.js +12 -4
- package/dist/agent/models.d.ts +11 -4
- package/dist/agent/models.js +63 -5
- package/dist/assistant/engine.d.ts +2 -0
- package/dist/assistant/engine.js +8 -2
- package/dist/assistant/state.d.ts +5 -0
- package/dist/assistant/state.js +6 -0
- package/package.json +3 -3
package/dist/agent/agent.d.ts
CHANGED
|
@@ -177,6 +177,8 @@ export interface AskInput {
|
|
|
177
177
|
outcomes?: AssistantScreenOutcome[];
|
|
178
178
|
/** The person kept this message in this chat after a fresh chat was offered (`offerFresh`). */
|
|
179
179
|
stay?: boolean;
|
|
180
|
+
/** How many images went with the message (set by the agent: the images go in its parts). */
|
|
181
|
+
image_count?: number;
|
|
180
182
|
}
|
|
181
183
|
/** A quote as sent, checked: its text cut to `ASSISTANT_QUOTE_MAX`, and whose message it came from. */
|
|
182
184
|
export declare function quoteOf(q: unknown): AssistantQuote | undefined;
|
package/dist/agent/agent.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, ASSISTANT_IMAGE_TYPES, ASS
|
|
|
18
18
|
import { createCompactFunction } from 'agents/sessions';
|
|
19
19
|
import { generateText } from 'ai';
|
|
20
20
|
import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
|
|
21
|
-
import { aiModel, cachedTurn, planFallback, turnSettings } from './models.js';
|
|
21
|
+
import { aiModel, cachedTurn, planFallback, prefixCacheKey, turnSettings, } from './models.js';
|
|
22
22
|
import { screenMapOf, screenNews, screenOutcomesOf, screenRules, screenTool, } from './screen.js';
|
|
23
23
|
import { LAZY_AT, TOOL_SEARCH, lazyTools, usedTools } from './search.js';
|
|
24
24
|
import { chunkEvents, itemsOf, shownTitle, } from './stream.js';
|
|
@@ -413,7 +413,12 @@ export class LumenAgent extends Think {
|
|
|
413
413
|
this.#steered = [];
|
|
414
414
|
// The images go in the message's parts; the turn's plan reads the rest.
|
|
415
415
|
const { images: _images, ...asked } = input;
|
|
416
|
-
|
|
416
|
+
const pictures = imageParts(input.images);
|
|
417
|
+
this.#input = {
|
|
418
|
+
...asked,
|
|
419
|
+
...(map ? { screen: map } : {}),
|
|
420
|
+
...(pictures.length ? { image_count: pictures.length } : {}),
|
|
421
|
+
};
|
|
417
422
|
if (!map)
|
|
418
423
|
delete this.#input.screen;
|
|
419
424
|
let failed = false;
|
|
@@ -424,7 +429,7 @@ export class LumenAgent extends Think {
|
|
|
424
429
|
parts: [
|
|
425
430
|
...(quote ? [{ type: 'text', text: quoteLead(quote) }] : []),
|
|
426
431
|
{ type: 'text', text: input.message },
|
|
427
|
-
...
|
|
432
|
+
...pictures,
|
|
428
433
|
],
|
|
429
434
|
metadata: meta,
|
|
430
435
|
}, {
|
|
@@ -854,7 +859,10 @@ export class LumenAgent extends Think {
|
|
|
854
859
|
effort: plan.effort ?? 'default',
|
|
855
860
|
...(plan.out ? { out: plan.out } : {}),
|
|
856
861
|
plan: free,
|
|
857
|
-
|
|
862
|
+
// Named for how the prompt starts, so a new chat reads its instructions and tools from the cache.
|
|
863
|
+
cacheKey: prefixCacheKey(plan.model.id, system, Object.keys(tools)
|
|
864
|
+
.filter((n) => !lazy || lazy.active().includes(n))
|
|
865
|
+
.join(',')),
|
|
858
866
|
}),
|
|
859
867
|
};
|
|
860
868
|
}
|
package/dist/agent/models.d.ts
CHANGED
|
@@ -35,14 +35,21 @@ export declare function cachedTurn(instructions: string, messages: ModelMessage[
|
|
|
35
35
|
messages: ModelMessage[];
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* OpenAI's prompt cache key
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* OpenAI's prompt cache key: calls with the same key go to the same cache, so a long prompt's start
|
|
39
|
+
* is read from it instead of paid again. A ChatGPT plan sign-in caches nothing without one. At most
|
|
40
|
+
* 64 characters.
|
|
41
41
|
*/
|
|
42
42
|
export declare const promptCacheKey: (key: string) => string;
|
|
43
|
+
/**
|
|
44
|
+
* A cache key named for what a prompt starts with (its model, instructions and tools), not for
|
|
45
|
+
* one chat or run: a new chat or run that starts the same way reads that start from the cache
|
|
46
|
+
* (a key per chat read 0 of a new chat's 5.9k-token start; one key for the start read 99.7%, our
|
|
47
|
+
* key, 2026-11-14). The key only routes; what is read is still only the exact same start.
|
|
48
|
+
*/
|
|
49
|
+
export declare function prefixCacheKey(...parts: (string | undefined)[]): string;
|
|
43
50
|
/**
|
|
44
51
|
* A tier's effort and output cap as a turn's settings, with the prompt cache on: Claude's
|
|
45
|
-
* breakpoint, and OpenAI's cache key when `cacheKey`
|
|
52
|
+
* breakpoint, and OpenAI's cache key (`prefixCacheKey`) when `cacheKey` is given.
|
|
46
53
|
*/
|
|
47
54
|
export declare function turnSettings(model: ModelDef, o: {
|
|
48
55
|
effort: AssistantEffort;
|
package/dist/agent/models.js
CHANGED
|
@@ -71,9 +71,51 @@ export function planFetch(base = fetch, name = 'Assistant') {
|
|
|
71
71
|
status: res.status,
|
|
72
72
|
body: (await res.clone().text()).slice(0, 500),
|
|
73
73
|
}));
|
|
74
|
-
|
|
74
|
+
if (!res.ok || !res.body)
|
|
75
|
+
return res;
|
|
76
|
+
// The plan's own usage numbers, as it sent them (cache reads included), one line a call.
|
|
77
|
+
const [mine, theirs] = res.body.tee();
|
|
78
|
+
void logPlanUsage(theirs, String(body.model ?? ''), typeof body.prompt_cache_key === 'string');
|
|
79
|
+
const seen = new Response(mine, {
|
|
80
|
+
status: res.status,
|
|
81
|
+
statusText: res.statusText,
|
|
82
|
+
headers: res.headers,
|
|
83
|
+
});
|
|
84
|
+
return streamed ? seen : whole(seen, init?.signal);
|
|
75
85
|
};
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Reads a plan's streamed answer for its final usage and logs it as sent (`assistant_plan_usage`):
|
|
89
|
+
* whether a plan reads its prompt from the cache shows in its own numbers. Never throws.
|
|
90
|
+
*/
|
|
91
|
+
async function logPlanUsage(stream, model, keyed) {
|
|
92
|
+
try {
|
|
93
|
+
const reader = stream.getReader();
|
|
94
|
+
const text = new TextDecoder();
|
|
95
|
+
let buf = '';
|
|
96
|
+
let usage;
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
buf += text.decode(value, { stream: true });
|
|
102
|
+
const lines = buf.split('\n');
|
|
103
|
+
buf = lines.pop() ?? '';
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (!line.startsWith('data:') || !line.includes('"usage"'))
|
|
106
|
+
continue;
|
|
107
|
+
const e = JSON.parse(line.slice(5));
|
|
108
|
+
if (e.type === 'response.completed' || e.type === 'response.incomplete')
|
|
109
|
+
usage = e.response?.usage;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (usage)
|
|
113
|
+
console.log(JSON.stringify({ event: 'assistant_plan_usage', model, keyed, usage }));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// A log line is never worth a failed answer.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
77
119
|
/**
|
|
78
120
|
* A streamed Responses answer as the one JSON response a request that was not streamed expects:
|
|
79
121
|
* the final `response.completed` (or `response.incomplete`), with the items from each
|
|
@@ -178,14 +220,30 @@ export function cachedTurn(instructions, messages) {
|
|
|
178
220
|
};
|
|
179
221
|
}
|
|
180
222
|
/**
|
|
181
|
-
* OpenAI's prompt cache key
|
|
182
|
-
*
|
|
183
|
-
*
|
|
223
|
+
* OpenAI's prompt cache key: calls with the same key go to the same cache, so a long prompt's start
|
|
224
|
+
* is read from it instead of paid again. A ChatGPT plan sign-in caches nothing without one. At most
|
|
225
|
+
* 64 characters.
|
|
184
226
|
*/
|
|
185
227
|
export const promptCacheKey = (key) => key.slice(0, 64);
|
|
228
|
+
/**
|
|
229
|
+
* A cache key named for what a prompt starts with (its model, instructions and tools), not for
|
|
230
|
+
* one chat or run: a new chat or run that starts the same way reads that start from the cache
|
|
231
|
+
* (a key per chat read 0 of a new chat's 5.9k-token start; one key for the start read 99.7%, our
|
|
232
|
+
* key, 2026-11-14). The key only routes; what is read is still only the exact same start.
|
|
233
|
+
*/
|
|
234
|
+
export function prefixCacheKey(...parts) {
|
|
235
|
+
const of = parts.filter(Boolean).join('\u0000');
|
|
236
|
+
let h = 0x811c9dc5;
|
|
237
|
+
let g = 0x01000193;
|
|
238
|
+
for (let i = 0; i < of.length; i++) {
|
|
239
|
+
h = Math.imul(h ^ of.charCodeAt(i), 0x01000193);
|
|
240
|
+
g = Math.imul(g ^ of.charCodeAt(i), 0x85ebca6b);
|
|
241
|
+
}
|
|
242
|
+
return `lumen-${(h >>> 0).toString(36)}${(g >>> 0).toString(36)}`;
|
|
243
|
+
}
|
|
186
244
|
/**
|
|
187
245
|
* A tier's effort and output cap as a turn's settings, with the prompt cache on: Claude's
|
|
188
|
-
* breakpoint, and OpenAI's cache key when `cacheKey`
|
|
246
|
+
* breakpoint, and OpenAI's cache key (`prefixCacheKey`) when `cacheKey` is given.
|
|
189
247
|
*/
|
|
190
248
|
export function turnSettings(model, o) {
|
|
191
249
|
const out = o.plan || !o.out ? {} : { maxOutputTokens: o.out };
|
|
@@ -278,6 +278,8 @@ export interface AgentTurnInput {
|
|
|
278
278
|
tier?: string;
|
|
279
279
|
/** The browser's IANA zone, for the bearings. */
|
|
280
280
|
time_zone?: string;
|
|
281
|
+
/** How many images went with the message, counted in the chat's usage (`UsageEntry.images`). */
|
|
282
|
+
image_count?: number;
|
|
281
283
|
/** When the chat's last message was, for "since you last asked". */
|
|
282
284
|
last_at?: number;
|
|
283
285
|
/**
|
package/dist/assistant/engine.js
CHANGED
|
@@ -66,7 +66,7 @@ function claudeHistory(history) {
|
|
|
66
66
|
};
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
/** OpenAI's prompt cache key
|
|
69
|
+
/** OpenAI's prompt cache key: a short, stable hash of `of` (how the prompt starts). */
|
|
70
70
|
function cacheKey(of) {
|
|
71
71
|
let h = 0x811c9dc5;
|
|
72
72
|
let g = 0x01000193;
|
|
@@ -549,6 +549,9 @@ export class Assistant {
|
|
|
549
549
|
id: `chat:${input.chat}`,
|
|
550
550
|
kind: 'chat',
|
|
551
551
|
title: message.split('\n')[0] || 'Chat',
|
|
552
|
+
turns: 1,
|
|
553
|
+
images: input.image_count ?? 0,
|
|
554
|
+
refs: input.refs?.length ?? 0,
|
|
552
555
|
own_usd: ownKeyUsd(paying, onPayer),
|
|
553
556
|
own_plan_calls: ownPlanCalls(paying, onPayer),
|
|
554
557
|
own_plan_usd: ownPlanUsd(paying, onPayer),
|
|
@@ -1276,7 +1279,10 @@ export class Assistant {
|
|
|
1276
1279
|
try {
|
|
1277
1280
|
if ((s.model.provider === 'openai' && clients.openai) ||
|
|
1278
1281
|
(s.model.provider === 'xai' && clients.xai)) {
|
|
1279
|
-
({ text, cut } = await this.openaiOnce(clients, s, o.system, parts, max, o.signal, o.images, o.effort,
|
|
1282
|
+
({ text, cut } = await this.openaiOnce(clients, s, o.system, parts, max, o.signal, o.images, o.effort,
|
|
1283
|
+
// Named for how the prompt starts (the person, task and instructions), not the run, so
|
|
1284
|
+
// the next run of the same task reads that start from the cache.
|
|
1285
|
+
cacheKey(`${caller.id}:${o.task}:${s.model.id}:${o.system ?? ''}`), o.history));
|
|
1280
1286
|
}
|
|
1281
1287
|
else if (clients.anthropic) {
|
|
1282
1288
|
// Streamed and read whole: a long answer (minutes at a high cap) keeps its connection alive,
|
|
@@ -316,6 +316,11 @@ export declare class State {
|
|
|
316
316
|
kind: UsageEntry['kind'];
|
|
317
317
|
task?: string;
|
|
318
318
|
title: string;
|
|
319
|
+
/** Messages the person sent with this use (1 a chat turn; a run's calls send none). */
|
|
320
|
+
turns?: number;
|
|
321
|
+
/** Images and picks (refs) that went with those messages. */
|
|
322
|
+
images?: number;
|
|
323
|
+
refs?: number;
|
|
319
324
|
own_usd?: number;
|
|
320
325
|
own_plan_calls?: number;
|
|
321
326
|
own_plan_usd?: number;
|
package/dist/assistant/state.js
CHANGED
|
@@ -429,6 +429,9 @@ export class State {
|
|
|
429
429
|
const own = (prev?.own_usd ?? 0) + (e.own_usd ?? 0);
|
|
430
430
|
const plan = (prev?.own_plan_calls ?? 0) + (e.own_plan_calls ?? 0);
|
|
431
431
|
const planUsd = (prev?.own_plan_usd ?? 0) + (e.own_plan_usd ?? 0);
|
|
432
|
+
const turns = (prev?.turns ?? 0) + (e.turns ?? 0);
|
|
433
|
+
const images = (prev?.images ?? 0) + (e.images ?? 0);
|
|
434
|
+
const refs = (prev?.refs ?? 0) + (e.refs ?? 0);
|
|
432
435
|
entries.unshift({
|
|
433
436
|
id: e.id,
|
|
434
437
|
kind: e.kind,
|
|
@@ -444,6 +447,9 @@ export class State {
|
|
|
444
447
|
...(own > 0 ? { own_usd: own } : {}),
|
|
445
448
|
...(plan > 0 ? { own_plan_calls: plan } : {}),
|
|
446
449
|
...(planUsd > 0 ? { own_plan_usd: planUsd } : {}),
|
|
450
|
+
...(turns > 0 ? { turns } : {}),
|
|
451
|
+
...(images > 0 ? { images } : {}),
|
|
452
|
+
...(refs > 0 ? { refs } : {}),
|
|
447
453
|
parts: merged,
|
|
448
454
|
});
|
|
449
455
|
return { entries: entries.slice(0, USAGE_KEPT) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.278",
|
|
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",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"@anthropic-ai/sdk": "0.133.0",
|
|
58
58
|
"@cloudflare/computer": "0.8.0",
|
|
59
59
|
"@cloudflare/think": "0.23.0",
|
|
60
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
61
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
60
|
+
"@columenlabs/lumen-ops": "0.1.278",
|
|
61
|
+
"@columenlabs/lumen-ui-types": "0.1.278",
|
|
62
62
|
"agents": "0.32.0",
|
|
63
63
|
"ai": "7.0.162",
|
|
64
64
|
"just-bash": "3.6.0",
|