@columenlabs/lumen-ai 0.1.200 → 0.1.202

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.
@@ -16,7 +16,7 @@ import { Think, } from '@cloudflare/think';
16
16
  import { createCompactFunction } from 'agents/sessions';
17
17
  import { generateText } from 'ai';
18
18
  import { CONNECTOR_GROUP, checkConnectorUrl, connectorGroup, connectorId, connectorSource, } from './connectors.js';
19
- import { aiModel, planFallback, turnSettings } from './models.js';
19
+ import { aiModel, cachedTurn, planFallback, turnSettings } from './models.js';
20
20
  import { screenMapOf, screenNews, screenOutcomesOf, screenRules, screenTool, } from './screen.js';
21
21
  import { LAZY_AT, TOOL_SEARCH, lazyTools } from './search.js';
22
22
  import { chunkEvents, itemsOf } from './stream.js';
@@ -79,6 +79,7 @@ export function chatUsage(parts, context) {
79
79
  read: was.read + p.read,
80
80
  out: was.out + p.out,
81
81
  usd: was.usd + p.usd,
82
+ ...(was.runs || p.runs ? { runs: (was.runs ?? 0) + (p.runs ?? 0) } : {}),
82
83
  }
83
84
  : { ...p });
84
85
  }
@@ -521,6 +522,7 @@ export class LumenAgent extends Think {
521
522
  this.#turn.lazy = lazy;
522
523
  }
523
524
  const turn = this.#turn;
525
+ const cached = cachedTurn(system, withLead(ctx.messages, `${this.#news()}${this.#screenNews()}${plan.lead ?? ''}`));
524
526
  return {
525
527
  model: this.#model(plan, () => {
526
528
  const fb = plan.fallback;
@@ -530,8 +532,9 @@ export class LumenAgent extends Think {
530
532
  turn.main = new UsageMeter(fb.model, fb.label, false);
531
533
  turn.meters.push(turn.main);
532
534
  }),
533
- instructions: system,
534
- messages: withLead(ctx.messages, `${this.#news()}${this.#screenNews()}${plan.lead ?? ''}`),
535
+ // Think types instructions as a string; the SDK it hands them to takes a system message too.
536
+ instructions: cached.instructions,
537
+ messages: cached.messages,
535
538
  tools,
536
539
  ...(lazy ? { activeTools: lazy.active() } : {}),
537
540
  ...(plan.first ? { toolChoice: { type: 'tool', toolName: plan.first } } : {}),
@@ -1,5 +1,5 @@
1
1
  import type { AssistantEffort } from '@columenlabs/lumen-ui-types/assistant';
2
- import type { JSONValue, LanguageModel } from 'ai';
2
+ import type { JSONValue, LanguageModel, ModelMessage, SystemModelMessage } from 'ai';
3
3
  import type { ModelDef } from '../assistant/models.js';
4
4
  /** The key a model runs on. `plan`: an OpenAI access token from a ChatGPT plan sign-in. */
