@columenlabs/lumen-ai 0.1.71 → 0.1.73
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 +46 -1
- package/dist/assistant/engine.d.ts +157 -0
- package/dist/assistant/engine.js +1064 -0
- package/dist/assistant/index.d.ts +18 -0
- package/dist/assistant/index.js +18 -0
- package/dist/assistant/models.d.ts +72 -0
- package/dist/assistant/models.js +69 -0
- package/dist/assistant/routes.d.ts +10 -0
- package/dist/assistant/routes.js +319 -0
- package/dist/assistant/state.d.ts +109 -0
- package/dist/assistant/state.js +138 -0
- package/dist/assistant/store.d.ts +78 -0
- package/dist/assistant/store.js +120 -0
- package/dist/assistant/tools.d.ts +49 -0
- package/dist/assistant/tools.js +110 -0
- package/dist/assistant/web.d.ts +45 -0
- package/dist/assistant/web.js +172 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @columenlabs/lumen-ai
|
|
2
2
|
|
|
3
|
-
Assistant and agent parts for apps on Cloudflare Workers.
|
|
3
|
+
Assistant and agent parts for apps on Cloudflare Workers. Serving an MCP server from a Worker, keeping only the read tools for a read-only caller, metering every tool call, and the server side of a chat assistant (`@columenlabs/lumen-ai/assistant`).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -54,3 +54,48 @@ app.all('/mcp', async (c) => {
|
|
|
54
54
|
## Versions
|
|
55
55
|
|
|
56
56
|
Every merge to `main` publishes the next patch of every Lumen package at once.
|
|
57
|
+
|
|
58
|
+
## A chat assistant
|
|
59
|
+
|
|
60
|
+
`@columenlabs/lumen-ai/assistant` is the server behind a chat side panel: Claude on the app's own tiers, the app's tools from any MCP server, the web, drafts the person saves, hand off to a higher tier, advice, and a monthly spend cap per person. The panel and Settings sections are `@columenlabs/lumen-ui`'s; the shared types are `@columenlabs/lumen-ui-types/assistant`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
assistantRoutes,
|
|
65
|
+
createAssistant,
|
|
66
|
+
doSql,
|
|
67
|
+
mcpServerTools,
|
|
68
|
+
sqlAssistantStore,
|
|
69
|
+
} from '@columenlabs/lumen-ai/assistant';
|
|
70
|
+
|
|
71
|
+
const assistant = createAssistant({
|
|
72
|
+
name: 'Helper',
|
|
73
|
+
system: 'You help people with their notes. Use the notes tools for facts.',
|
|
74
|
+
// Smallest first. The app names them; a person picks each tier's model and effort.
|
|
75
|
+
tiers: [
|
|
76
|
+
{ id: 'fast', label: 'Fast', about: 'Quick lookups', model: 'claude-haiku-5-5' },
|
|
77
|
+
{ id: 'steady', label: 'Steady', about: 'Most work', model: 'claude-sonnet-5-5' },
|
|
78
|
+
{ id: 'deep', label: 'Deep', about: 'Hard judgement', model: 'claude-opus-5-5' },
|
|
79
|
+
],
|
|
80
|
+
groups: [
|
|
81
|
+
{ id: 'notes', label: 'Notes', about: 'Read and edit notes', tools: (caller) => mcpServerTools(() => notesServer(caller.id)) },
|
|
82
|
+
],
|
|
83
|
+
web: { fetch: { blockHosts: ['app.example.com'] }, scrapeql: { url: 'https://scrapeql.example.com/mcp' } },
|
|
84
|
+
store: sqlAssistantStore(doSql(ctx.storage.sql)), // or d1Sql(env.DB)
|
|
85
|
+
sealer, // the app's own sealing: keys and chats are stored sealed
|
|
86
|
+
appKey: async () => env.ANTHROPIC_API_KEY, // when no key is set in Settings
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const routes = assistantRoutes(assistant, {
|
|
90
|
+
base: '/api/assistant',
|
|
91
|
+
caller: (req) => ({ id: personId(req), admin: isAdmin(req) }),
|
|
92
|
+
});
|
|
93
|
+
app.all('/api/assistant/*', (c) => routes(c.req.raw));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Keys.** An admin pastes the app's Claude key (`PUT /key`); it is tested with one free call before it is kept, then sealed, and no route returns it. A person may add their own (`PUT /my-key`) with its own monthly cap.
|
|
97
|
+
- **Who pays.** The person's pick (the app's key by default) while it has money left this month under its cap (an admin sets the app key's cap per person, `PUT /app`), else the other; with neither the panel says why.
|
|
98
|
+
- **Tools.** Each group is Off, Read or Read + draft per person. A tool is a read when its MCP `readOnlyHint` is set; a write only drafts a card, and runs as the person when they press Save, after access is checked again. Use `mcpServerTools` (an SDK `McpServer`), `mcpFetchTools` (a server served as a fetch handler, any SDK), `remoteMcpTools` or `plainTools`.
|
|
99
|
+
- **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`).
|
|
100
|
+
- **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.
|
|
101
|
+
- **Chats** stream as server-sent events (`POST /chat`); closing the request stops the answer and keeps what was said. Past 150k tokens, or on `/compact`, the history sent to the model becomes a summary. Run `assistant.purge()` once a day to drop chats 30 days after their last message.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantStatus, DraftItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
import { type ModelDef, type TaskDef, type TierDef } from './models.js';
|
|
3
|
+
import { State } from './state.js';
|
|
4
|
+
import { type AssistantStore, type Sealer } from './store.js';
|
|
5
|
+
import { type ToolSource } from './tools.js';
|
|
6
|
+
import { type AccessToken, type WebFetchOptions } from './web.js';
|
|
7
|
+
/** Who is asking: their id (the store's scope for their things) and whether they are an admin. */
|
|
8
|
+
export interface Caller {
|
|
9
|
+
id: string;
|
|
10
|
+
admin: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** A group of the app's tools the person sets Off, Read or Read + draft. */
|
|
13
|
+
export interface ToolGroup {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
about: string;
|
|
17
|
+
/** Where a person starts: `read` by default. */
|
|
18
|
+
access?: AssistantAccess;
|
|
19
|
+
tools: (caller: Caller) => ToolSource | ToolSource[];
|
|
20
|
+
}
|
|
21
|
+
/** What one step or answer was, for the app's meter: shapes only, never content. */
|
|
22
|
+
export type StepRecord = {
|
|
23
|
+
kind: 'tool';
|
|
24
|
+
tool: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
ms: number;
|
|
27
|
+
rows?: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
tier: string;
|
|
30
|
+
handoff: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'draft';
|
|
33
|
+
tool: string;
|
|
34
|
+
tier: string;
|
|
35
|
+
handoff: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'turn';
|
|
38
|
+
outcome: 'ok' | 'stopped' | 'error';
|
|
39
|
+
ms: number;
|
|
40
|
+
tier: string;
|
|
41
|
+
end_tier: string;
|
|
42
|
+
handoffs: number;
|
|
43
|
+
advice: number;
|
|
44
|
+
steps: number;
|
|
45
|
+
calls: number;
|
|
46
|
+
in: number;
|
|
47
|
+
out: number;
|
|
48
|
+
usd: number;
|
|
49
|
+
};
|
|
50
|
+
export interface AssistantOptions {
|
|
51
|
+
/** What people call it ("Sage"): the panel, Settings and the model's own name. */
|
|
52
|
+
name: string;
|
|
53
|
+
/** The app's instructions: what it is, what its tools are for. */
|
|
54
|
+
system: string | ((caller: Caller) => string | Promise<string>);
|
|
55
|
+
/** The app's tiers, smallest first. */
|
|
56
|
+
tiers: TierDef[];
|
|
57
|
+
/** Tasks a person picks a starting tier for; the chat on the smallest tier by default. */
|
|
58
|
+
tasks?: TaskDef[];
|
|
59
|
+
/** Models a tier may run; Claude's by default. */
|
|
60
|
+
models?: ModelDef[];
|
|
61
|
+
groups?: ToolGroup[];
|
|
62
|
+
web?: {
|
|
63
|
+
/** `fetch` options, or false for no fetch. */
|
|
64
|
+
fetch?: WebFetchOptions | false;
|
|
65
|
+
/** ScrapeQL's MCP address; its tools join once an admin adds the Access token. */
|
|
66
|
+
scrapeql?: {
|
|
67
|
+
url: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
store: AssistantStore;
|
|
71
|
+
sealer?: Sealer;
|
|
72
|
+
/** The app's key when none is stored in Settings (a Worker secret). */
|
|
73
|
+
appKey?: () => Promise<string | null | undefined>;
|
|
74
|
+
/** USD a person may spend a month when nobody set a cap; 10. */
|
|
75
|
+
defaultCap?: number;
|
|
76
|
+
/** Months and dates in this zone; UTC by default. */
|
|
77
|
+
timeZone?: string;
|
|
78
|
+
/** Chats are deleted this many days after their last message (`purge`); 30. */
|
|
79
|
+
keepDays?: number;
|
|
80
|
+
/** The fetch Anthropic and ScrapeQL calls go through. */
|
|
81
|
+
fetch?: typeof fetch;
|
|
82
|
+
onStep?: (s: StepRecord) => void | Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
/** A turn's failure the person can act on. */
|
|
85
|
+
export declare class AssistantError extends Error {
|
|
86
|
+
readonly code: string;
|
|
87
|
+
readonly status: number;
|
|
88
|
+
constructor(message: string, code: string, status?: number);
|
|
89
|
+
}
|
|
90
|
+
type Emit = (event: string, data: unknown) => void;
|
|
91
|
+
export declare class Assistant {
|
|
92
|
+
private readonly o;
|
|
93
|
+
readonly name: string;
|
|
94
|
+
readonly tiers: TierDef[];
|
|
95
|
+
readonly tasks: TaskDef[];
|
|
96
|
+
readonly models: ModelDef[];
|
|
97
|
+
readonly state: State;
|
|
98
|
+
private readonly groups;
|
|
99
|
+
constructor(o: AssistantOptions);
|
|
100
|
+
private client;
|
|
101
|
+
private tierById;
|
|
102
|
+
private above;
|
|
103
|
+
private modelById;
|
|
104
|
+
/** The person's model and effort for a tier, or the tier's own when unset or gone. */
|
|
105
|
+
private pick;
|
|
106
|
+
private session;
|
|
107
|
+
/** The tier the person's chat runs on: their pick, else the chat task's start tier. */
|
|
108
|
+
private mainTier;
|
|
109
|
+
private access;
|
|
110
|
+
status(caller: Caller): Promise<AssistantStatus>;
|
|
111
|
+
listChats(caller: Caller): Promise<AssistantChatRow[]>;
|
|
112
|
+
readChat(caller: Caller, id: string): Promise<AssistantChat | null>;
|
|
113
|
+
removeChat(caller: Caller, id: string): Promise<void>;
|
|
114
|
+
/** Deletes chats past `keepDays` since their last message; run it once a day. */
|
|
115
|
+
purge(): Promise<number>;
|
|
116
|
+
private writeChat;
|
|
117
|
+
/** Opens the tools the person allows: each group's, then the web's. */
|
|
118
|
+
private openTools;
|
|
119
|
+
private toolsFor;
|
|
120
|
+
/** One answer as server-sent events. Abort `signal` to stop it. */
|
|
121
|
+
turnStream(caller: Caller, req: AssistantMessage, signal?: AbortSignal): ReadableStream<Uint8Array>;
|
|
122
|
+
turn(caller: Caller, req: AssistantMessage, emit: Emit, signal?: AbortSignal): Promise<void>;
|
|
123
|
+
/** What the kit adds to the app's instructions: tools, drafts, hand off, advice, limits. */
|
|
124
|
+
private rules;
|
|
125
|
+
/** The advisor for this answer, or null when off. Never weaker than the model it advises. */
|
|
126
|
+
private advisorFor;
|
|
127
|
+
private escalateTool;
|
|
128
|
+
/** The tiers the person's message asks for by name ("go deeper" is the next one up). */
|
|
129
|
+
private askedFor;
|
|
130
|
+
private handOff;
|
|
131
|
+
/** One tool call: a draft card for a write, else the call itself, timed. */
|
|
132
|
+
private runTool;
|
|
133
|
+
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
134
|
+
private loop;
|
|
135
|
+
/** Swaps the history for a short summary. False with nothing to compact. */
|
|
136
|
+
private compact;
|
|
137
|
+
/** How the person's cards ended since the last answer, once each. */
|
|
138
|
+
private cardNews;
|
|
139
|
+
/** The answer as later turns remember it: with what its tools returned, cut. */
|
|
140
|
+
private remember;
|
|
141
|
+
/** One free call (reading a model) that says whether Claude takes the key. */
|
|
142
|
+
testKey(apiKey: string): Promise<{
|
|
143
|
+
ok: boolean;
|
|
144
|
+
detail: string;
|
|
145
|
+
at: number;
|
|
146
|
+
}>;
|
|
147
|
+
/** Lists ScrapeQL's tools with the token: works or why not. */
|
|
148
|
+
testScrapeql(token: AccessToken): Promise<{
|
|
149
|
+
ok: boolean;
|
|
150
|
+
detail: string;
|
|
151
|
+
at: number;
|
|
152
|
+
}>;
|
|
153
|
+
/** Saves (runs, as the person) or dismisses a drafted card. Access is checked again first. */
|
|
154
|
+
settleDraft(caller: Caller, chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
|
155
|
+
}
|
|
156
|
+
export declare function createAssistant(o: AssistantOptions): Assistant;
|
|
157
|
+
export {};
|