@columenlabs/lumen-ai 0.1.190 → 0.1.192
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 -0
- package/dist/agent/agent.d.ts +7 -1
- package/dist/agent/agent.js +73 -6
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/screen.d.ts +63 -0
- package/dist/agent/screen.js +178 -0
- package/dist/agent/stream.d.ts +4 -1
- package/dist/agent/stream.js +22 -6
- package/dist/assistant/engine.js +2 -0
- package/dist/assistant/routes.js +5 -2
- package/dist/assistant/state.d.ts +3 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ app.all('/api/assistant/*', (c) => routes(c.req.raw));
|
|
|
102
102
|
- **Web.** `fetch` is a plain GET from the Worker (10 s, 2 MB, never the app's own hosts or private addresses). ScrapeQL's own tools join over its MCP API once an admin pastes a Cloudflare Access service token (`PUT /web/scrapeql`). A Worker on ScrapeQL's own Cloudflare account passes its service binding instead, `scrapeql: { url: 'https://scrapeql/mcp', fetch: env.SCRAPEQL.fetch.bind(env.SCRAPEQL) }`: no token is stored, the tools are always on and Settings shows the link with a Test.
|
|
103
103
|
- **Hand off and advice.** The `escalate` tool hands one task to a higher tier (the person sets next, any or only when asked); Anthropic's advisor tool lets a higher tier send a plan, at most two an answer.
|
|
104
104
|
- **Chats** run on the agent (`./agent`, `LumenAgent`, below): the app routes `POST /chat` and each chat's reads, cards and Undo to its own agent, and `assistantRoutes` answers `POST /chat` with 410 `moved`. Chats saved by the old turn loop stay readable (`GET /chats`, `GET|DELETE /chats/:id`) until `assistant.purge()` drops them 30 days after their last message.
|
|
105
|
+
- **Screen moves**: while the person's Settings ▸ Screen is Move (the default), the panel sends its page map (`AssistantMessage.screen`: routes, nav pages, Settings sections, cards on screen, rows to open) and `LumenAgent` gives the model a `screen` tool (`./agent` `screen.ts`). Each call's moves (go, settings, chart, open, point) are checked against the map, kept in the chat and streamed as a `screen` event; the panel runs them through `AssistantScreenProvider` (mounted by `LumenShell`) with an Undo chip, and their outcomes go back with the next message (`outcomes`). Nothing is saved by a move.
|
|
105
106
|
- **Background work** an app runs for a person (a score, a summary) is `assistant.complete(caller, { task, system, prompt, images?, maxTokens })`: one answer on the task's tier (`tasks`, so the person picks its model in Settings ▸ Models), no chat and no tools (`images`, base64 with their media type, are read with the prompt: a scanned page, a screenshot), billed to their payer and counted in their spend and allowance like a chat turn. It throws `AssistantError` `not_connected`, `app_cap` or `own_cap` when nobody can pay, so the caller waits and tries later.
|
|
106
107
|
|
|
107
108
|
### Commands, `@` and `#`
|
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 { AssistantChat, AssistantEffort, AssistantEvent, AssistantRefNote, ChatUsage, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
15
|
+
import type { AssistantChat, AssistantEffort, AssistantEvent, AssistantRefNote, AssistantScreenMap, AssistantScreenOutcome, 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';
|
|
@@ -72,6 +72,8 @@ export interface AgentTurnPlan {
|
|
|
72
72
|
};
|
|
73
73
|
/** Dollars the payer has left; at or past it the model gets no more tools. */
|
|
74
74
|
left?: number;
|
|
75
|
+
/** The person lets the assistant move their screen: the `screen` tool joins when the browser sent its map. */
|
|
76
|
+
screen?: boolean;
|
|
75
77
|
/** Most model calls in one answer; 8. */
|
|
76
78
|
maxSteps?: number;
|
|
77
79
|
/** Fetch for provider calls (a metered one). */
|
|
@@ -121,6 +123,10 @@ export interface AskInput {
|
|
|
121
123
|
refs?: AssistantRefNote[];
|
|
122
124
|
tier?: string;
|
|
123
125
|
time_zone?: string;
|
|
126
|
+
/** What the browser can show (Screen on): the `screen` tool and its rules join the turn. */
|
|
127
|
+
screen?: AssistantScreenMap;
|
|
128
|
+
/** How earlier screen calls ended in the browser. */
|
|
129
|
+
outcomes?: AssistantScreenOutcome[];
|
|
124
130
|
}
|
|
125
131
|
/** Where an answer's events go: the app's route, over RPC. */
|
|
126
132
|
export interface AskSink {
|
package/dist/agent/agent.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createCompactFunction } from 'agents/sessions';
|
|
|
17
17
|
import { generateText } from 'ai';
|
|
18
18
|
import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
|
|
19
19
|
import { aiModel, planFallback, turnSettings } from './models.js';
|
|
20
|
+
import { screenMapOf, screenNews, screenOutcomesOf, screenRules, screenTool, } from './screen.js';
|
|
20
21
|
import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
|
|
21
22
|
import { chunkEvents, itemsOf } from './stream.js';
|
|
22
23
|
import { openAiTools, } from './tools.js';
|
|
@@ -136,7 +137,7 @@ export class LumenAgent extends Think {
|
|
|
136
137
|
id,
|
|
137
138
|
title: this.#get('title') ?? '',
|
|
138
139
|
at: this.#get('at') ?? 0,
|
|
139
|
-
items: itemsOf(await this.getMessages(), this.cards()),
|
|
140
|
+
items: itemsOf(await this.getMessages(), this.cards(), this.#screens()),
|
|
140
141
|
usage: this.usage(),
|
|
141
142
|
};
|
|
142
143
|
}
|
|
@@ -158,18 +159,23 @@ export class LumenAgent extends Think {
|
|
|
158
159
|
}
|
|
159
160
|
this.#put('at', at);
|
|
160
161
|
await sink.event({ event: 'chat', data: { id: this.name, title } });
|
|
162
|
+
this.#outcomes(screenOutcomesOf(input.outcomes));
|
|
161
163
|
const meta = {
|
|
162
164
|
at,
|
|
163
165
|
...(input.context ? { context: { label: input.context.label } } : {}),
|
|
164
166
|
...(input.refs?.length ? { refs: input.refs } : {}),
|
|
165
167
|
...(input.tier ? { tier: input.tier } : {}),
|
|
166
168
|
};
|
|
167
|
-
const
|
|
169
|
+
const toEvents = chunkEvents({
|
|
168
170
|
...(this.#last?.plan.advisor ? { advisor: this.#last.plan.advisor.label } : {}),
|
|
169
171
|
cards: () => new Map(this.cards().map((c) => [c.id, c])),
|
|
172
|
+
screens: () => this.#screens(),
|
|
170
173
|
kindOf: (n) => this.#turn?.tools.info.find((i) => i.name === n)?.kind,
|
|
171
174
|
});
|
|
172
|
-
|
|
175
|
+
const map = screenMapOf(input.screen);
|
|
176
|
+
this.#input = { ...input, ...(map ? { screen: map } : {}) };
|
|
177
|
+
if (!map)
|
|
178
|
+
delete this.#input.screen;
|
|
173
179
|
let failed = false;
|
|
174
180
|
try {
|
|
175
181
|
await this.chat({
|
|
@@ -187,7 +193,7 @@ export class LumenAgent extends Think {
|
|
|
187
193
|
catch {
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
190
|
-
for (const e of
|
|
196
|
+
for (const e of toEvents(chunk))
|
|
191
197
|
await sink.event(e);
|
|
192
198
|
},
|
|
193
199
|
onDone: () => undefined,
|
|
@@ -240,6 +246,54 @@ export class LumenAgent extends Think {
|
|
|
240
246
|
this
|
|
241
247
|
.sql `UPDATE lumen_cards SET status = ${status}, result = COALESCE(${o.result === undefined ? null : JSON.stringify(o.result).slice(0, 20_000)}, result), error = ${o.error ?? null}, told = 0 WHERE id = ${id}`;
|
|
242
248
|
}
|
|
249
|
+
#screensTabled = false;
|
|
250
|
+
#screenTable() {
|
|
251
|
+
if (!this.#screensTabled) {
|
|
252
|
+
this
|
|
253
|
+
.sql `CREATE TABLE IF NOT EXISTS lumen_screens (id TEXT PRIMARY KEY, label TEXT NOT NULL, moves TEXT NOT NULL, status TEXT NOT NULL, text TEXT, told INTEGER NOT NULL DEFAULT 1, at INTEGER NOT NULL)`;
|
|
254
|
+
this.#screensTabled = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** The chat's screen calls (at most 200), by id. */
|
|
258
|
+
#screens() {
|
|
259
|
+
this.#screenTable();
|
|
260
|
+
const rows = this.sql `SELECT id, label, moves, status, text FROM lumen_screens ORDER BY at DESC LIMIT 200`;
|
|
261
|
+
return new Map(rows.map((r) => [
|
|
262
|
+
r.id,
|
|
263
|
+
{
|
|
264
|
+
id: r.id,
|
|
265
|
+
label: r.label,
|
|
266
|
+
moves: JSON.parse(r.moves),
|
|
267
|
+
status: r.status,
|
|
268
|
+
...(r.text ? { text: r.text } : {}),
|
|
269
|
+
},
|
|
270
|
+
]));
|
|
271
|
+
}
|
|
272
|
+
/** How screen calls ended in the browser: kept, and told to the model with its next turn. */
|
|
273
|
+
#outcomes(list) {
|
|
274
|
+
if (!list.length)
|
|
275
|
+
return;
|
|
276
|
+
this.#screenTable();
|
|
277
|
+
for (const o of list)
|
|
278
|
+
this
|
|
279
|
+
.sql `UPDATE lumen_screens SET status = ${o.status}, text = ${o.text ?? null}, told = 0 WHERE id = ${o.id} AND (status != ${o.status} OR COALESCE(text, '') != ${o.text ?? ''})`;
|
|
280
|
+
}
|
|
281
|
+
/** Screen calls whose end the model has not heard, as its note; marks them told. */
|
|
282
|
+
#screenNews() {
|
|
283
|
+
this.#screenTable();
|
|
284
|
+
const rows = this.sql `SELECT id, label, moves, status, text FROM lumen_screens WHERE told = 0 ORDER BY at LIMIT 20`;
|
|
285
|
+
if (!rows.length)
|
|
286
|
+
return '';
|
|
287
|
+
for (const r of rows)
|
|
288
|
+
this.sql `UPDATE lumen_screens SET told = 1 WHERE id = ${r.id}`;
|
|
289
|
+
return screenNews(rows.map((r) => ({
|
|
290
|
+
id: r.id,
|
|
291
|
+
label: r.label,
|
|
292
|
+
moves: JSON.parse(r.moves),
|
|
293
|
+
status: r.status,
|
|
294
|
+
...(r.text ? { text: r.text } : {}),
|
|
295
|
+
})));
|
|
296
|
+
}
|
|
243
297
|
#connectorClient() {
|
|
244
298
|
return {
|
|
245
299
|
listTools: () => this.getMcpServers().tools,
|
|
@@ -436,7 +490,19 @@ export class LumenAgent extends Think {
|
|
|
436
490
|
maxUses: plan.advisor.maxUses,
|
|
437
491
|
maxTokens: plan.advisor.maxTokens,
|
|
438
492
|
});
|
|
439
|
-
const
|
|
493
|
+
const shown = plan.screen ? this.#input?.screen : undefined;
|
|
494
|
+
const screen = shown
|
|
495
|
+
? screenTool(shown, (c) => {
|
|
496
|
+
this.#screenTable();
|
|
497
|
+
this
|
|
498
|
+
.sql `INSERT OR REPLACE INTO lumen_screens (id, label, moves, status, told, at) VALUES (${c.id}, ${c.label}, ${JSON.stringify(c.moves)}, 'sent', 1, ${Date.now()})`;
|
|
499
|
+
})
|
|
500
|
+
: {};
|
|
501
|
+
if (shown) {
|
|
502
|
+
system = `${system}\n\n${screenRules(shown)}`;
|
|
503
|
+
this.#turn.system = system;
|
|
504
|
+
}
|
|
505
|
+
const tools = { ...opened.tools, ...more, ...advisor, ...screen };
|
|
440
506
|
let lazy;
|
|
441
507
|
if (Object.keys(tools).length > (plan.lazy?.at ?? LAZY_AT)) {
|
|
442
508
|
const groups = new Map(opened.info.map((i) => [i.name, i.group]));
|
|
@@ -444,6 +510,7 @@ export class LumenAgent extends Think {
|
|
|
444
510
|
...opened.info.filter((i) => i.find).map((i) => i.name),
|
|
445
511
|
...Object.keys(more ?? {}),
|
|
446
512
|
...Object.keys(advisor),
|
|
513
|
+
...Object.keys(screen),
|
|
447
514
|
];
|
|
448
515
|
if (plan.first && !core.includes(plan.first))
|
|
449
516
|
core.push(plan.first);
|
|
@@ -464,7 +531,7 @@ export class LumenAgent extends Think {
|
|
|
464
531
|
turn.meters.push(turn.main);
|
|
465
532
|
}),
|
|
466
533
|
instructions: system,
|
|
467
|
-
messages: withLead(ctx.messages, `${this.#news()}${plan.lead ?? ''}`),
|
|
534
|
+
messages: withLead(ctx.messages, `${this.#news()}${this.#screenNews()}${plan.lead ?? ''}`),
|
|
468
535
|
tools,
|
|
469
536
|
...(lazy ? { activeTools: lazy.active() } : {}),
|
|
470
537
|
...(plan.first ? { toolChoice: { type: 'tool', toolName: plan.first } } : {}),
|
package/dist/agent/index.d.ts
CHANGED
package/dist/agent/index.js
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assistant moves the person's screen: one tool, `screen`, takes a short list of moves the
|
|
3
|
+
* browser runs (`@columenlabs/lumen-ui`'s `AssistantScreenProvider`): a page of the app, Settings
|
|
4
|
+
* at a section (pulsing a row, never changing it), a symbol's chart, a row's details, a card
|
|
5
|
+
* pointed at. The browser sends its page map with each message; every move is checked against it
|
|
6
|
+
* here before the call reaches the screen, and the browser checks again. How each call ended comes
|
|
7
|
+
* back with the next message, so the model never guesses. Settings ▸ Screen: Off leaves the tool out.
|
|
8
|
+
*/
|
|
9
|
+
import type { AssistantScreenMap, AssistantScreenMove, AssistantScreenOutcome } from '@columenlabs/lumen-ui-types/assistant';
|
|
10
|
+
import { type ToolSet } from 'ai';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
export declare const screenInput: z.ZodObject<{
|
|
13
|
+
label: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
14
|
+
moves: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
15
|
+
go: z.ZodString;
|
|
16
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
17
|
+
settings: z.ZodString;
|
|
18
|
+
row: z.ZodOptional<z.ZodString>;
|
|
19
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
20
|
+
chart: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
21
|
+
indicators: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
22
|
+
label: z.ZodString;
|
|
23
|
+
f: z.ZodString;
|
|
24
|
+
where: z.ZodOptional<z.ZodEnum<{
|
|
25
|
+
overlay: "overlay";
|
|
26
|
+
pane: "pane";
|
|
27
|
+
}>>;
|
|
28
|
+
refs: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
29
|
+
}, z.core.$strict>]>>>;
|
|
30
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
31
|
+
open: z.ZodString;
|
|
32
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
33
|
+
point: z.ZodString;
|
|
34
|
+
}, z.core.$strict>]>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
/** Bounds on a page map from the browser, so a bad one costs nothing. */
|
|
37
|
+
export declare function screenMapOf(v: unknown): AssistantScreenMap | undefined;
|
|
38
|
+
/** Outcomes from the browser, checked. */
|
|
39
|
+
export declare function screenOutcomesOf(v: unknown): AssistantScreenOutcome[];
|
|
40
|
+
/**
|
|
41
|
+
* Each move checked against the page map: an unknown page, Settings section or card is an error
|
|
42
|
+
* back to the model in the same turn. A card after `go` is checked by the browser alone.
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkMoves(moves: AssistantScreenMove[], map: AssistantScreenMap): string | null;
|
|
45
|
+
/** The rules and this app's page map, for the instructions. */
|
|
46
|
+
export declare function screenRules(map: AssistantScreenMap): string;
|
|
47
|
+
/** One screen call as the chat keeps it. */
|
|
48
|
+
export interface ScreenCall {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
moves: AssistantScreenMove[];
|
|
52
|
+
status: 'sent' | AssistantScreenOutcome['status'];
|
|
53
|
+
text?: string;
|
|
54
|
+
}
|
|
55
|
+
/** The calls the model has not heard about since the browser ran them or the person pressed Undo. */
|
|
56
|
+
export declare function screenNews(calls: ScreenCall[]): string;
|
|
57
|
+
export declare const screenId: () => string;
|
|
58
|
+
/** The `screen` tool for one turn: checks the moves, keeps the call and answers with its id. */
|
|
59
|
+
export declare function screenTool(map: AssistantScreenMap, keep: (call: {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
moves: AssistantScreenMove[];
|
|
63
|
+
}) => void): ToolSet;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { indicatorSpecIssue, indicatorSpecSchema } from '@columenlabs/lumen-ui-types/chart-formula';
|
|
2
|
+
import { tool } from 'ai';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const ticker = z
|
|
5
|
+
.string()
|
|
6
|
+
.trim()
|
|
7
|
+
.regex(/^[A-Za-z0-9.^$=:/-]{1,24}$/);
|
|
8
|
+
const move = z.union([
|
|
9
|
+
z.object({ go: z.string().trim().min(1).max(600) }).strict(),
|
|
10
|
+
z
|
|
11
|
+
.object({
|
|
12
|
+
settings: z.string().trim().min(1).max(80),
|
|
13
|
+
row: z.string().trim().min(1).max(120).optional(),
|
|
14
|
+
})
|
|
15
|
+
.strict(),
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
chart: z.union([ticker, z.array(ticker).min(1).max(8)]),
|
|
19
|
+
indicators: z.array(indicatorSpecSchema).max(8).optional(),
|
|
20
|
+
})
|
|
21
|
+
.strict(),
|
|
22
|
+
z.object({ open: z.string().trim().min(1).max(200) }).strict(),
|
|
23
|
+
z.object({ point: z.string().trim().min(1).max(200) }).strict(),
|
|
24
|
+
]);
|
|
25
|
+
export const screenInput = z.object({
|
|
26
|
+
label: z
|
|
27
|
+
.string()
|
|
28
|
+
.describe('What the screen will show, in a few words, e.g. "Settings · Email".')
|
|
29
|
+
.transform((s) => s.replace(/\s+/g, ' ').trim().slice(0, 80)),
|
|
30
|
+
moves: z
|
|
31
|
+
.array(move)
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(8)
|
|
34
|
+
.describe('In order. Each is one of: {go: "/path"} (a page; fill path params), {settings: "<section id>", row?: "<row label>"}, {chart: "SYM"} or {chart: ["A", "B"]} (up to 8 on one chart), {open: "<row title>"} (the row\'s details in the view on screen), {point: "<card id>"} (scroll to a card and highlight it).'),
|
|
35
|
+
});
|
|
36
|
+
/** Bounds on a page map from the browser, so a bad one costs nothing. */
|
|
37
|
+
export function screenMapOf(v) {
|
|
38
|
+
if (!v || typeof v !== 'object')
|
|
39
|
+
return undefined;
|
|
40
|
+
const o = v;
|
|
41
|
+
const list = (x, n, f) => (Array.isArray(x) ? x : [])
|
|
42
|
+
.slice(0, n)
|
|
43
|
+
.flatMap((i) => (i && typeof i === 'object' ? [f(i)] : []))
|
|
44
|
+
.filter((i) => i !== null);
|
|
45
|
+
const str = (x, n) => (typeof x === 'string' && x ? x.slice(0, n) : null);
|
|
46
|
+
return {
|
|
47
|
+
routes: (Array.isArray(o.routes) ? o.routes : [])
|
|
48
|
+
.slice(0, 200)
|
|
49
|
+
.flatMap((r) => (typeof r === 'string' ? [r.slice(0, 300)] : [])),
|
|
50
|
+
pages: list(o.pages, 300, (p) => {
|
|
51
|
+
const path = str(p.path, 300);
|
|
52
|
+
const title = str(p.title, 120);
|
|
53
|
+
const section = str(p.section, 80);
|
|
54
|
+
return path && title ? { path, title, ...(section ? { section } : {}) } : null;
|
|
55
|
+
}),
|
|
56
|
+
settings: list(o.settings, 60, (p) => {
|
|
57
|
+
const id = str(p.id, 80);
|
|
58
|
+
const title = str(p.title, 120);
|
|
59
|
+
return id && title ? { id, title } : null;
|
|
60
|
+
}),
|
|
61
|
+
cards: list(o.cards, 60, (p) => {
|
|
62
|
+
const id = str(p.id, 200);
|
|
63
|
+
const title = str(p.title, 200) ?? '';
|
|
64
|
+
const kind = str(p.kind, 40);
|
|
65
|
+
return id ? { id, title, ...(kind ? { kind } : {}) } : null;
|
|
66
|
+
}),
|
|
67
|
+
rows: (Array.isArray(o.rows) ? o.rows : [])
|
|
68
|
+
.slice(0, 30)
|
|
69
|
+
.flatMap((r) => (typeof r === 'string' && r ? [r.slice(0, 120)] : [])),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Outcomes from the browser, checked. */
|
|
73
|
+
export function screenOutcomesOf(v) {
|
|
74
|
+
return (Array.isArray(v) ? v : []).slice(0, 50).flatMap((o) => {
|
|
75
|
+
const x = o;
|
|
76
|
+
if (!x || typeof x.id !== 'string' || !/^sc_[a-f0-9]{16}$/.test(x.id))
|
|
77
|
+
return [];
|
|
78
|
+
if (!['done', 'failed', 'waiting', 'undone'].includes(String(x.status)))
|
|
79
|
+
return [];
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: x.id,
|
|
83
|
+
status: x.status,
|
|
84
|
+
...(typeof x.text === 'string' && x.text ? { text: x.text.slice(0, 1200) } : {}),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** The pattern's segments match the path's: `$param` matches any one segment. */
|
|
90
|
+
function matches(pattern, path) {
|
|
91
|
+
const a = pattern.split('/').filter(Boolean);
|
|
92
|
+
const b = path.split('/').filter(Boolean);
|
|
93
|
+
if (a.length !== b.length)
|
|
94
|
+
return false;
|
|
95
|
+
return a.every((s, i) => s.startsWith('$') || s === b[i]);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Each move checked against the page map: an unknown page, Settings section or card is an error
|
|
99
|
+
* back to the model in the same turn. A card after `go` is checked by the browser alone.
|
|
100
|
+
*/
|
|
101
|
+
export function checkMoves(moves, map) {
|
|
102
|
+
let moved = false;
|
|
103
|
+
for (const m of moves) {
|
|
104
|
+
if ('go' in m) {
|
|
105
|
+
const path = m.go.split(/[?#]/)[0] ?? '';
|
|
106
|
+
if (!path.startsWith('/') || /\$/.test(path))
|
|
107
|
+
return `go "${m.go}": give a path of this app with its params filled`;
|
|
108
|
+
if (!map.routes.some((r) => matches(r, path)))
|
|
109
|
+
return `go "${m.go}": no such page. Routes: ${map.routes.join(', ')}`;
|
|
110
|
+
moved = true;
|
|
111
|
+
}
|
|
112
|
+
else if ('chart' in m) {
|
|
113
|
+
for (const spec of m.indicators ?? []) {
|
|
114
|
+
const wrong = indicatorSpecIssue(spec);
|
|
115
|
+
if (wrong)
|
|
116
|
+
return `chart indicators: ${wrong}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if ('settings' in m) {
|
|
120
|
+
if (!map.settings.some((s) => s.id === m.settings))
|
|
121
|
+
return `settings "${m.settings}": no such section. Sections: ${map.settings.map((s) => s.id).join(', ')}`;
|
|
122
|
+
}
|
|
123
|
+
else if ('point' in m && !moved) {
|
|
124
|
+
const id = m.point.replace(/^card:/, '');
|
|
125
|
+
if (!map.cards.some((c) => c.id === id))
|
|
126
|
+
return `point "${m.point}": no such card on this page. Cards: ${map.cards.map((c) => c.id).join(', ') || 'none'}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/** The rules and this app's page map, for the instructions. */
|
|
132
|
+
export function screenRules(map) {
|
|
133
|
+
const pages = map.pages
|
|
134
|
+
.map((p) => `${p.path} (${p.section ? `${p.section} ▸ ` : ''}${p.title})`)
|
|
135
|
+
.join('; ');
|
|
136
|
+
return `Screen: with the screen tool you move the person's screen in their browser.
|
|
137
|
+
- Use it when the person asks to see, open, show, go to or be taken somewhere ("show me", "take me to", "where do I set X"). Otherwise answer in words.
|
|
138
|
+
- One call with every move, in order. After go, the next moves act on the new page; open and point act on the page on screen after the moves before them.
|
|
139
|
+
- Moves only change the screen: nothing is saved. Never say something was saved or changed. Say in one line what you put on screen.
|
|
140
|
+
- Settings: you open a section and point at a row; you never flip a switch or change a value. Say which one the person taps.
|
|
141
|
+
- A line "Your screen moves since your last answer" tells you how each call ended (done, failed and why, waiting for the person's tap, or undone) and what the view shows now: answer from it.
|
|
142
|
+
Routes: ${map.routes.join(', ') || '(none)'}
|
|
143
|
+
Pages: ${pages || '(none listed)'}
|
|
144
|
+
Settings sections: ${map.settings.map((s) => `${s.id} (${s.title})`).join(', ') || '(none)'}
|
|
145
|
+
Cards on this page: ${map.cards.map((c) => `${c.id} (${c.title})`).join(', ') || '(none)'}${map.rows?.length ? `\nRows you can open here: ${map.rows.join('; ')}` : ''}`;
|
|
146
|
+
}
|
|
147
|
+
const SAID = {
|
|
148
|
+
sent: 'sent; no word back yet',
|
|
149
|
+
done: 'done',
|
|
150
|
+
failed: 'failed',
|
|
151
|
+
waiting: 'not run yet: the person was mid-edit; it runs when they tap it',
|
|
152
|
+
undone: 'undone by the person',
|
|
153
|
+
};
|
|
154
|
+
/** The calls the model has not heard about since the browser ran them or the person pressed Undo. */
|
|
155
|
+
export function screenNews(calls) {
|
|
156
|
+
if (!calls.length)
|
|
157
|
+
return '';
|
|
158
|
+
const lines = calls.map((c) => `- "${c.label}" ${JSON.stringify(c.moves).slice(0, 300)}: ${SAID[c.status]}${c.text ? `. ${c.status === 'failed' ? 'Why' : 'On screen now'}: ${c.text}` : ''}`);
|
|
159
|
+
return `Your screen moves since your last answer:\n${lines.join('\n')}\n\n`;
|
|
160
|
+
}
|
|
161
|
+
export const screenId = () => `sc_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
162
|
+
/** The `screen` tool for one turn: checks the moves, keeps the call and answers with its id. */
|
|
163
|
+
export function screenTool(map, keep) {
|
|
164
|
+
return {
|
|
165
|
+
screen: tool({
|
|
166
|
+
description: "Move the person's screen: go to a page, open Settings at a section, open a symbol's chart, open a row's details or point at a card. Moves run in order in their browser at once, with an Undo. Use the routes, pages, Settings sections and cards listed under Screen. The outcome comes with the person's next message.",
|
|
167
|
+
inputSchema: screenInput,
|
|
168
|
+
execute: async ({ label, moves }) => {
|
|
169
|
+
const wrong = checkMoves(moves, map);
|
|
170
|
+
if (wrong)
|
|
171
|
+
return { error: wrong };
|
|
172
|
+
const id = screenId();
|
|
173
|
+
keep({ id, label, moves: moves });
|
|
174
|
+
return { screen: id, label, note: 'Sent to the screen. Say in one line what it shows.' };
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/agent/stream.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { AssistantEvent, AssistantRefNote, ChatItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
7
7
|
import type { AgentCard } from './agent.js';
|
|
8
|
+
import type { ScreenCall } from './screen.js';
|
|
8
9
|
/** A tool's name as a line's title: `notes__find_notes` → `find notes`. */
|
|
9
10
|
export declare function stepTitle(name: string): string;
|
|
10
11
|
/**
|
|
@@ -14,6 +15,8 @@ export declare function stepTitle(name: string): string;
|
|
|
14
15
|
export declare function chunkEvents(o?: {
|
|
15
16
|
advisor?: string;
|
|
16
17
|
cards?: () => Map<string, AgentCard>;
|
|
18
|
+
/** The chat's screen calls by id: a `screen` tool's result names its call. */
|
|
19
|
+
screens?: () => Map<string, ScreenCall>;
|
|
17
20
|
/** A tool's kind this turn: a draft shows as its card only, with no line before it. */
|
|
18
21
|
kindOf?: (name: string) => 'read' | 'draft' | 'write' | undefined;
|
|
19
22
|
}): (chunk: {
|
|
@@ -39,5 +42,5 @@ export interface AskMeta {
|
|
|
39
42
|
tier?: string;
|
|
40
43
|
}
|
|
41
44
|
/** 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[];
|
|
45
|
+
export declare function itemsOf(messages: StoredMessage[], cards: AgentCard[], screens?: Map<string, ScreenCall>): ChatItem[];
|
|
43
46
|
export {};
|
package/dist/agent/stream.js
CHANGED
|
@@ -6,8 +6,24 @@ export function stepTitle(name) {
|
|
|
6
6
|
}
|
|
7
7
|
const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
8
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) {
|
|
9
|
+
function settledCall(id, c, out, ms, cards, screens = new Map()) {
|
|
10
10
|
const o = obj(out.output);
|
|
11
|
+
if (c.name === 'screen') {
|
|
12
|
+
// A call the page map refused goes back to the model only; the person sees the one that ran.
|
|
13
|
+
const call = typeof o?.screen === 'string' ? screens.get(o.screen) : undefined;
|
|
14
|
+
if (!call)
|
|
15
|
+
return [];
|
|
16
|
+
const item = {
|
|
17
|
+
kind: 'screen',
|
|
18
|
+
id: call.id,
|
|
19
|
+
label: call.label,
|
|
20
|
+
moves: call.moves,
|
|
21
|
+
...(call.status !== 'sent'
|
|
22
|
+
? { outcome: { status: call.status, ...(call.text ? { text: call.text } : {}) } }
|
|
23
|
+
: {}),
|
|
24
|
+
};
|
|
25
|
+
return [{ event: 'screen', data: item }];
|
|
26
|
+
}
|
|
11
27
|
const card = cards.get(id);
|
|
12
28
|
if (card?.kind === 'draft' || (o && typeof o.card === 'string')) {
|
|
13
29
|
const draft = {
|
|
@@ -73,7 +89,7 @@ export function chunkEvents(o = {}) {
|
|
|
73
89
|
advising = true;
|
|
74
90
|
return [{ event: 'advice', data: { tier: 'advisor', label: o.advisor ?? 'Advice' } }];
|
|
75
91
|
}
|
|
76
|
-
if (name === 'escalate' || o.kindOf?.(name) === 'draft')
|
|
92
|
+
if (name === 'escalate' || name === 'screen' || o.kindOf?.(name) === 'draft')
|
|
77
93
|
return [];
|
|
78
94
|
return [{ event: 'step_start', data: { id, tool: name, title: stepTitle(name) } }];
|
|
79
95
|
}
|
|
@@ -93,7 +109,7 @@ export function chunkEvents(o = {}) {
|
|
|
93
109
|
advising = false;
|
|
94
110
|
return [{ event: 'advice_done', data: error ? { error } : {} }];
|
|
95
111
|
}
|
|
96
|
-
return settledCall(id, c, error !== undefined ? { error } : { output: chunk.output }, Date.now() - c.t0, o.cards?.() ?? new Map());
|
|
112
|
+
return settledCall(id, c, error !== undefined ? { error } : { output: chunk.output }, Date.now() - c.t0, o.cards?.() ?? new Map(), c.name === 'screen' ? (o.screens?.() ?? new Map()) : undefined);
|
|
97
113
|
}
|
|
98
114
|
case 'error':
|
|
99
115
|
return [
|
|
@@ -112,7 +128,7 @@ export function chunkEvents(o = {}) {
|
|
|
112
128
|
};
|
|
113
129
|
}
|
|
114
130
|
/** A stored chat as the panel's items: the messages in order, each tool call as it ended. */
|
|
115
|
-
export function itemsOf(messages, cards) {
|
|
131
|
+
export function itemsOf(messages, cards, screens = new Map()) {
|
|
116
132
|
const byId = new Map(cards.map((c) => [c.id, c]));
|
|
117
133
|
const items = [];
|
|
118
134
|
for (const m of messages) {
|
|
@@ -161,7 +177,7 @@ export function itemsOf(messages, cards) {
|
|
|
161
177
|
: null;
|
|
162
178
|
if (!ended)
|
|
163
179
|
continue;
|
|
164
|
-
for (const e of settledCall(p.toolCallId, { name: tool, input: p.input, t0: 0 }, ended, 0, byId)) {
|
|
180
|
+
for (const e of settledCall(p.toolCallId, { name: tool, input: p.input, t0: 0 }, ended, 0, byId, screens)) {
|
|
165
181
|
if (e.event === 'handoff')
|
|
166
182
|
items.push({ ...e.data, items: [] });
|
|
167
183
|
else if (e.event === 'handoff_done') {
|
|
@@ -169,7 +185,7 @@ export function itemsOf(messages, cards) {
|
|
|
169
185
|
if (h?.kind === 'handoff')
|
|
170
186
|
h.usage = e.data.usage;
|
|
171
187
|
}
|
|
172
|
-
else if (e.event === 'step' || e.event === 'draft') {
|
|
188
|
+
else if (e.event === 'step' || e.event === 'draft' || e.event === 'screen') {
|
|
173
189
|
const { handoff: _h, ...item } = e.data;
|
|
174
190
|
items.push(item);
|
|
175
191
|
}
|
package/dist/assistant/engine.js
CHANGED
|
@@ -284,6 +284,7 @@ export class Assistant {
|
|
|
284
284
|
...(fallback ? { fallback } : {}),
|
|
285
285
|
left: paying.left,
|
|
286
286
|
maxSteps: MAX_ROUNDS,
|
|
287
|
+
screen: (p.screen ?? 'move') === 'move',
|
|
287
288
|
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
|
288
289
|
...(handTo && reach.length ? { first: 'escalate' } : {}),
|
|
289
290
|
...(advisor
|
|
@@ -551,6 +552,7 @@ export class Assistant {
|
|
|
551
552
|
})),
|
|
552
553
|
hand_off: p.hand_off ?? 'next',
|
|
553
554
|
advice: p.advice && (p.advice === 'off' || this.tierById(p.advice)) ? p.advice : 'off',
|
|
555
|
+
screen: p.screen ?? 'move',
|
|
554
556
|
web: {
|
|
555
557
|
fetch: p.web ?? true,
|
|
556
558
|
scrapeql: this.o.web?.scrapeql
|
package/dist/assistant/routes.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* app.all('/api/assistant/*', (c) => assistant(c.req.raw)); // Hono
|
|
6
6
|
*
|
|
7
7
|
* GET /status what the panel and Settings show
|
|
8
|
-
* PUT /settings the person's tiers, tasks, hand off, advice, tool access, web, payer
|
|
8
|
+
* PUT /settings the person's tiers, tasks, hand off, advice, screen, tool access, web, payer
|
|
9
9
|
* PUT /app {cap_usd} admin: what each person may spend a month on the app's keys
|
|
10
10
|
* PUT /key {api_key} admin: the app's key of `?provider=` (anthropic by default,
|
|
11
11
|
* or openai), tested before it is kept
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* form or fetch is refused before it reaches a rule. Keys and tokens are write-only: no route
|
|
31
31
|
* returns one, only a hint (`sk-ant-…abcd`).
|
|
32
32
|
*/
|
|
33
|
-
import { ASSISTANT_ACCESS, ASSISTANT_EFFORTS, ASSISTANT_OWN_WAYS, ASSISTANT_PAYERS, HAND_OFF_POLICIES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
33
|
+
import { ASSISTANT_ACCESS, ASSISTANT_EFFORTS, ASSISTANT_OWN_WAYS, ASSISTANT_PAYERS, ASSISTANT_SCREEN, HAND_OFF_POLICIES, } from '@columenlabs/lumen-ui-types/assistant';
|
|
34
34
|
import { AssistantError } from './engine.js';
|
|
35
35
|
import { PROVIDERS } from './models.js';
|
|
36
36
|
import { keyDocName, planDocName, } from './state.js';
|
|
@@ -89,6 +89,8 @@ function settingsChange(a, groups, b) {
|
|
|
89
89
|
return 'No such hand off.';
|
|
90
90
|
if (p.advice !== undefined && p.advice !== 'off' && !tierIds.has(p.advice))
|
|
91
91
|
return 'Advice is off or a tier.';
|
|
92
|
+
if (p.screen !== undefined && !oneOf(ASSISTANT_SCREEN, p.screen))
|
|
93
|
+
return 'screen is off or move.';
|
|
92
94
|
if (p.access !== undefined) {
|
|
93
95
|
if (!p.access || typeof p.access !== 'object')
|
|
94
96
|
return 'access is {group: off | read | draft}.';
|
|
@@ -127,6 +129,7 @@ function settingsChange(a, groups, b) {
|
|
|
127
129
|
...(p.tasks !== undefined ? { tasks: { ...(d.tasks ?? {}), ...p.tasks } } : {}),
|
|
128
130
|
...(p.hand_off !== undefined ? { hand_off: p.hand_off } : {}),
|
|
129
131
|
...(p.advice !== undefined ? { advice: p.advice } : {}),
|
|
132
|
+
...(p.screen !== undefined ? { screen: p.screen } : {}),
|
|
130
133
|
...(p.access !== undefined ? { access: { ...(d.access ?? {}), ...p.access } } : {}),
|
|
131
134
|
...(p.web !== undefined ? { web: p.web.fetch } : {}),
|
|
132
135
|
...(p.pay !== undefined ? { pay: p.pay } : {}),
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tier runs on and money left this month, else the other; with neither the assistant waits and says
|
|
20
20
|
* why. A cap covers every provider of its payer.
|
|
21
21
|
*/
|
|
22
|
-
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
22
|
+
import type { AllowanceRequest, AssistantAccess, AssistantEffort, AssistantOwnWay, AssistantPayer, AssistantProvider, AssistantScreenSetting, AssistantWait, HandOffPolicy, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
23
23
|
import { type ChatgptTokens } from './chatgpt.js';
|
|
24
24
|
import { type Docs } from './store.js';
|
|
25
25
|
export interface KeyTest {
|
|
@@ -59,6 +59,8 @@ export interface PersonDoc {
|
|
|
59
59
|
tasks?: Record<string, string>;
|
|
60
60
|
hand_off?: HandOffPolicy;
|
|
61
61
|
advice?: string;
|
|
62
|
+
/** Whether the assistant may move the screen; Move when unset. */
|
|
63
|
+
screen?: AssistantScreenSetting;
|
|
62
64
|
access?: Record<string, AssistantAccess>;
|
|
63
65
|
web?: boolean;
|
|
64
66
|
pay?: AssistantPayer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.192",
|
|
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.192",
|
|
56
|
+
"@columenlabs/lumen-ui-types": "0.1.192",
|
|
57
57
|
"agents": "0.32.0",
|
|
58
58
|
"ai": "7.0.162",
|
|
59
59
|
"just-bash": "3.6.0",
|