5
5
  export interface ModelKey {
@@ -24,6 +24,14 @@ export interface AiModelOptions {
24
24
  export declare function planFetch(base?: typeof fetch, name?: string): typeof fetch;
25
25
  /** The AI SDK model a tier runs: its provider's client on the payer's key. */
26
26
  export declare function aiModel(model: ModelDef, key: ModelKey, o?: AiModelOptions): LanguageModel;
27
+ /**
28
+ * A turn's instructions and messages with Claude's two lasting cache breakpoints: the instructions
29
+ * as a system message that carries one, and the message before the newest person's message.
30
+ */
31
+ export declare function cachedTurn(instructions: string, messages: ModelMessage[]): {
32
+ instructions: SystemModelMessage;
33
+ messages: ModelMessage[];
34
+ };
27
35
  /** A tier's effort and output cap as a turn's settings, with Claude's prompt cache on. */
28
36
  export declare function turnSettings(model: ModelDef, o: {
29
37
  effort: AssistantEffort;
@@ -88,12 +88,40 @@ export function aiModel(model, key, o = {}) {
88
88
  return createOpenAI({ apiKey: key.apiKey, ...(f ? { fetch: f } : {}) }).responses(model.id);
89
89
  }
90
90
  /**
91
- * Claude's prompt cache: one breakpoint on the request, which Claude moves to the newest block on
92
- * each call, so a turn's later steps and the next turn read the tools, instructions and chat so
93
- * far from cache (a tenth of the input price) instead of paying for them again. Sent whatever the
94
- * tier's provider, so a plan's fallback to Claude caches too; other providers ignore it.
91
+ * Claude's prompt cache. The request's own breakpoint follows the newest block, so a turn's later
92
+ * steps read the steps before them. It alone never helps the next turn: the bearings put before
93
+ * the newest message are gone from it by then, so that prefix never comes again. `cachedTurn`
94
+ * adds two breakpoints that do: after the instructions (tools and instructions) and on the last
95
+ * message before the newest question (the chat so far). Sent whatever the tier's provider, so a
96
+ * plan's fallback to Claude caches too; other providers ignore it.
95
97
  */
96
98
  const CLAUDE_CACHE = { cacheControl: { type: 'ephemeral' } };
99
+ const CACHED = { anthropic: CLAUDE_CACHE };
100
+ /**
101
+ * A turn's instructions and messages with Claude's two lasting cache breakpoints: the instructions
102
+ * as a system message that carries one, and the message before the newest person's message.
103
+ */
104
+ export function cachedTurn(instructions, messages) {
105
+ let i = messages.length - 1;
106
+ while (i >= 0 && messages[i]?.role !== 'user')
107
+ i -= 1;
108
+ const before = i - 1;
109
+ const marked = before >= 0
110
+ ? messages.map((m, n) => n === before
111
+ ? {
112
+ ...m,
113
+ providerOptions: {
114
+ ...m.providerOptions,
115
+ anthropic: { ...m.providerOptions?.anthropic, ...CLAUDE_CACHE },
116
+ },
117
+ }
118
+ : m)
119
+ : messages;
120
+ return {
121
+ instructions: { role: 'system', content: instructions, providerOptions: CACHED },
122
+ messages: marked,
123
+ };
124
+ }
97
125
  /** A tier's effort and output cap as a turn's settings, with Claude's prompt cache on. */
98
126
  export function turnSettings(model, o) {
99
127
  const out = o.plan || !o.out ? {} : { maxOutputTokens: o.out };
@@ -32,5 +32,7 @@ export declare class UsageMeter {
32
32
  read: number;
33
33
  usd: number;
34
34
  };
35
+ /** Marks this part as one hand-off to a higher tier; parts of the same label add up their runs. */
36
+ handOff(): this;
35
37
  part(): UsagePart;
36
38
  }
@@ -40,6 +40,11 @@ export class UsageMeter {
40
40
  this.p.usd += t.usd;
41
41
  return t;
42
42
  }
43
+ /** Marks this part as one hand-off to a higher tier; parts of the same label add up their runs. */
44
+ handOff() {
45
+ this.p.runs = 1;
46
+ return this;
47
+ }
43
48
  part() {
44
49
  return { ...this.p };
45
50
  }
@@ -426,7 +426,7 @@ export class Assistant {
426
426
  return {
427
427
  error: `${tier.label} runs on ${provider} and no ${provider} key is set for whoever pays this answer: answer yourself.`,
428
428
  };
429
- const meter = tt.meter(sub.model, sub.meter.label, !!key.plan);
429
+ const meter = tt.meter(sub.model, sub.meter.label, !!key.plan).handOff();
430
430
  c.counts.handoffs += 1;
431
431
  if (this.tiers.indexOf(tier) > this.tiers.indexOf(c.counts.end))
432
432
  c.counts.end = tier;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.200",
3
+ "version": "0.1.202",
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.200",
56
- "@columenlabs/lumen-ui-types": "0.1.200",
55
+ "@columenlabs/lumen-ops": "0.1.202",
56
+ "@columenlabs/lumen-ui-types": "0.1.202",
57
57
  "agents": "0.32.0",
58
58
  "ai": "7.0.162",
59
59
  "just-bash": "3.6.0",