@columenlabs/lumen-ai 0.1.215 → 0.1.216
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 -0
- package/dist/agent/agent.d.ts +15 -0
- package/dist/agent/agent.js +77 -4
- package/dist/agent/stream.d.ts +8 -1
- package/dist/agent/stream.js +18 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -241,6 +241,8 @@ Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agen
|
|
|
241
241
|
|
|
242
242
|
**The panel.** `LumenAssistant` reads a Think chat with no change. `ask(input, sink)` runs one answer and sends the panel's own events (`AssistantEvent`: the chat, the text, each tool line, cards, hand-off sections, advice, the use) to `sink`. The app's `POST /chat` returns `askResponse((sink) => chat.ask(input, sink), { signal, stop: () => chat.stop() })`, the same server-sent events `assistantRoutes` sends. `view()` gives a stored chat as the panel's items, and `usage()` gives the chat's use so far. A card's id is its tool call's, so the panel's Save, Dismiss and Undo go to `saveCard`, `dismissCard` and `undoCard`. The app keeps the list of chats, and each chat is its own agent, such as a facet of the person's store. `chunkEvents` and `itemsOf` are the mapping, for an app that streams some other way.
|
|
243
243
|
|
|
244
|
+
**Steer and queue.** While an answer runs, the panel's box stays open: Enter steers the message into the answer (`steer(message)` on the agent; the app routes `POST /chats/:id/steer` `{ message }` to it and answers `{ steered }`), Alt+Enter queues it for after. A steer joins at the answer's next step, after the tool it is on, with `STEER_NOTE` before it for the model; the panel shows it there marked Steered (event `steered`), and `view()` keeps it in place (`SteerNote`, under the message the answer is to). A steer the answer ends before reading comes back as `unsteered` and goes first in the queue. `plan.steer: false` turns it off for a model; an app without the route (404) shows Queue only. Queued messages each go as their own message when the answer before them ends; each has Steer now, Edit and Remove.
|
|
245
|
+
|
|
244
246
|
```ts
|
|
245
247
|
class Chat extends LumenAgent<Env> {
|
|
246
248
|
#settle?: (end: AgentTurnEnd) => Promise<void>;
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -76,6 +76,11 @@ export interface AgentTurnPlan {
|
|
|
76
76
|
screen?: boolean;
|
|
77
77
|
/** Most model calls in one answer; 8. */
|
|
78
78
|
maxSteps?: number;
|
|
79
|
+
/**
|
|
80
|
+
* A message sent while the answer runs joins it at its next step (Steer); on unless `false`,
|
|
81
|
+
* for a model that should not read a new message between its calls. Refused steers are queued.
|
|
82
|
+
*/
|
|
83
|
+
steer?: boolean;
|
|
79
84
|
/** Fetch for provider calls (a metered one). */
|
|
80
85
|
fetch?: typeof fetch;
|
|
81
86
|
tools?: Omit<AiToolsOptions, 'maxChars'>;
|
|
@@ -94,6 +99,8 @@ export interface AgentTurnEnd {
|
|
|
94
99
|
capped: boolean;
|
|
95
100
|
}
|
|
96
101
|
export declare const CAP_NOTE = "The spend cap for this month is reached: answer now from what you have, in a sentence or two. You can call no more tools.";
|
|
102
|
+
/** Put before a message the person steered into a running answer, for the model only. */
|
|
103
|
+
export declare const STEER_NOTE = "The person sent this while you were working. Take it into account from here on; it does not cancel what you were asked before unless it says so.";
|
|
97
104
|
/** A card in the chat: a draft the person saves or dismisses, or a write made at once (Undo). */
|
|
98
105
|
export interface AgentCard {
|
|
99
106
|
id: string;
|
|
@@ -165,6 +172,14 @@ export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
|
|
|
165
172
|
view(id?: string): Promise<AssistantChat>;
|
|
166
173
|
/** Stops the answer running now; what was said so far is kept. */
|
|
167
174
|
stop(): void;
|
|
175
|
+
/**
|
|
176
|
+
* A message for the answer running now: it joins at the answer's next step (after the tool it
|
|
177
|
+
* is on), shown there as steered. `steered: false` when no answer runs or its plan turned steer
|
|
178
|
+
* off: the panel queues it. One the answer ends without reading comes back as `unsteered`.
|
|
179
|
+
*/
|
|
180
|
+
steer(message: string): {
|
|
181
|
+
steered: boolean;
|
|
182
|
+
};
|
|
168
183
|
/**
|
|
169
184
|
* One answer to `input`, its events sent to `sink` in the panel's words: the chat's title, the
|
|
170
185
|
* text, each tool line and card, then its use. The app's route calls it over RPC.
|
package/dist/agent/agent.js
CHANGED
|
@@ -24,6 +24,8 @@ import { chunkEvents, itemsOf } from './stream.js';
|
|
|
24
24
|
import { openAiTools, } from './tools.js';
|
|
25
25
|
import { UsageMeter } from './usage.js';
|
|
26
26
|
export const CAP_NOTE = 'The spend cap for this month is reached: answer now from what you have, in a sentence or two. You can call no more tools.';
|
|
27
|
+
/** Put before a message the person steered into a running answer, for the model only. */
|
|
28
|
+
export const STEER_NOTE = 'The person sent this while you were working. Take it into account from here on; it does not cancel what you were asked before unless it says so.';
|
|
27
29
|
/** Past this many tokens in the chat Think summarizes its older part. */
|
|
28
30
|
export const COMPACT_AT = 150_000;
|
|
29
31
|
const CARD_NOTE = "A card is on the person's screen; nothing has changed yet. Say what it will do when they save it. The next message tells you how it ended.";
|
|
@@ -129,6 +131,14 @@ export class LumenAgent extends Think {
|
|
|
129
131
|
includeMcpTools = false;
|
|
130
132
|
#turn = null;
|
|
131
133
|
#input = null;
|
|
134
|
+
/** The answer's events go here while it runs; a steer's line goes here too. */
|
|
135
|
+
#sink = null;
|
|
136
|
+
/** The person's message this answer is to, by id: where its steers are kept. */
|
|
137
|
+
#asked = null;
|
|
138
|
+
/** Steered messages waiting for the next step. */
|
|
139
|
+
#steers = [];
|
|
140
|
+
/** Steered messages read this answer: where they sit in the step's messages, and their words. */
|
|
141
|
+
#steered = [];
|
|
132
142
|
#metaTabled = false;
|
|
133
143
|
/** What the person sent with the message this turn answers (page, refs, tier, zone). */
|
|
134
144
|
get input() {
|
|
@@ -159,7 +169,7 @@ export class LumenAgent extends Think {
|
|
|
159
169
|
id,
|
|
160
170
|
title: this.#get('title') ?? '',
|
|
161
171
|
at: this.#get('at') ?? 0,
|
|
162
|
-
items: itemsOf(await this.getMessages(), this.cards(), this.#screens()),
|
|
172
|
+
items: itemsOf(await this.getMessages(), this.cards(), this.#screens(), this.#get('steers') ?? {}),
|
|
163
173
|
usage: this.usage(),
|
|
164
174
|
};
|
|
165
175
|
}
|
|
@@ -167,6 +177,19 @@ export class LumenAgent extends Think {
|
|
|
167
177
|
stop() {
|
|
168
178
|
this.cancelAllChats();
|
|
169
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* A message for the answer running now: it joins at the answer's next step (after the tool it
|
|
182
|
+
* is on), shown there as steered. `steered: false` when no answer runs or its plan turned steer
|
|
183
|
+
* off: the panel queues it. One the answer ends without reading comes back as `unsteered`.
|
|
184
|
+
*/
|
|
185
|
+
steer(message) {
|
|
186
|
+
const text = message.trim();
|
|
187
|
+
const t = this.#turn;
|
|
188
|
+
if (!text || !t || !this.#sink || t.plan.steer === false)
|
|
189
|
+
return { steered: false };
|
|
190
|
+
this.#steers.push({ text: text.slice(0, 20_000), at: Date.now() });
|
|
191
|
+
return { steered: true };
|
|
192
|
+
}
|
|
170
193
|
/**
|
|
171
194
|
* One answer to `input`, its events sent to `sink` in the panel's words: the chat's title, the
|
|
172
195
|
* text, each tool line and card, then its use. The app's route calls it over RPC.
|
|
@@ -195,6 +218,11 @@ export class LumenAgent extends Think {
|
|
|
195
218
|
kindOf: (n) => this.#turn?.tools.info.find((i) => i.name === n)?.kind,
|
|
196
219
|
});
|
|
197
220
|
const map = screenMapOf(input.screen);
|
|
221
|
+
const userId = `u_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
222
|
+
this.#sink = sink;
|
|
223
|
+
this.#asked = userId;
|
|
224
|
+
this.#steers = [];
|
|
225
|
+
this.#steered = [];
|
|
198
226
|
// The images go in the message's parts; the turn's plan reads the rest.
|
|
199
227
|
const { images: _images, ...asked } = input;
|
|
200
228
|
this.#input = { ...asked, ...(map ? { screen: map } : {}) };
|
|
@@ -203,7 +231,7 @@ export class LumenAgent extends Think {
|
|
|
203
231
|
let failed = false;
|
|
204
232
|
try {
|
|
205
233
|
await this.chat({
|
|
206
|
-
id:
|
|
234
|
+
id: userId,
|
|
207
235
|
role: 'user',
|
|
208
236
|
parts: [{ type: 'text', text: input.message }, ...imageParts(input.images)],
|
|
209
237
|
metadata: meta,
|
|
@@ -236,7 +264,14 @@ export class LumenAgent extends Think {
|
|
|
236
264
|
}
|
|
237
265
|
finally {
|
|
238
266
|
this.#input = null;
|
|
267
|
+
this.#sink = null;
|
|
268
|
+
this.#asked = null;
|
|
269
|
+
this.#steered = [];
|
|
239
270
|
}
|
|
271
|
+
// Steers the answer ended before reading go back to the panel, first in its queue.
|
|
272
|
+
const unread = this.#steers.splice(0).map((s) => s.text);
|
|
273
|
+
if (unread.length)
|
|
274
|
+
await sink.event({ event: 'unsteered', data: { texts: unread } });
|
|
240
275
|
if (this.#capped)
|
|
241
276
|
await sink.event({ event: 'stopped', data: { reason: 'cap' } });
|
|
242
277
|
await sink.event({ event: 'done', data: { id: this.name, usage: this.usage() } });
|
|
@@ -573,20 +608,58 @@ export class LumenAgent extends Think {
|
|
|
573
608
|
const t = this.#turn;
|
|
574
609
|
if (!t)
|
|
575
610
|
return undefined;
|
|
611
|
+
const steered = this.#steerInto(ctx);
|
|
612
|
+
const messages = steered ? { messages: steered } : {};
|
|
576
613
|
// The SDK's step result is a union that Omit narrows to shared keys; these exist on it.
|
|
577
614
|
if (t.plan.left !== undefined && this.#spent() >= t.plan.left) {
|
|
578
615
|
t.capped = true;
|
|
579
|
-
|
|
616
|
+
const capped = {
|
|
617
|
+
...messages,
|
|
618
|
+
activeTools: [],
|
|
619
|
+
instructions: `${t.system}\n\n${CAP_NOTE}`,
|
|
620
|
+
};
|
|
621
|
+
return capped;
|
|
580
622
|
}
|
|
581
623
|
// A forced first call is for the first step only; tools a search found join each step.
|
|
582
624
|
const auto = t.plan.first && ctx.stepNumber > 0;
|
|
583
|
-
if (!auto && !t.lazy)
|
|
625
|
+
if (!auto && !t.lazy && !steered)
|
|
584
626
|
return undefined;
|
|
585
627
|
return {
|
|
586
628
|
...(auto ? { toolChoice: 'auto' } : {}),
|
|
587
629
|
...(t.lazy ? { activeTools: t.lazy.active() } : {}),
|
|
630
|
+
...messages,
|
|
588
631
|
};
|
|
589
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* The step's messages with every steer read so far in its place: waiting ones join at the end
|
|
635
|
+
* now (told to the panel and kept for the chat's history), earlier ones where they joined.
|
|
636
|
+
*/
|
|
637
|
+
#steerInto(ctx) {
|
|
638
|
+
const sink = this.#sink;
|
|
639
|
+
const asked = this.#asked;
|
|
640
|
+
if (this.#steers.length && sink && asked) {
|
|
641
|
+
const now = this.#steers.splice(0);
|
|
642
|
+
const kept = this.#get('steers') ?? {};
|
|
643
|
+
kept[asked] = [
|
|
644
|
+
...(kept[asked] ?? []),
|
|
645
|
+
...now.map((s) => ({ step: ctx.stepNumber, text: s.text, at: s.at })),
|
|
646
|
+
];
|
|
647
|
+
this.#put('steers', kept);
|
|
648
|
+
for (const s of now) {
|
|
649
|
+
this.#steered.push({ index: ctx.messages.length, text: s.text });
|
|
650
|
+
void Promise.resolve(sink.event({ event: 'steered', data: s })).catch(() => undefined);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!this.#steered.length)
|
|
654
|
+
return null;
|
|
655
|
+
const out = [...ctx.messages];
|
|
656
|
+
for (const s of [...this.#steered].reverse())
|
|
657
|
+
out.splice(s.index, 0, {
|
|
658
|
+
role: 'user',
|
|
659
|
+
content: [{ type: 'text', text: `${STEER_NOTE}\n\n${s.text}` }],
|
|
660
|
+
});
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
590
663
|
onStepFinish(ctx) {
|
|
591
664
|
const t = this.#turn;
|
|
592
665
|
if (!t)
|
package/dist/agent/stream.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare function chunkEvents(o?: {
|
|
|
25
25
|
}) => AssistantEvent[];
|
|
26
26
|
/** A stored UI message, as much of it as a chat's items need. */
|
|
27
27
|
interface StoredMessage {
|
|
28
|
+
id?: string;
|
|
28
29
|
role: string;
|
|
29
30
|
parts: {
|
|
30
31
|
type: string;
|
|
@@ -42,5 +43,11 @@ export interface AskMeta {
|
|
|
42
43
|
tier?: string;
|
|
43
44
|
}
|
|
44
45
|
/** A stored chat as the panel's items: the messages in order, each tool call as it ended. */
|
|
45
|
-
|
|
46
|
+
/** A message steered into an answer: the step it was read at, under the message the answer is to. */
|
|
47
|
+
export interface SteerNote {
|
|
48
|
+
step: number;
|
|
49
|
+
text: string;
|
|
50
|
+
at: number;
|
|
51
|
+
}
|
|
52
|
+
export declare function itemsOf(messages: StoredMessage[], cards: AgentCard[], screens?: Map<string, ScreenCall>, steers?: Record<string, SteerNote[]>): ChatItem[];
|
|
46
53
|
export {};
|
package/dist/agent/stream.js
CHANGED
|
@@ -127,12 +127,13 @@ export function chunkEvents(o = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
export function itemsOf(messages, cards, screens = new Map()) {
|
|
130
|
+
export function itemsOf(messages, cards, screens = new Map(), steers = {}) {
|
|
132
131
|
const byId = new Map(cards.map((c) => [c.id, c]));
|
|
133
132
|
const items = [];
|
|
133
|
+
let asked;
|
|
134
134
|
for (const m of messages) {
|
|
135
135
|
if (m.role === 'user') {
|
|
136
|
+
asked = m.id;
|
|
136
137
|
const text = m.parts
|
|
137
138
|
.filter((p) => p.type === 'text')
|
|
138
139
|
.map((p) => String(p.text ?? ''))
|
|
@@ -154,7 +155,21 @@ export function itemsOf(messages, cards, screens = new Map()) {
|
|
|
154
155
|
}
|
|
155
156
|
if (m.role !== 'assistant')
|
|
156
157
|
continue;
|
|
158
|
+
// Steered messages show where the answer read them: before the step they joined.
|
|
159
|
+
const steered = [...((asked && steers[asked]) || [])].sort((x, y) => x.step - y.step);
|
|
160
|
+
const steerTo = (step) => {
|
|
161
|
+
while (steered.length && steered[0].step <= step) {
|
|
162
|
+
const n = steered.shift();
|
|
163
|
+
items.push({ kind: 'user', text: n.text, at: n.at, steered: true });
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
let step = -1;
|
|
157
167
|
for (const p of m.parts) {
|
|
168
|
+
if (p.type === 'step-start') {
|
|
169
|
+
step += 1;
|
|
170
|
+
steerTo(step);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
158
173
|
if (p.type === 'text' && typeof p.text === 'string' && p.text) {
|
|
159
174
|
const last = items[items.length - 1];
|
|
160
175
|
if (last?.kind === 'text')
|
|
@@ -195,6 +210,7 @@ export function itemsOf(messages, cards, screens = new Map()) {
|
|
|
195
210
|
}
|
|
196
211
|
}
|
|
197
212
|
}
|
|
213
|
+
steerTo(Number.POSITIVE_INFINITY);
|
|
198
214
|
}
|
|
199
215
|
return items;
|
|
200
216
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.216",
|
|
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",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@ai-sdk/openai": "4.0.108",
|
|
53
53
|
"@anthropic-ai/sdk": "0.133.0",
|
|
54
54
|
"@cloudflare/think": "0.23.0",
|
|
55
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
56
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
55
|
+
"@columenlabs/lumen-ops": "0.1.216",
|
|
56
|
+
"@columenlabs/lumen-ui-types": "0.1.216",
|
|
57
57
|
"agents": "0.32.0",
|
|
58
58
|
"ai": "7.0.162",
|
|
59
59
|
"just-bash": "3.6.0",
|