@columenlabs/lumen-ai 0.1.88 → 0.1.89

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.
@@ -160,6 +160,12 @@ export declare class Assistant {
160
160
  readonly state: State;
161
161
  private readonly groups;
162
162
  constructor(o: AssistantOptions);
163
+ /**
164
+ * This assistant for one request: its documents are read once each (`Docs.cached`), so a page of
165
+ * Settings or a chat turn costs a handful of store calls, not dozens. `assistantRoutes` makes one
166
+ * per request itself.
167
+ */
168
+ forRequest(): Assistant;
163
169
  private anthropic;
164
170
  private openai;
165
171
  private clients;
@@ -172,6 +172,16 @@ export class Assistant {
172
172
  : {}),
173
173
  });
174
174
  }
175
+ /**
176
+ * This assistant for one request: its documents are read once each (`Docs.cached`), so a page of
177
+ * Settings or a chat turn costs a handful of store calls, not dozens. `assistantRoutes` makes one
178
+ * per request itself.
179
+ */
180
+ forRequest() {
181
+ const view = Object.create(this);
182
+ Object.defineProperty(view, 'state', { value: this.state.withDocs(this.state.docs.cached()) });
183
+ return view;
184
+ }
175
185
  anthropic(apiKey) {
176
186
  const f = this.o.fetch;
177
187
  return new Anthropic({ apiKey, maxRetries: 1, ...(f ? { fetch: f } : {}) });
@@ -6,5 +6,5 @@ export interface AssistantRoutesOptions {
6
6
  /** The verified caller, or null (403). The app decides who is an admin. */
7
7
  caller: (req: Request) => Awaitable<Caller | null>;
8
8
  }
9
- export declare function assistantRoutes(a: Assistant, o: AssistantRoutesOptions): (req: Request) => Promise<Response>;
9
+ export declare function assistantRoutes(app: Assistant, o: AssistantRoutesOptions): (req: Request) => Promise<Response>;
10
10
  export {};
@@ -121,10 +121,12 @@ function settingsChange(a, groups, b) {
121
121
  };
122
122
  };
123
123
  }
