@columenlabs/lumen-ai 0.1.217 → 0.1.219

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
@@ -241,8 +241,12 @@ Drafts and writes are cards in the chat's own storage (`lumen_cards` in the agen
241
241
 
242
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.
243
243
 
244
+ **Steps.** One answer makes at most `plan.maxSteps` model calls (`MAX_STEPS`, 8). The last one runs with no tools and `LAST_STEP_NOTE`, so an answer always ends in words: what it found, and what is left when the task did not fit.
245
+
244
246
  **Steer and queue.** While an answer runs, the panel's box stays open: Enter steers the message into the answer (`steer(message)` on the agent; the app routes `POST /chats/:id/steer` `{ message }` to it and answers `{ steered }`), Alt+Enter queues it for after. A steer joins at the answer's next step, after the tool it is on, with `STEER_NOTE` before it for the model; the panel shows it there marked Steered (event `steered`), and `view()` keeps it in place (`SteerNote`, under the message the answer is to). A steer the answer ends before reading comes back as `unsteered` and goes first in the queue. `plan.steer: false` turns it off for a model; an app without the route (404) shows Queue only. Queued messages each go as their own message when the answer before them ends; each has Steer now, Edit and Remove.
245
247
 
248
+ **Retry.** A turn that ended in an error shows a Retry icon (⟳) beside Copy, always visible, while no answer runs and someone can pay (`status.wait` unset; hidden otherwise, never disabled). It sends the turn's words, `@` parts, `#` tier and images again as a new message (`chat.retry(at, user, extra)` in `useAssistantChat`), marked Retry above the bubble; the old error turns grey (`retried` on the error item) and stops offering Retry. Nothing changes on the server: the retried message is an ordinary message.
249
+
246
250
  ```ts
247
251
  class Chat extends LumenAgent<Env> {
248
252
  #settle?: (end: AgentTurnEnd) => Promise<void>;
@@ -74,7 +74,7 @@ export interface AgentTurnPlan {
74
74
  left?: number;
75
75
  /** The person lets the assistant move their screen: the `screen` tool joins when the browser sent its map. */
76
76
  screen?: boolean;
77
- /** Most model calls in one answer; 8. */
77
+ /** Most model calls in one answer (`MAX_STEPS`, 8); the last one answers in words, with no tools. */
78
78
  maxSteps?: number;
79
79
  /**
80
80
  * A message sent while the answer runs joins it at its next step (Steer); on unless `false`,
@@ -99,6 +99,10 @@ export interface AgentTurnEnd {
99
99
  capped: boolean;
100
100
  }
101
101
  export declare 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.";
102
+ /** The last call of an answer: no tools, so the answer never ends on a tool's result with nothing said. */
103
+ export declare const LAST_STEP_NOTE = "This is the answer's last call and you can call no more tools: answer now from what you found. If the task is not finished, say what is done, what is left and that you can carry on when asked.";
104
+ /** Most model calls in one answer when the plan names none. */
105
+ export declare const MAX_STEPS = 8;
102
106
  /** Put before a message the person steered into a running answer, for the model only. */
103
107
  export declare const STEER_NOTE = "The person sent this while you were working. Take it into account from here on; it does not cancel what you were asked before unless it says so.";
104
108
  /** A card in the chat: a draft the person saves or dismisses, or a write made at once (Undo). */
@@ -24,6 +24,10 @@ import { chunkEvents, itemsOf } from './stream.js';
24
24
  import { openAiTools, } from './tools.js';
25
25
  import { UsageMeter } from './usage.js';
26
26
  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.';
27
+ /** The last call of an answer: no tools, so the answer never ends on a tool's result with nothing said. */
28
+ export const LAST_STEP_NOTE = "This is the answer's last call and you can call no more tools: answer now from what you found. If the task is not finished, say what is done, what is left and that you can carry on when asked.";
29
+ /** Most model calls in one answer when the plan names none. */
30
+ export const MAX_STEPS = 8;
27
31
  /** Put before a message the person steered into a running answer, for the model only. */
28
32
  export const STEER_NOTE = 'The person sent this while you were working. Take it into account from here on; it does not cancel what you were asked before unless it says so.';
29
33
  /** Past this many tokens in the chat Think summarizes its older part. */
@@ -596,7 +600,7 @@ export class LumenAgent extends Think {
596
600
  tools,
597
601
  ...(lazy ? { activeTools: lazy.active() } : {}),
598
602
  ...(plan.first ? { toolChoice: { type: 'tool', toolName: plan.first } } : {}),
599
- maxSteps: plan.maxSteps ?? 8,
603
+ maxSteps: plan.maxSteps ?? MAX_STEPS,
600
604
  ...turnSettings(plan.model, {
601
605
  effort: plan.effort ?? 'default',
602
606
  ...(plan.out ? { out: plan.out } : {}),
@@ -620,6 +624,17 @@ export class LumenAgent extends Think {
620
624
  };
621
625
  return capped;
622
626
  }
627
+ // The last call answers in words: past it the loop stops, so a tool call there would end the
628
+ // answer on a tool's result with nothing said.
629
+ if (ctx.stepNumber >= (t.plan.maxSteps ?? MAX_STEPS) - 1) {
630
+ const last = {
631
+ ...messages,
632
+ activeTools: [],
633
+ toolChoice: 'none',
634
+ instructions: `${t.system}\n\n${LAST_STEP_NOTE}`,
635
+ };
636
+ return last;
637
+ }
623
638
  // A forced first call is for the first step only; tools a search found join each step.
624
639
  const auto = t.plan.first && ctx.stepNumber > 0;
625
640
  if (!auto && !t.lazy && !steered)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.217",
3
+ "version": "0.1.219",
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.217",
56
- "@columenlabs/lumen-ui-types": "0.1.217",
55
+ "@columenlabs/lumen-ops": "0.1.219",
56
+ "@columenlabs/lumen-ui-types": "0.1.219",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",