@columenlabs/lumen-ai 0.1.210 → 0.1.211

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 CHANGED
@@ -237,6 +237,8 @@ Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agen
237
237
 
238
238
  **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.
239
239
 
240
+ **Images.** `AskInput.images` (base64 PNG, JPEG, WebP or GIF; at most `ASSISTANT_IMAGES_MAX` (4) and `ASSISTANT_IMAGE_CHARS` together) go into the user message as file parts (`imageParts`), so the model sees them and the chat shows them again. `LumenAssistant images` lets the person paste (Cmd+V) or drop images on the box; the composer scales each to 1568 px on its long edge. An app's `POST /chat` passes `images` on in its `AskInput`.
241
+
240
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.
241
243
 
242
244
  ```ts
@@ -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, AssistantScreenMap, AssistantScreenOutcome, ChatUsage, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
15
+ import { type AssistantChat, type AssistantEffort, type AssistantEvent, type AssistantImage, type AssistantRefNote, type AssistantScreenMap, type AssistantScreenOutcome, type ChatUsage, type 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';
@@ -116,6 +116,8 @@ export declare function withLead(messages: ModelMessage[], lead: string | undefi
116
116
  /** One message the person sent, with the page, refs and tier the composer added. */
117
117
  export interface AskInput {
118
118
  message: string;
119
+ /** Images to look at with the message; kept to the allowed types, count and size. */
120
+ images?: AssistantImage[];
119
121
  context?: {
120
122
  label: string;
121
123
  note?: unknown;
@@ -128,6 +130,15 @@ export interface AskInput {
128
130
  /** How earlier screen calls ended in the browser. */
129
131
  outcomes?: AssistantScreenOutcome[];
130
132
  }
133
+ /**
134
+ * The images a message may carry, as file parts: allowed types only, at most `ASSISTANT_IMAGES_MAX`,
135
+ * and no more than `ASSISTANT_IMAGE_CHARS` of base64 together; the rest are left out.
136
+ */
137
+ export declare function imageParts(images: unknown): {
138
+ type: 'file';
139
+ mediaType: string;
140
+ url: string;
141
+ }[];
131
142
  /** Where an answer's events go: the app's route, over RPC. */
132
143
  export interface AskSink {
133
144
  event(e: AssistantEvent): void | Promise<void>;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { anthropic } from '@ai-sdk/anthropic';
15
15
  import { Think, } from '@cloudflare/think';
16
+ import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, ASSISTANT_IMAGE_TYPES, } from '@columenlabs/lumen-ui-types/assistant';
16
17
  import { createCompactFunction } from 'agents/sessions';
17
18
  import { generateText } from 'ai';
18
19
  import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
@@ -66,6 +67,26 @@ export function withLead(messages, lead) {
66
67
  : [{ type: 'text', text: lead }, ...m.content];
67
68
  return [...messages.slice(0, i), { ...m, content }, ...messages.slice(i + 1)];
68
69
  }
70
+ /**
71
+ * The images a message may carry, as file parts: allowed types only, at most `ASSISTANT_IMAGES_MAX`,
72
+ * and no more than `ASSISTANT_IMAGE_CHARS` of base64 together; the rest are left out.
73
+ */
74
+ export function imageParts(images) {
75
+ if (!Array.isArray(images))
76
+ return [];
77
+ const out = [];
78
+ let chars = 0;
79
+ for (const i of images.slice(0, ASSISTANT_IMAGES_MAX)) {
80
+ const type = ASSISTANT_IMAGE_TYPES.find((t) => t === i?.media_type);
81
+ if (!type || typeof i?.data !== 'string' || !/^[A-Za-z0-9+/]+=*$/.test(i.data))
82
+ continue;
83
+ if (chars + i.data.length > ASSISTANT_IMAGE_CHARS)
84
+ break;
85
+ chars += i.data.length;
86
+ out.push({ type: 'file', mediaType: type, url: `data:${type};base64,${i.data}` });
87
+ }
88
+ return out;
89
+ }
69
90
  /** Usage parts added up: same label, one part. */
70
91
  export function chatUsage(parts, context) {
71
92
  const by = new Map();
@@ -174,7 +195,9 @@ export class LumenAgent extends Think {
174
195
  kindOf: (n) => this.#turn?.tools.info.find((i) => i.name === n)?.kind,
175
196
  });
176
197
  const map = screenMapOf(input.screen);
177
- this.#input = { ...input, ...(map ? { screen: map } : {}) };
198
+ // The images go in the message's parts; the turn's plan reads the rest.
199
+ const { images: _images, ...asked } = input;
200
+ this.#input = { ...asked, ...(map ? { screen: map } : {}) };
178
201
  if (!map)
179
202
  delete this.#input.screen;
180
203
  let failed = false;
@@ -182,7 +205,7 @@ export class LumenAgent extends Think {
182
205
  await this.chat({
183
206
  id: `u_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`,
184
207
  role: 'user',
185
- parts: [{ type: 'text', text: input.message }],
208
+ parts: [{ type: 'text', text: input.message }, ...imageParts(input.images)],
186
209
  metadata: meta,
187
210
  }, {
188
211
  onStart: () => undefined,
@@ -138,10 +138,14 @@ export function itemsOf(messages, cards, screens = new Map()) {
138
138
  .map((p) => String(p.text ?? ''))
139
139
  .join('');
140
140
  const meta = (obj(m.metadata) ?? {});
141
+ const images = m.parts
142
+ .filter((p) => p.type === 'file' && typeof p.url === 'string' && p.url.startsWith('data:image/'))
143
+ .map((p) => p.url);
141
144
  items.push({
142
145
  kind: 'user',
143
146
  text,
144
147
  at: meta.at ?? 0,
148
+ ...(images.length ? { images } : {}),
145
149
  ...(meta.context?.label ? { context: meta.context.label } : {}),
146
150
  ...(meta.refs?.length ? { refs: meta.refs } : {}),
147
151
  ...(meta.tier ? { tier: meta.tier } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.210",
3
+ "version": "0.1.211",
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.210",
56
- "@columenlabs/lumen-ui-types": "0.1.210",
55
+ "@columenlabs/lumen-ops": "0.1.211",
56
+ "@columenlabs/lumen-ui-types": "0.1.211",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",