@columenlabs/lumen-ai 0.1.265 → 0.1.267
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 +1 -1
- package/dist/agent/agent.d.ts +2 -20
- package/dist/agent/agent.js +43 -47
- package/dist/assistant/engine.js +28 -10
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ The panel's composer lists what can follow three keys as they are typed. The kit
|
|
|
162
162
|
|
|
163
163
|
### Bearings
|
|
164
164
|
|
|
165
|
-
Each message the model gets starts with a few lines of bearings: the time now in the person's zone (the panel sends the browser's), UTC, how long since the chat's last message and the app's name. They go in the message, not the system prompt, so the prompt cache holds.
|
|
165
|
+
Each message the model gets starts with a few lines of bearings: the time now in the person's zone (the panel sends the browser's), UTC, how long since the chat's last message and the app's name. They go in the message, not the system prompt, so the prompt cache holds. A turn's lead (bearings, `plan.lead`, news) goes before the newest message only, so the chat never grows by it; a chat's OpenAI calls carry the chat's id as their cache key. `/compact` in the panel summarizes the chat's older part at once (as past `compactAt()`), without asking the model. An app adds its own lines with `bearings`; each call gets 300 ms, and when it is late or throws its lines are left out.
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
168
|
createAssistant({
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -123,28 +123,10 @@ export interface AgentCard {
|
|
|
123
123
|
export declare const COMPACT_AT = 150000;
|
|
124
124
|
/** What the model is told of cards settled and changes undone since its last answer. */
|
|
125
125
|
export declare function cardNews(cards: AgentCard[]): string;
|
|
126
|
+
/** The panel's command that compacts the chat now (the kit's slash command). */
|
|
127
|
+
export declare const COMPACT_COMMAND = "/compact";
|
|
126
128
|
/** The messages with `lead` put before the newest message the person wrote. */
|
|
127
129
|
export declare function withLead(messages: ModelMessage[], lead: string | undefined): ModelMessage[];
|
|
128
|
-
/** A lead a past turn put before one of the person's messages, kept so later turns send it again. */
|
|
129
|
-
export interface KeptLead {
|
|
130
|
-
/** The message's place among the person's messages and a hash of what it says. */
|
|
131
|
-
k: string;
|
|
132
|
-
lead: string;
|
|
133
|
-
}
|
|
134
|
-
/** Leads kept for the newest messages; older ones fall out of the chat's prompt cache anyway. */
|
|
135
|
-
export declare const LEADS_KEPT = 40;
|
|
136
|
-
/**
|
|
137
|
-
* The messages with this turn's `lead` before the newest message the person wrote, and every lead
|
|
138
|
-
* an earlier turn put before an earlier message put back where it was. A lead that is sent once
|
|
139
|
-
* and then dropped changes the chat's beginning on the next turn, so the provider's prompt cache
|
|
140
|
-
* misses from that message on and the whole earlier turn (its tool calls and results) is paid for
|
|
141
|
-
* again at the full price. Sent again, every turn's prompt starts with the last turn's prompt.
|
|
142
|
-
* Returns the leads to keep (this one included), newest last.
|
|
143
|
-
*/
|
|
144
|
-
export declare function withLeads(messages: ModelMessage[], lead: string | undefined, kept: readonly KeptLead[]): {
|
|
145
|
-
messages: ModelMessage[];
|
|
146
|
-
kept: KeptLead[];
|
|
147
|
-
};
|
|
148
130
|
/** One message the person sent, with the page, refs and tier the composer added. */
|
|
149
131
|
export interface AskInput {
|
|
150
132
|
message: string;
|
package/dist/agent/agent.js
CHANGED
|
@@ -58,6 +58,8 @@ const cardOf = (r) => ({
|
|
|
58
58
|
...(r.error ? { error: r.error } : {}),
|
|
59
59
|
at: r.at,
|
|
60
60
|
});
|
|
61
|
+
/** The panel's command that compacts the chat now (the kit's slash command). */
|
|
62
|
+
export const COMPACT_COMMAND = '/compact';
|
|
61
63
|
/** The messages with `lead` put before the newest message the person wrote. */
|
|
62
64
|
export function withLead(messages, lead) {
|
|
63
65
|
if (!lead)
|
|
@@ -77,50 +79,6 @@ function leadAt(messages, i, lead) {
|
|
|
77
79
|
: [{ type: 'text', text: lead }, ...m.content];
|
|
78
80
|
return [...messages.slice(0, i), { ...m, content }, ...messages.slice(i + 1)];
|
|
79
81
|
}
|
|
80
|
-
/** Leads kept for the newest messages; older ones fall out of the chat's prompt cache anyway. */
|
|
81
|
-
export const LEADS_KEPT = 40;
|
|
82
|
-
/** A short, stable hash of a message's content (FNV-1a), to know the same message again. */
|
|
83
|
-
function contentKey(n, m) {
|
|
84
|
-
const text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
85
|
-
let h = 0x811c9dc5;
|
|
86
|
-
for (let i = 0; i < text.length; i++) {
|
|
87
|
-
h ^= text.charCodeAt(i);
|
|
88
|
-
h = Math.imul(h, 0x01000193);
|
|
89
|
-
}
|
|
90
|
-
return `${n}:${(h >>> 0).toString(36)}:${text.length}`;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The messages with this turn's `lead` before the newest message the person wrote, and every lead
|
|
94
|
-
* an earlier turn put before an earlier message put back where it was. A lead that is sent once
|
|
95
|
-
* and then dropped changes the chat's beginning on the next turn, so the provider's prompt cache
|
|
96
|
-
* misses from that message on and the whole earlier turn (its tool calls and results) is paid for
|
|
97
|
-
* again at the full price. Sent again, every turn's prompt starts with the last turn's prompt.
|
|
98
|
-
* Returns the leads to keep (this one included), newest last.
|
|
99
|
-
*/
|
|
100
|
-
export function withLeads(messages, lead, kept) {
|
|
101
|
-
const users = [];
|
|
102
|
-
messages.forEach((m, i) => {
|
|
103
|
-
if (m.role === 'user')
|
|
104
|
-
users.push(i);
|
|
105
|
-
});
|
|
106
|
-
const newest = users.at(-1);
|
|
107
|
-
const byKey = new Map(kept.map((l) => [l.k, l.lead]));
|
|
108
|
-
let out = messages;
|
|
109
|
-
users.forEach((at, n) => {
|
|
110
|
-
if (at === newest)
|
|
111
|
-
return;
|
|
112
|
-
const l = byKey.get(contentKey(n, messages[at]));
|
|
113
|
-
if (l)
|
|
114
|
-
out = leadAt(out, at, l);
|
|
115
|
-
});
|
|
116
|
-
if (newest === undefined || !lead)
|
|
117
|
-
return { messages: out, kept: [...kept] };
|
|
118
|
-
const k = contentKey(users.length - 1, messages[newest]);
|
|
119
|
-
return {
|
|
120
|
-
messages: leadAt(out, newest, lead),
|
|
121
|
-
kept: [...kept.filter((l) => l.k !== k), { k, lead }].slice(-LEADS_KEPT),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
82
|
/** A quote as sent, checked: its text cut to `ASSISTANT_QUOTE_MAX`, and whose message it came from. */
|
|
125
83
|
export function quoteOf(q) {
|
|
126
84
|
const o = q;
|
|
@@ -356,6 +314,11 @@ export class LumenAgent extends Think {
|
|
|
356
314
|
}
|
|
357
315
|
this.#put('at', at);
|
|
358
316
|
await sink.event({ event: 'chat', data: { id: this.name, title } });
|
|
317
|
+
if (input.message.trim() === COMPACT_COMMAND) {
|
|
318
|
+
await this.#compactNow(input, sink);
|
|
319
|
+
await sink.event({ event: 'done', data: { id: this.name, usage: this.usage() } });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
359
322
|
// New work in a long chat: the panel asks first, and the message is not kept or answered here.
|
|
360
323
|
if (!input.stay && (await this.offerFresh(input).catch(() => false))) {
|
|
361
324
|
await sink.event({ event: 'fresh', data: {} });
|
|
@@ -629,6 +592,39 @@ export class LumenAgent extends Think {
|
|
|
629
592
|
.onCompaction(createCompactFunction({ summarize: (prompt) => this.#summarize(prompt) }))
|
|
630
593
|
.compactAfter(this.compactAt());
|
|
631
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* /compact: the chat's older part summarized now, as Think does past `compactAt()`, on the last
|
|
597
|
+
* turn's model and key (planned afresh when this object has not answered yet). Never a message
|
|
598
|
+
* to the model, and nothing to answer.
|
|
599
|
+
*/
|
|
600
|
+
async #compactNow(input, sink) {
|
|
601
|
+
try {
|
|
602
|
+
if (!this.#last) {
|
|
603
|
+
this.#input = { ...input, message: '' };
|
|
604
|
+
try {
|
|
605
|
+
this.#last = { plan: await this.plan({ messages: [] }) };
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
this.#input = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const r = await this.session.compact();
|
|
612
|
+
if (r)
|
|
613
|
+
await sink.event({ event: 'compacted', data: {} });
|
|
614
|
+
else
|
|
615
|
+
await sink.event({
|
|
616
|
+
event: 'text',
|
|
617
|
+
data: { delta: 'This chat is already short: nothing to compact yet.' },
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
console.error(JSON.stringify({ event: 'assistant_compact_failed', error: String(err) }));
|
|
622
|
+
await sink.event({
|
|
623
|
+
event: 'error',
|
|
624
|
+
data: { message: 'The chat could not be compacted. Try again, or start a new chat.' },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
632
628
|
async #summarize(prompt) {
|
|
633
629
|
const last = this.#last;
|
|
634
630
|
if (!last)
|
|
@@ -760,9 +756,9 @@ export class LumenAgent extends Think {
|
|
|
760
756
|
this.#turn.lazy = lazy;
|
|
761
757
|
}
|
|
762
758
|
const turn = this.#turn;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const cached = cachedTurn(system,
|
|
759
|
+
// The lead goes before the newest message only. Kept on every earlier message it grew the chat
|
|
760
|
+
// by a whole lead (an app's page, often thousands of tokens) each turn.
|
|
761
|
+
const cached = cachedTurn(system, withLead(ctx.messages, `${this.#news()}${this.#screenNews()}${plan.lead ?? ''}`));
|
|
766
762
|
return {
|
|
767
763
|
model: this.#model(plan, () => {
|
|
768
764
|
const fb = plan.fallback;
|
package/dist/assistant/engine.js
CHANGED
|
@@ -100,14 +100,28 @@ function shownError(err) {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* A plain sentence for an error OpenAI sends inside the answer's stream (an `error` event, or a
|
|
103
|
-
* failed response): what happened and what to do, by its code
|
|
104
|
-
*
|
|
103
|
+
* failed response): what happened and what to do, by its code, with OpenAI's code in brackets so
|
|
104
|
+
* the person and the logs see the real reason. A plan's usage limit and a per-minute rate limit are
|
|
105
|
+
* different things: the first waits for the plan to reset, the second only a minute. OpenAI's own
|
|
106
|
+
* message is logged (one `assistant_upstream_error` line), never shown.
|
|
105
107
|
*/
|
|
106
|
-
function openaiStreamError(
|
|
107
|
-
|
|
108
|
+
function openaiStreamError(er, plan) {
|
|
109
|
+
const code = String(er.code || er.type || '');
|
|
110
|
+
console.error(JSON.stringify({
|
|
111
|
+
event: 'assistant_upstream_error',
|
|
112
|
+
provider: 'openai',
|
|
113
|
+
plan,
|
|
114
|
+
code: er.code ?? null,
|
|
115
|
+
type: er.type ?? null,
|
|
116
|
+
message: typeof er.message === 'string' ? er.message.slice(0, 500) : null,
|
|
117
|
+
}));
|
|
118
|
+
const tag = code ? ` (${code})` : '';
|
|
119
|
+
if (/usage_limit|usage_unavailable|insufficient_quota/.test(code))
|
|
108
120
|
return plan
|
|
109
|
-
?
|
|
110
|
-
:
|
|
121
|
+
? `Your ChatGPT plan reached its usage limit for now${tag}: try again later, or pick another model.`
|
|
122
|
+
: `OpenAI says the key is out of quota${tag}. Add credit to the key, or pick another model.`;
|
|
123
|
+
if (/rate_limit/.test(code))
|
|
124
|
+
return `OpenAI is rate limiting these requests${tag}: try again in a minute.`;
|
|
111
125
|
if (/context_length|too_long|max_tokens/.test(code))
|
|
112
126
|
return 'This chat is too long for the model. Start a new chat, or type /compact.';
|
|
113
127
|
if (/model_not_found|model_not_supported|invalid_model|unsupported_model/.test(code))
|
|
@@ -115,7 +129,7 @@ function openaiStreamError(code, plan) {
|
|
|
115
129
|
? 'This model is not on your ChatGPT plan. Pick another model for this tier in Settings.'
|
|
116
130
|
: 'OpenAI does not offer this model to the key. Pick another model for this tier in Settings.';
|
|
117
131
|
if (/server_error|overloaded|timeout|unavailable/.test(code))
|
|
118
|
-
return
|
|
132
|
+
return `OpenAI is overloaded${tag}. Try again shortly.`;
|
|
119
133
|
if (/content_policy|safety|refus/.test(code))
|
|
120
134
|
return 'OpenAI declined to answer this message.';
|
|
121
135
|
return `OpenAI could not answer (${code || 'no reason given'}). It is logged; try again or pick another model.`;
|
|
@@ -1073,7 +1087,7 @@ export class Assistant {
|
|
|
1073
1087
|
done = e.response;
|
|
1074
1088
|
else if (e.type === 'error') {
|
|
1075
1089
|
const er = (e.error ?? e);
|
|
1076
|
-
throw new AssistantError(openaiStreamError(er
|
|
1090
|
+
throw new AssistantError(openaiStreamError(er, plan), 'upstream');
|
|
1077
1091
|
}
|
|
1078
1092
|
}
|
|
1079
1093
|
}
|
|
@@ -1081,12 +1095,16 @@ export class Assistant {
|
|
|
1081
1095
|
// The SDK throws a stream's `error` event as an APIError with no HTTP status.
|
|
1082
1096
|
if (err instanceof OpenAI.APIError && err.status === undefined) {
|
|
1083
1097
|
const er = (err.error ?? {});
|
|
1084
|
-
throw new AssistantError(openaiStreamError(
|
|
1098
|
+
throw new AssistantError(openaiStreamError({
|
|
1099
|
+
...er,
|
|
1100
|
+
code: er.code || er.type || err.code || '',
|
|
1101
|
+
message: er.message ?? err.message,
|
|
1102
|
+
}, plan), 'upstream');
|
|
1085
1103
|
}
|
|
1086
1104
|
throw err;
|
|
1087
1105
|
}
|
|
1088
1106
|
if (done?.status === 'failed')
|
|
1089
|
-
throw new AssistantError(openaiStreamError(done.error
|
|
1107
|
+
throw new AssistantError(openaiStreamError(done.error ?? {}, plan), 'upstream');
|
|
1090
1108
|
const u = done?.usage;
|
|
1091
1109
|
if (u) {
|
|
1092
1110
|
const read = u.input_tokens_details?.cached_tokens ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.267",
|
|
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",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@ai-sdk/openai": "4.0.108",
|
|
57
57
|
"@anthropic-ai/sdk": "0.133.0",
|
|
58
58
|
"@cloudflare/think": "0.23.0",
|
|
59
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
60
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
59
|
+
"@columenlabs/lumen-ops": "0.1.267",
|
|
60
|
+
"@columenlabs/lumen-ui-types": "0.1.267",
|
|
61
61
|
"agents": "0.32.0",
|
|
62
62
|
"ai": "7.0.162",
|
|
63
63
|
"just-bash": "3.6.0",
|