124
- export function assistantRoutes(a, o) {
124
+ export function assistantRoutes(app, o) {
125
125
  const base = o.base.replace(/\/+$/, '');
126
- const docs = a.state.docs;
127
126
  return async (req) => {
127
+ // One request's view: each stored document is read once however many routes ask for it.
128
+ const a = app.forRequest();
129
+ const docs = a.state.docs;
128
130
  const url = new URL(req.url);
129
131
  if (!url.pathname.startsWith(base))
130
132
  return fail('Not found.', 404);
@@ -127,6 +127,8 @@ export declare class State {
127
127
  fetch?: typeof fetch;
128
128
  allowance?: AllowanceConfig;
129
129
  });
130
+ /** The same state over other documents (a request's `Docs.cached`). */
131
+ withDocs(docs: Docs): State;
130
132
  get allowanceConfig(): AllowanceConfig | undefined;
131
133
  allowance(id: string): Promise<AllowanceDoc>;
132
134
  setAllowance(id: string, change: (d: AllowanceDoc) => AllowanceDoc): Promise<AllowanceDoc | null>;
@@ -32,6 +32,10 @@ export class State {
32
32
  this.docs = docs;
33
33
  this.o = o;
34
34
  }
35
+ /** The same state over other documents (a request's `Docs.cached`). */
36
+ withDocs(docs) {
37
+ return new State(docs, this.o);
38
+ }
35
39
  get allowanceConfig() {
36
40
  return this.o.allowance;
37
41
  }
@@ -61,12 +61,26 @@ export declare function sqlAssistantStore(sql: SqlRunner): AssistantStore;
61
61
  export declare class Docs {
62
62
  private readonly store;
63
63
  private readonly sealer?;
64
- constructor(store: AssistantStore, sealer?: Sealer | undefined);
64
+ /** Opened values already read in this request (`cached`), by scope and key. */
65
+ private readonly memo?;
66
+ constructor(store: AssistantStore, sealer?: Sealer | undefined,
67
+ /** Opened values already read in this request (`cached`), by scope and key. */
68
+ memo?: Map<string, Promise<string | null>> | undefined);
69
+ /**
70
+ * The same documents with each one read once: one request (a Settings page, a chat turn) reads
71
+ * a person's settings, keys and spend many times, and each read is a store call and an unseal.
72
+ * Writes keep it current; `update` always reads the store, so a change never builds on a value
73
+ * another request has since replaced. Make one per request, never one per app.
74
+ */
75
+ cached(): Docs;
65
76
  private seal;
66
77
  private open;
78
+ private id;
79
+ private fresh;
67
80
  read<T>(scope: string, key: string): Promise<T | null>;
68
81
  write<T>(scope: string, key: string, value: T, meta?: string): Promise<void>;
69
- /** Read, change and write back; `change` returning null removes it. */
82
+ /** Read (from the store, never the request's copy), change and write back; `change` returning
83
+ * null removes it. */
70
84
  update<T>(scope: string, key: string, change: (prev: T | null) => T | null): Promise<T | null>;
71
85
  remove(scope: string, key: string): Promise<void>;
72
86
  list(scope: string, prefix: string, limit: number): Promise<{
@@ -84,30 +84,66 @@ export function sqlAssistantStore(sql) {
84
84
  export class Docs {
85
85
  store;
86
86
  sealer;
87
- constructor(store, sealer) {
87
+ memo;
88
+ constructor(store, sealer,
89
+ /** Opened values already read in this request (`cached`), by scope and key. */
90
+ memo) {
88
91
  this.store = store;
89
92
  this.sealer = sealer;
93
+ this.memo = memo;
94
+ }
95
+ /**
96
+ * The same documents with each one read once: one request (a Settings page, a chat turn) reads
97
+ * a person's settings, keys and spend many times, and each read is a store call and an unseal.
98
+ * Writes keep it current; `update` always reads the store, so a change never builds on a value
99
+ * another request has since replaced. Make one per request, never one per app.
100
+ */
101
+ cached() {
102
+ return new Docs(this.store, this.sealer, new Map());
90
103
  }
91
104
  seal = (s) => (this.sealer ? this.sealer.seal(s) : Promise.resolve(s));
92
105
  open = (s) => (this.sealer ? this.sealer.open(s) : Promise.resolve(s));
93
- async read(scope, key) {
106
+ id = (scope, key) => `${scope}\u0000${key}`;
107
+ async fresh(scope, key) {
94
108
  const d = await this.store.get(scope, key);
95
- return d ? JSON.parse(await this.open(d.value)) : null;
109
+ return d ? this.open(d.value) : null;
110
+ }
111
+ async read(scope, key) {
112
+ let text;
113
+ if (this.memo) {
114
+ const id = this.id(scope, key);
115
+ const hit = this.memo.get(id);
116
+ text = hit ?? this.fresh(scope, key);
117
+ if (!hit) {
118
+ this.memo.set(id, text);
119
+ text.catch(() => this.memo?.delete(id));
120
+ }
121
+ }
122
+ else
123
+ text = this.fresh(scope, key);
124
+ const t = await text;
125
+ // Parsed each time, so a caller that changes what it got never changes the next read.
126
+ return t === null ? null : JSON.parse(t);
96
127
  }
97
128
  async write(scope, key, value, meta) {
98
- await this.store.put(scope, key, await this.seal(JSON.stringify(value)), meta === undefined ? null : await this.seal(meta));
129
+ const text = JSON.stringify(value);
130
+ await this.store.put(scope, key, await this.seal(text), meta === undefined ? null : await this.seal(meta));
131
+ this.memo?.set(this.id(scope, key), Promise.resolve(text));
99
132
  }
100
- /** Read, change and write back; `change` returning null removes it. */
133
+ /** Read (from the store, never the request's copy), change and write back; `change` returning
134
+ * null removes it. */
101
135
  async update(scope, key, change) {
102
- const next = change(await this.read(scope, key));
136
+ const t = await this.fresh(scope, key);
137
+ const next = change(t === null ? null : JSON.parse(t));
103
138
  if (next === null)
104
- await this.store.remove(scope, key);
139
+ await this.remove(scope, key);
105
140
  else
106
141
  await this.write(scope, key, next);
107
142
  return next;
108
143
  }
109
- remove(scope, key) {
110
- return this.store.remove(scope, key);
144
+ async remove(scope, key) {
145
+ await this.store.remove(scope, key);
146
+ this.memo?.set(this.id(scope, key), Promise.resolve(null));
111
147
  }
112
148
  async list(scope, prefix, limit) {
113
149
  const rows = await this.store.list(scope, prefix, limit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
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",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@anthropic-ai/sdk": "0.133.0",
44
- "@columenlabs/lumen-ops": "0.1.88",
45
- "@columenlabs/lumen-ui-types": "0.1.88",
44
+ "@columenlabs/lumen-ops": "0.1.89",
45
+ "@columenlabs/lumen-ui-types": "0.1.89",
46
46
  "openai": "7.32.0",
47
47
  "zod": "^4.3.6"
48
48
  },