@columenlabs/lumen-ai 0.1.181 → 0.1.183
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 -1
- package/dist/agent/agent.d.ts +31 -1
- package/dist/agent/agent.js +142 -3
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/respond.d.ts +10 -0
- package/dist/agent/respond.js +44 -0
- package/dist/agent/stream.d.ts +43 -0
- package/dist/agent/stream.js +180 -0
- package/dist/agent/tools.d.ts +2 -0
- package/dist/agent/tools.js +10 -3
- package/dist/assistant/engine.js +5 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -234,11 +234,14 @@ Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agen
|
|
|
234
234
|
|
|
235
235
|
**Lazy tools.** Past `LAZY_AT` (30) tools, a turn sends the model its core tools and `tool_search`, with one catalog line per group in the instructions. By default the core tools are the apps' finds, the hand off and the advisor. `plan.lazy` sets `{ at, core }`. A search matches the words in a tool's name and description and turns on up to 8 tools from the next step on.
|
|
236
236
|
|
|
237
|
+
**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.
|
|
238
|
+
|
|
237
239
|
```ts
|
|
238
240
|
class Chat extends LumenAgent<Env> {
|
|
239
241
|
#settle?: (end: AgentTurnEnd) => Promise<void>;
|
|
240
242
|
async plan(turn: TurnContext) {
|
|
241
|
-
|
|
243
|
+
// this.input: what the person sent (message, page, refs, tier, time zone).
|
|
244
|
+
const { plan, settle } = await assistant.agentTurn(caller, this.input ?? { message: '' });
|
|
242
245
|
this.#settle = settle;
|
|
243
246
|
return plan;
|
|
244
247
|
}
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* An `Assistant` gives both: `assistant.agentTurn(caller, input)` returns a plan and its settle.
|
|
13
13
|
*/
|
|
14
14
|
import { type ChatResponseResult, type PrepareStepContext, type Session, type StepConfig, type StepContext, Think, type ThinkModel, type TurnConfig, type TurnContext } from '@cloudflare/think';
|
|
15
|
-
import type { AssistantEffort, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
15
|
+
import type { AssistantChat, AssistantEffort, AssistantEvent, AssistantRefNote, ChatUsage, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
16
16
|
import { type ModelMessage, type ToolSet } from 'ai';
|
|
17
17
|
import type { ModelDef } from '../assistant/models.js';
|
|
18
18
|
import { type AgentConnector, type ConnectorAt } from './connectors.js';
|
|
@@ -102,6 +102,23 @@ export declare const COMPACT_AT = 150000;
|
|
|
102
102
|
export declare function cardNews(cards: AgentCard[]): string;
|
|
103
103
|
/** The messages with `lead` put before the newest message the person wrote. */
|
|
104
104
|
export declare function withLead(messages: ModelMessage[], lead: string | undefined): ModelMessage[];
|
|
105
|
+
/** One message the person sent, with the page, refs and tier the composer added. */
|
|
106
|
+
export interface AskInput {
|
|
107
|
+
message: string;
|
|
108
|
+
context?: {
|
|
109
|
+
label: string;
|
|
110
|
+
note?: unknown;
|
|
111
|
+
};
|
|
112
|
+
refs?: AssistantRefNote[];
|
|
113
|
+
tier?: string;
|
|
114
|
+
time_zone?: string;
|
|
115
|
+
}
|
|
116
|
+
/** Where an answer's events go: the app's route, over RPC. */
|
|
117
|
+
export interface AskSink {
|
|
118
|
+
event(e: AssistantEvent): void | Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
/** Usage parts added up: same label, one part. */
|
|
121
|
+
export declare function chatUsage(parts: UsagePart[], context?: number): ChatUsage;
|
|
105
122
|
export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
|
|
106
123
|
#private;
|
|
107
124
|
/** The turn the app runs now. Throw to refuse it (no key, no money); the error is shown. */
|
|
@@ -114,6 +131,19 @@ export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
|
|
|
114
131
|
protected undoWrite(_card: AgentCard): Promise<void>;
|
|
115
132
|
/** Connectors' tools join a turn through `plan.connectors`, at the person's access. */
|
|
116
133
|
includeMcpTools: boolean;
|
|
134
|
+
/** What the person sent with the message this turn answers (page, refs, tier, zone). */
|
|
135
|
+
protected get input(): AskInput | null;
|
|
136
|
+
/** The chat's running use: every answer's parts added up, and the last context size. */
|
|
137
|
+
usage(): ChatUsage;
|
|
138
|
+
/** The chat as the panel reads it: title, last message, items and use. */
|
|
139
|
+
view(id?: string): Promise<AssistantChat>;
|
|
140
|
+
/** Stops the answer running now; what was said so far is kept. */
|
|
141
|
+
stop(): void;
|
|
142
|
+
/**
|
|
143
|
+
* One answer to `input`, its events sent to `sink` in the panel's words: the chat's title, the
|
|
144
|
+
* text, each tool line and card, then its use. The app's route calls it over RPC.
|
|
145
|
+
*/
|
|
146
|
+
ask(input: AskInput, sink: AskSink): Promise<void>;
|
|
117
147
|
/** The person's connectors, with their state and tool counts. */
|
|
118
148
|
connectors(): AgentConnector[];
|
|
119
149
|
/** Adds a connector by its https address; the answer says where to sign in when it asks. */
|
package/dist/agent/agent.js
CHANGED
|
@@ -18,6 +18,7 @@ import { generateText } from 'ai';
|
|
|
18
18
|
import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
|
|
19
19
|
import { aiModel, turnSettings } from './models.js';
|
|
20
20
|
import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
|
|
21
|
+
import { chunkEvents, itemsOf } from './stream.js';
|
|
21
22
|
import { openAiTools, } from './tools.js';
|
|
22
23
|
import { UsageMeter } from './usage.js';
|
|
23
24
|
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.';
|
|
@@ -64,6 +65,34 @@ export function withLead(messages, lead) {
|
|
|
64
65
|
: [{ type: 'text', text: lead }, ...m.content];
|
|
65
66
|
return [...messages.slice(0, i), { ...m, content }, ...messages.slice(i + 1)];
|
|
66
67
|
}
|
|
68
|
+
/** Usage parts added up: same label, one part. */
|
|
69
|
+
export function chatUsage(parts, context) {
|
|
70
|
+
const by = new Map();
|
|
71
|
+
for (const p of parts) {
|
|
72
|
+
const was = by.get(p.label);
|
|
73
|
+
by.set(p.label, was
|
|
74
|
+
? {
|
|
75
|
+
label: p.label,
|
|
76
|
+
calls: was.calls + p.calls,
|
|
77
|
+
in: was.in + p.in,
|
|
78
|
+
read: was.read + p.read,
|
|
79
|
+
out: was.out + p.out,
|
|
80
|
+
usd: was.usd + p.usd,
|
|
81
|
+
}
|
|
82
|
+
: { ...p });
|
|
83
|
+
}
|
|
84
|
+
const all = [...by.values()];
|
|
85
|
+
const sum = (k) => all.reduce((a, p) => a + p[k], 0);
|
|
86
|
+
return {
|
|
87
|
+
calls: sum('calls'),
|
|
88
|
+
in: sum('in'),
|
|
89
|
+
read: sum('read'),
|
|
90
|
+
out: sum('out'),
|
|
91
|
+
usd: sum('usd'),
|
|
92
|
+
parts: all,
|
|
93
|
+
...(context ? { context } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
67
96
|
export class LumenAgent extends Think {
|
|
68
97
|
/** Saves a draft: runs its write as the person. The result is shown and told to the model. */
|
|
69
98
|
applyDraft(_card) {
|
|
@@ -76,6 +105,113 @@ export class LumenAgent extends Think {
|
|
|
76
105
|
/** Connectors' tools join a turn through `plan.connectors`, at the person's access. */
|
|
77
106
|
includeMcpTools = false;
|
|
78
107
|
#turn = null;
|
|
108
|
+
#input = null;
|
|
109
|
+
#metaTabled = false;
|
|
110
|
+
/** What the person sent with the message this turn answers (page, refs, tier, zone). */
|
|
111
|
+
get input() {
|
|
112
|
+
return this.#input;
|
|
113
|
+
}
|
|
114
|
+
#meta() {
|
|
115
|
+
if (!this.#metaTabled) {
|
|
116
|
+
this.sql `CREATE TABLE IF NOT EXISTS lumen_chat (k TEXT PRIMARY KEY, v TEXT NOT NULL)`;
|
|
117
|
+
this.#metaTabled = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
#get(k) {
|
|
121
|
+
this.#meta();
|
|
122
|
+
const [r] = this.sql `SELECT v FROM lumen_chat WHERE k = ${k} LIMIT 1`;
|
|
123
|
+
return r ? JSON.parse(r.v) : null;
|
|
124
|
+
}
|
|
125
|
+
#put(k, v) {
|
|
126
|
+
this.#meta();
|
|
127
|
+
this.sql `INSERT OR REPLACE INTO lumen_chat (k, v) VALUES (${k}, ${JSON.stringify(v)})`;
|
|
128
|
+
}
|
|
129
|
+
/** The chat's running use: every answer's parts added up, and the last context size. */
|
|
130
|
+
usage() {
|
|
131
|
+
return this.#get('usage') ?? chatUsage([]);
|
|
132
|
+
}
|
|
133
|
+
/** The chat as the panel reads it: title, last message, items and use. */
|
|
134
|
+
async view(id = this.name) {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
title: this.#get('title') ?? '',
|
|
138
|
+
at: this.#get('at') ?? 0,
|
|
139
|
+
items: itemsOf(await this.getMessages(), this.cards()),
|
|
140
|
+
usage: this.usage(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Stops the answer running now; what was said so far is kept. */
|
|
144
|
+
stop() {
|
|
145
|
+
this.cancelAllChats();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One answer to `input`, its events sent to `sink` in the panel's words: the chat's title, the
|
|
149
|
+
* text, each tool line and card, then its use. The app's route calls it over RPC.
|
|
150
|
+
*/
|
|
151
|
+
async ask(input, sink) {
|
|
152
|
+
const at = Date.now();
|
|
153
|
+
this.#capped = false;
|
|
154
|
+
let title = this.#get('title');
|
|
155
|
+
if (!title) {
|
|
156
|
+
title = input.message.replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
157
|
+
this.#put('title', title);
|
|
158
|
+
}
|
|
159
|
+
this.#put('at', at);
|
|
160
|
+
await sink.event({ event: 'chat', data: { id: this.name, title } });
|
|
161
|
+
const meta = {
|
|
162
|
+
at,
|
|
163
|
+
...(input.context ? { context: { label: input.context.label } } : {}),
|
|
164
|
+
...(input.refs?.length ? { refs: input.refs } : {}),
|
|
165
|
+
...(input.tier ? { tier: input.tier } : {}),
|
|
166
|
+
};
|
|
167
|
+
const map = chunkEvents({
|
|
168
|
+
...(this.#last?.plan.advisor ? { advisor: this.#last.plan.advisor.label } : {}),
|
|
169
|
+
cards: () => new Map(this.cards().map((c) => [c.id, c])),
|
|
170
|
+
kindOf: (n) => this.#turn?.tools.info.find((i) => i.name === n)?.kind,
|
|
171
|
+
});
|
|
172
|
+
this.#input = input;
|
|
173
|
+
let failed = false;
|
|
174
|
+
try {
|
|
175
|
+
await this.chat({
|
|
176
|
+
id: `u_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`,
|
|
177
|
+
role: 'user',
|
|
178
|
+
parts: [{ type: 'text', text: input.message }],
|
|
179
|
+
metadata: meta,
|
|
180
|
+
}, {
|
|
181
|
+
onStart: () => undefined,
|
|
182
|
+
onEvent: async (json) => {
|
|
183
|
+
let chunk;
|
|
184
|
+
try {
|
|
185
|
+
chunk = JSON.parse(json);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const e of map(chunk))
|
|
191
|
+
await sink.event(e);
|
|
192
|
+
},
|
|
193
|
+
onDone: () => undefined,
|
|
194
|
+
onError: async (message) => {
|
|
195
|
+
failed = true;
|
|
196
|
+
await sink.event({ event: 'error', data: { message: message.slice(0, 500) } });
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (!failed)
|
|
202
|
+
await sink.event({
|
|
203
|
+
event: 'error',
|
|
204
|
+
data: { message: String(err?.message ?? err).slice(0, 500) },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
this.#input = null;
|
|
209
|
+
}
|
|
210
|
+
if (this.#capped)
|
|
211
|
+
await sink.event({ event: 'stopped', data: { reason: 'cap' } });
|
|
212
|
+
await sink.event({ event: 'done', data: { id: this.name, usage: this.usage() } });
|
|
213
|
+
}
|
|
214
|
+
#capped = false;
|
|
79
215
|
/** The last turn's model and key, for compaction between turns. */
|
|
80
216
|
#last = null;
|
|
81
217
|
/** Usage spent between turns (compaction), settled with the next turn. */
|
|
@@ -90,9 +226,9 @@ export class LumenAgent extends Think {
|
|
|
90
226
|
}
|
|
91
227
|
#addCard(kind, d, result) {
|
|
92
228
|
this.#cards();
|
|
93
|
-
const id = `card_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
229
|
+
const id = d.call ?? `card_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
94
230
|
this
|
|
95
|
-
.sql `INSERT INTO lumen_cards (id, kind, grp, tool, args, result, status, at) VALUES (${id}, ${kind}, ${d.group}, ${d.tool}, ${JSON.stringify(d.args ?? null)}, ${result === undefined ? null : JSON.stringify(result).slice(0, 20_000)}, ${kind === 'draft' ? 'open' : 'done'}, ${Date.now()})`;
|
|
231
|
+
.sql `INSERT OR REPLACE INTO lumen_cards (id, kind, grp, tool, args, result, status, at) VALUES (${id}, ${kind}, ${d.group}, ${d.tool}, ${JSON.stringify(d.args ?? null)}, ${result === undefined ? null : JSON.stringify(result).slice(0, 20_000)}, ${kind === 'draft' ? 'open' : 'done'}, ${Date.now()})`;
|
|
96
232
|
return id;
|
|
97
233
|
}
|
|
98
234
|
#card(id) {
|
|
@@ -377,10 +513,13 @@ export class LumenAgent extends Think {
|
|
|
377
513
|
return;
|
|
378
514
|
this.#turn = null;
|
|
379
515
|
await t.tools.close();
|
|
516
|
+
const parts = t.meters.map((m) => m.part()).filter((p) => p.calls);
|
|
517
|
+
this.#capped = t.capped;
|
|
518
|
+
this.#put('usage', chatUsage([...this.usage().parts, ...parts], t.context));
|
|
380
519
|
try {
|
|
381
520
|
await this.settle({
|
|
382
521
|
outcome,
|
|
383
|
-
parts
|
|
522
|
+
parts,
|
|
384
523
|
...(t.context ? { context: t.context } : {}),
|
|
385
524
|
steps: t.steps,
|
|
386
525
|
ms: Date.now() - t.t0,
|
package/dist/agent/index.d.ts
CHANGED
package/dist/agent/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AskSink } from './agent.js';
|
|
2
|
+
export interface AskResponseOptions {
|
|
3
|
+
/** The request's signal: when the person stops or leaves, `stop` ends the answer. */
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
stop?: () => unknown;
|
|
6
|
+
/** Keeps the answer running past the response (a Worker's `ctx.waitUntil`). */
|
|
7
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
8
|
+
}
|
|
9
|
+
/** Streams `run`'s events as `event:` / `data:` blocks; a failure ends in an `error` event. */
|
|
10
|
+
export declare function askResponse(run: (sink: AskSink) => Promise<void>, o?: AskResponseOptions): Response;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Streams `run`'s events as `event:` / `data:` blocks; a failure ends in an `error` event. */
|
|
2
|
+
export function askResponse(run, o = {}) {
|
|
3
|
+
const { readable, writable } = new TransformStream();
|
|
4
|
+
const writer = writable.getWriter();
|
|
5
|
+
const enc = new TextEncoder();
|
|
6
|
+
let open = true;
|
|
7
|
+
const write = async (e) => {
|
|
8
|
+
if (!open)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
await writer.write(enc.encode(`event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
open = false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
o.signal?.addEventListener('abort', () => {
|
|
18
|
+
open = false;
|
|
19
|
+
void Promise.resolve(o.stop?.()).catch(() => undefined);
|
|
20
|
+
});
|
|
21
|
+
const work = (async () => {
|
|
22
|
+
try {
|
|
23
|
+
await run({ event: write });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
await write({
|
|
27
|
+
event: 'error',
|
|
28
|
+
data: { message: String(err?.message ?? err).slice(0, 500) },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
open = false;
|
|
33
|
+
await writer.close().catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
o.waitUntil?.(work);
|
|
37
|
+
return new Response(readable, {
|
|
38
|
+
headers: {
|
|
39
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
40
|
+
'cache-control': 'no-store',
|
|
41
|
+
'x-accel-buffering': 'no',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Think chat in the panel's words: the AI SDK's UI message stream (what `chat()` sends) as the
|
|
3
|
+
* panel's `AssistantEvent`s, and a stored chat's messages and cards as its `ChatItem`s. The panel
|
|
4
|
+
* (`@columenlabs/lumen-ui`'s `LumenAssistant`) reads a Think chat the same way as before.
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantEvent, AssistantRefNote, ChatItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
7
|
+
import type { AgentCard } from './agent.js';
|
|
8
|
+
/** A tool's name as a line's title: `notes__find_notes` → `find notes`. */
|
|
9
|
+
export declare function stepTitle(name: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Turns UI message chunks into the panel's events, one chunk at a time. `cards` says how a draft
|
|
12
|
+
* or write ended (a card's id is its tool call's). `advisor` names the advice part.
|
|
13
|
+
*/
|
|
14
|
+
export declare function chunkEvents(o?: {
|
|
15
|
+
advisor?: string;
|
|
16
|
+
cards?: () => Map<string, AgentCard>;
|
|
17
|
+
/** A tool's kind this turn: a draft shows as its card only, with no line before it. */
|
|
18
|
+
kindOf?: (name: string) => 'read' | 'draft' | 'write' | undefined;
|
|
19
|
+
}): (chunk: {
|
|
20
|
+
type: string;
|
|
21
|
+
[k: string]: unknown;
|
|
22
|
+
}) => AssistantEvent[];
|
|
23
|
+
/** A stored UI message, as much of it as a chat's items need. */
|
|
24
|
+
interface StoredMessage {
|
|
25
|
+
role: string;
|
|
26
|
+
parts: {
|
|
27
|
+
type: string;
|
|
28
|
+
[k: string]: unknown;
|
|
29
|
+
}[];
|
|
30
|
+
metadata?: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** A user message's extras, kept in its metadata by `LumenAgent.ask`. */
|
|
33
|
+
export interface AskMeta {
|
|
34
|
+
at?: number;
|
|
35
|
+
context?: {
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
refs?: AssistantRefNote[];
|
|
39
|
+
tier?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A stored chat as the panel's items: the messages in order, each tool call as it ended. */
|
|
42
|
+
export declare function itemsOf(messages: StoredMessage[], cards: AgentCard[]): ChatItem[];
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { rowCount } from '../assistant/tools.js';
|
|
2
|
+
/** A tool's name as a line's title: `notes__find_notes` → `find notes`. */
|
|
3
|
+
export function stepTitle(name) {
|
|
4
|
+
const own = name.includes('__') ? name.slice(name.indexOf('__') + 2) : name;
|
|
5
|
+
return own.replace(/_/g, ' ');
|
|
6
|
+
}
|
|
7
|
+
const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
8
|
+
/** What one finished tool call shows as: a line, a card, a hand-off section or nothing. */
|
|
9
|
+
function settledCall(id, c, out, ms, cards) {
|
|
10
|
+
const o = obj(out.output);
|
|
11
|
+
const card = cards.get(id);
|
|
12
|
+
if (card?.kind === 'draft' || (o && typeof o.card === 'string')) {
|
|
13
|
+
const draft = {
|
|
14
|
+
kind: 'draft',
|
|
15
|
+
id,
|
|
16
|
+
tool: c.name,
|
|
17
|
+
title: stepTitle(c.name),
|
|
18
|
+
args: card?.args ?? c.input ?? null,
|
|
19
|
+
status: card && card.status !== 'done' && card.status !== 'undone' ? card.status : 'open',
|
|
20
|
+
...(card?.result !== undefined ? { result: card.result } : {}),
|
|
21
|
+
...(card?.error ? { error: card.error } : {}),
|
|
22
|
+
};
|
|
23
|
+
return [{ event: 'draft', data: draft }];
|
|
24
|
+
}
|
|
25
|
+
const h = obj(o?.handoff);
|
|
26
|
+
if (h && typeof h.tier === 'string' && typeof o?.memo === 'string') {
|
|
27
|
+
const task = String(obj(c.input)?.task ?? '');
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
event: 'handoff',
|
|
31
|
+
data: { kind: 'handoff', tier: h.tier, label: h.label, task, why: h.why },
|
|
32
|
+
},
|
|
33
|
+
{ event: 'handoff_done', data: { usage: h.usage } },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
const error = out.error ?? (o && typeof o.error === 'string' ? o.error : undefined);
|
|
37
|
+
const rows = rowCount(o && 'result' in o ? o.result : out.output);
|
|
38
|
+
const step = {
|
|
39
|
+
kind: 'step',
|
|
40
|
+
id,
|
|
41
|
+
tool: c.name,
|
|
42
|
+
title: stepTitle(c.name),
|
|
43
|
+
ok: !error,
|
|
44
|
+
ms,
|
|
45
|
+
...(rows !== undefined && !error ? { rows } : {}),
|
|
46
|
+
...(error ? { error: error.slice(0, 300) } : {}),
|
|
47
|
+
...(card?.kind === 'write' || (o && typeof o.change === 'string')
|
|
48
|
+
? { write: { undo: true, status: card?.status === 'undone' ? 'undone' : 'done' } }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
return [{ event: 'step', data: step }];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Turns UI message chunks into the panel's events, one chunk at a time. `cards` says how a draft
|
|
55
|
+
* or write ended (a card's id is its tool call's). `advisor` names the advice part.
|
|
56
|
+
*/
|
|
57
|
+
export function chunkEvents(o = {}) {
|
|
58
|
+
const calls = new Map();
|
|
59
|
+
let advising = false;
|
|
60
|
+
return (chunk) => {
|
|
61
|
+
const id = typeof chunk.toolCallId === 'string' ? chunk.toolCallId : '';
|
|
62
|
+
switch (chunk.type) {
|
|
63
|
+
case 'text-delta':
|
|
64
|
+
return typeof chunk.delta === 'string' && chunk.delta
|
|
65
|
+
? [{ event: 'text', data: { delta: chunk.delta } }]
|
|
66
|
+
: [];
|
|
67
|
+
case 'text-end':
|
|
68
|
+
return [{ event: 'text_end', data: {} }];
|
|
69
|
+
case 'tool-input-start': {
|
|
70
|
+
const name = String(chunk.toolName ?? '');
|
|
71
|
+
calls.set(id, { name, t0: Date.now() });
|
|
72
|
+
if (name === 'advisor') {
|
|
73
|
+
advising = true;
|
|
74
|
+
return [{ event: 'advice', data: { tier: 'advisor', label: o.advisor ?? 'Advice' } }];
|
|
75
|
+
}
|
|
76
|
+
if (name === 'escalate' || o.kindOf?.(name) === 'draft')
|
|
77
|
+
return [];
|
|
78
|
+
return [{ event: 'step_start', data: { id, tool: name, title: stepTitle(name) } }];
|
|
79
|
+
}
|
|
80
|
+
case 'tool-input-available': {
|
|
81
|
+
const c = calls.get(id) ?? { name: String(chunk.toolName ?? ''), t0: Date.now() };
|
|
82
|
+
calls.set(id, { ...c, input: chunk.input });
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
case 'tool-output-available':
|
|
86
|
+
case 'tool-output-error': {
|
|
87
|
+
const c = calls.get(id);
|
|
88
|
+
if (!c)
|
|
89
|
+
return [];
|
|
90
|
+
calls.delete(id);
|
|
91
|
+
const error = chunk.type === 'tool-output-error' ? String(chunk.errorText ?? '') : undefined;
|
|
92
|
+
if (c.name === 'advisor') {
|
|
93
|
+
advising = false;
|
|
94
|
+
return [{ event: 'advice_done', data: error ? { error } : {} }];
|
|
95
|
+
}
|
|
96
|
+
return settledCall(id, c, error !== undefined ? { error } : { output: chunk.output }, Date.now() - c.t0, o.cards?.() ?? new Map());
|
|
97
|
+
}
|
|
98
|
+
case 'error':
|
|
99
|
+
return [
|
|
100
|
+
{ event: 'error', data: { message: String(chunk.errorText ?? 'The answer failed.') } },
|
|
101
|
+
];
|
|
102
|
+
case 'abort':
|
|
103
|
+
return advising
|
|
104
|
+
? [
|
|
105
|
+
{ event: 'advice_done', data: {} },
|
|
106
|
+
{ event: 'stopped', data: {} },
|
|
107
|
+
]
|
|
108
|
+
: [{ event: 'stopped', data: {} }];
|
|
109
|
+
default:
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** A stored chat as the panel's items: the messages in order, each tool call as it ended. */
|
|
115
|
+
export function itemsOf(messages, cards) {
|
|
116
|
+
const byId = new Map(cards.map((c) => [c.id, c]));
|
|
117
|
+
const items = [];
|
|
118
|
+
for (const m of messages) {
|
|
119
|
+
if (m.role === 'user') {
|
|
120
|
+
const text = m.parts
|
|
121
|
+
.filter((p) => p.type === 'text')
|
|
122
|
+
.map((p) => String(p.text ?? ''))
|
|
123
|
+
.join('');
|
|
124
|
+
const meta = (obj(m.metadata) ?? {});
|
|
125
|
+
items.push({
|
|
126
|
+
kind: 'user',
|
|
127
|
+
text,
|
|
128
|
+
at: meta.at ?? 0,
|
|
129
|
+
...(meta.context?.label ? { context: meta.context.label } : {}),
|
|
130
|
+
...(meta.refs?.length ? { refs: meta.refs } : {}),
|
|
131
|
+
...(meta.tier ? { tier: meta.tier } : {}),
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (m.role !== 'assistant')
|
|
136
|
+
continue;
|
|
137
|
+
for (const p of m.parts) {
|
|
138
|
+
if (p.type === 'text' && typeof p.text === 'string' && p.text) {
|
|
139
|
+
const last = items[items.length - 1];
|
|
140
|
+
if (last?.kind === 'text')
|
|
141
|
+
last.text += p.text;
|
|
142
|
+
else
|
|
143
|
+
items.push({ kind: 'text', text: p.text });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const tool = p.type === 'dynamic-tool'
|
|
147
|
+
? String(p.toolName ?? '')
|
|
148
|
+
: p.type.startsWith('tool-')
|
|
149
|
+
? p.type.slice(5)
|
|
150
|
+
: '';
|
|
151
|
+
if (!tool || typeof p.toolCallId !== 'string')
|
|
152
|
+
continue;
|
|
153
|
+
if (tool === 'advisor') {
|
|
154
|
+
items.push({ kind: 'advice', tier: 'advisor', label: 'Advice' });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const ended = p.state === 'output-available'
|
|
158
|
+
? { output: p.output }
|
|
159
|
+
: p.state === 'output-error'
|
|
160
|
+
? { error: String(p.errorText ?? 'failed') }
|
|
161
|
+
: null;
|
|
162
|
+
if (!ended)
|
|
163
|
+
continue;
|
|
164
|
+
for (const e of settledCall(p.toolCallId, { name: tool, input: p.input, t0: 0 }, ended, 0, byId)) {
|
|
165
|
+
if (e.event === 'handoff')
|
|
166
|
+
items.push({ ...e.data, items: [] });
|
|
167
|
+
else if (e.event === 'handoff_done') {
|
|
168
|
+
const h = items[items.length - 1];
|
|
169
|
+
if (h?.kind === 'handoff')
|
|
170
|
+
h.usage = e.data.usage;
|
|
171
|
+
}
|
|
172
|
+
else if (e.event === 'step' || e.event === 'draft') {
|
|
173
|
+
const { handoff: _h, ...item } = e.data;
|
|
174
|
+
items.push(item);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return items;
|
|
180
|
+
}
|
package/dist/agent/tools.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface SourceAt {
|
|
|
16
16
|
}
|
|
17
17
|
/** A write the model asked for while its group drafts: the card's content. */
|
|
18
18
|
export interface DraftCall {
|
|
19
|
+
/** The model's tool call id: a card keeps it as its own. */
|
|
20
|
+
call?: string;
|
|
19
21
|
group: string;
|
|
20
22
|
tool: string;
|
|
21
23
|
args: unknown;
|
package/dist/agent/tools.js
CHANGED
|
@@ -41,9 +41,10 @@ export async function openAiTools(sources, o = {}) {
|
|
|
41
41
|
tools[name] = tool({
|
|
42
42
|
description: describe(def, kind),
|
|
43
43
|
inputSchema: jsonSchema(withoutMeta(def.inputSchema)),
|
|
44
|
-
execute: async (args) => {
|
|
44
|
+
execute: async (args, { toolCallId }) => {
|
|
45
|
+
const call = toolCallId;
|
|
45
46
|
if (kind === 'draft')
|
|
46
|
-
return o.draft?.({ group: s.group, tool: def.name, args });
|
|
47
|
+
return o.draft?.({ call, group: s.group, tool: def.name, args });
|
|
47
48
|
const t0 = Date.now();
|
|
48
49
|
try {
|
|
49
50
|
const out = await open.call(def.name, args);
|
|
@@ -51,7 +52,13 @@ export async function openAiTools(sources, o = {}) {
|
|
|
51
52
|
const text = typeof out === 'string' ? out : JSON.stringify(out);
|
|
52
53
|
const shown = text.length > max ? `${text.slice(0, max)} …(cut)` : out;
|
|
53
54
|
if (kind === 'write' && o.wrote) {
|
|
54
|
-
const change = await o.wrote({
|
|
55
|
+
const change = await o.wrote({
|
|
56
|
+
call,
|
|
57
|
+
group: s.group,
|
|
58
|
+
tool: def.name,
|
|
59
|
+
args,
|
|
60
|
+
result: out,
|
|
61
|
+
});
|
|
55
62
|
if (change !== undefined)
|
|
56
63
|
return { result: shown, change };
|
|
57
64
|
}
|
package/dist/assistant/engine.js
CHANGED
|
@@ -592,7 +592,11 @@ export class Assistant {
|
|
|
592
592
|
plan: !!key.plan,
|
|
593
593
|
}),
|
|
594
594
|
});
|
|
595
|
-
return {
|
|
595
|
+
return {
|
|
596
|
+
memo: `Memo from ${sub.meter.label}:\n${r.text.slice(0, MEMO_CHARS)}`,
|
|
597
|
+
// For the chat's hand-off section; the model reads the memo.
|
|
598
|
+
handoff: { tier: tier.id, label: sub.meter.label, why: shown, usage: meter.part() },
|
|
599
|
+
};
|
|
596
600
|
}
|
|
597
601
|
catch (err) {
|
|
598
602
|
if (abortSignal?.aborted)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.183",
|
|
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.183",
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.183",
|
|
53
53
|
"agents": "0.32.0",
|
|
54
54
|
"ai": "7.0.162",
|
|
55
55
|
"just-bash": "3.6.0",
|