@columenlabs/lumen-ai 0.1.271 → 0.1.274

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.
@@ -23,6 +23,11 @@ import { UsageMeter } from './usage.js';
23
23
  /** What a plan's extra tools get: the open tools, a meter of their own and the cap. */
24
24
  export interface TurnTools {
25
25
  opened: OpenedTools;
26
+ /**
27
+ * The agent's own file tools over its workspace (read, list, find, grep and the rest, as Think
28
+ * gives the main model), so a hand-off reads the same files the app wrote for this turn.
29
+ */
30
+ files: ToolSet;
26
31
  /** A new part of this answer's usage (a hand off), counted against the cap. */
27
32
  meter(model: ModelDef, label: string, free?: boolean): UsageMeter;
28
33
  /** Spent this answer reached what the payer has left. */
@@ -12,6 +12,7 @@
12
12
  * An `Assistant` gives both: `assistant.agentTurn(caller, input)` returns a plan and its settle.
13
13
  */
14
14
  import { anthropic } from '@ai-sdk/anthropic';
15
+ import { createAITools } from '@cloudflare/computer/tools/ai-sdk';
15
16
  import { Think, } from '@cloudflare/think';
16
17
  import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, ASSISTANT_IMAGE_TYPES, ASSISTANT_QUOTE_MAX, } from '@columenlabs/lumen-ui-types/assistant';
17
18
  import { createCompactFunction } from 'agents/sessions';
@@ -706,6 +707,7 @@ export class LumenAgent extends Think {
706
707
  };
707
708
  const more = plan.more?.({
708
709
  opened,
710
+ files: createAITools({ workspace: this.workspace }),
709
711
  over: () => plan.left !== undefined && this.#spent() >= plan.left,
710
712
  meter: (model, label, f = free) => {
711
713
  const m = new UsageMeter(model, label, f);
@@ -76,7 +76,7 @@ const WHY_TEXT = {
76
76
  check_failed: 'check failed',
77
77
  };
78
78
  /** A short line of what an error was, never an upstream body. */
79
- function shownError(err) {
79
+ function shownError(err, plan = false) {
80
80
  if (err instanceof AssistantError)
81
81
  return { message: err.message, code: err.code };
82
82
  const api = err instanceof Anthropic.APIError
@@ -100,6 +100,16 @@ function shownError(err) {
100
100
  // The AI SDK's errors (APICallError, and RetryError around its last one) carry the HTTP status as
101
101
  // statusCode: say what that status means, never the body; the body goes to the log only.
102
102
  const call = sdkCall(err);
103
+ // OpenAI names the reason in its body (a plan's usage limit answers 500): say that reason.
104
+ if (call?.code && /openai\.com|chatgpt\.com/.test(call.url))
105
+ return {
106
+ message: openaiStreamError({ code: call.code, message: call.message }, plan),
107
+ code: /usage_limit|insufficient_quota/.test(call.code)
108
+ ? 'usage_limit'
109
+ : /rate_limit/.test(call.code)
110
+ ? 'rate_limited'
111
+ : 'model_error',
112
+ };
103
113
  if (call) {
104
114
  console.error(JSON.stringify({
105
115
  event: 'assistant_upstream_error',
@@ -126,7 +136,10 @@ function shownError(err) {
126
136
  }
127
137
  return { message: 'Something went wrong while answering.', code: 'internal' };
128
138
  }
129
- /** An AI SDK call error's status and message, through RetryError's lastError and `cause`. */
139
+ /**
140
+ * An AI SDK call error's status, message, address and the provider's own error code (from its
141
+ * body: `error.code`, `error.type` or `detail.code`), through RetryError's lastError and `cause`.
142
+ */
130
143
  function sdkCall(err, depth = 0) {
131
144
  if (!err || typeof err !== 'object' || depth > 3)
132
145
  return null;
@@ -134,13 +147,41 @@ function sdkCall(err, depth = 0) {
134
147
  const name = typeof e.name === 'string' ? e.name : '';
135
148
  if (name === 'AI_RetryError' || name === 'RetryError')
136
149
  return sdkCall(e.lastError, depth + 1);
137
- if (name === 'AI_APICallError' || name === 'APICallError' || typeof e.statusCode === 'number')
150
+ if (name === 'AI_APICallError' || name === 'APICallError' || typeof e.statusCode === 'number') {
151
+ const code = bodyCode(e.data) ?? bodyCode(parseJson(e.responseBody));
138
152
  return {
139
153
  ...(typeof e.statusCode === 'number' && { status: e.statusCode }),
140
154
  message: typeof e.message === 'string' ? e.message : '',
155
+ url: typeof e.url === 'string' ? e.url : '',
156
+ ...(code && { code }),
141
157
  };
158
+ }
142
159
  return e.cause ? sdkCall(e.cause, depth + 1) : null;
143
160
  }
161
+ const parseJson = (v) => {
162
+ if (typeof v !== 'string')
163
+ return undefined;
164
+ try {
165
+ return JSON.parse(v);
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ };
171
+ /** A provider body's error code: `{error: {code | type}}` or `{detail: {code}}`. */
172
+ function bodyCode(v) {
173
+ if (!v || typeof v !== 'object')
174
+ return undefined;
175
+ const o = v;
176
+ for (const x of [o.error, o.detail]) {
177
+ if (!x || typeof x !== 'object')
178
+ continue;
179
+ const c = x.code ?? x.type;
180
+ if (typeof c === 'string' && c)
181
+ return c.slice(0, 80);
182
+ }
183
+ return undefined;
184
+ }
144
185
  /**
145
186
  * A plain sentence for an error OpenAI sends inside the answer's stream (an `error` event, or a
146
187
  * failed response): what happened and what to do, by its code, with OpenAI's code in brackets so
@@ -416,7 +457,7 @@ export class Assistant {
416
457
  label: main.meter.label,
417
458
  system: (t) => rulesFor(main, t),
418
459
  lead: `${page}${handTo && reach.length
419
- ? `The person handed this message to ${handTo.label}: call escalate {tier: "${handTo.id}", why: "asked"} now, its task the message with every fact from this chat it needs, then answer from its memo.\n\n`
460
+ ? `The person handed this message to ${handTo.label}: call escalate {tier: "${handTo.id}", why: "asked"} now, its task the message with every fact from this chat it needs, then answer from its memo. If it answers with an error, do the task yourself with your tools.\n\n`
420
461
  : ''}${command}`,
421
462
  sources: await this.sourcesOf(caller, p),
422
463
  ...(fallback ? { fallback } : {}),
@@ -604,7 +645,7 @@ export class Assistant {
604
645
  }),
605
646
  instructions: c.system(tt.opened),
606
647
  prompt: `${c.message}\n\n${this.name} handed you this task (${WHY_TEXT[shown]}): ${task}\n\nWork it out with the tools, then end with a memo for ${this.name} of at most 200 words: the answer and the facts it rests on. ${this.name} answers the person.`,
607
- tools: tt.opened.tools,
648
+ tools: { ...tt.files, ...tt.opened.tools },
608
649
  stopWhen: stepCountIs(MAX_ROUNDS),
609
650
  prepareStep: () => (tt.over() ? { activeTools: [] } : undefined),
610
651
  onStepFinish: (s) => {
@@ -639,7 +680,9 @@ export class Assistant {
639
680
  tier: tier.id,
640
681
  handoff: true,
641
682
  })).catch(() => undefined);
642
- return { error: `${sub.meter.label} failed: ${shownError(err).message}` };
683
+ return {
684
+ error: `${sub.meter.label} could not take the task: ${shownError(err, !!key.plan).message} The task is still yours: do it now with your own tools, and tell the person only this reason if you cannot.`,
685
+ };
643
686
  }
644
687
  },
645
688
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ai",
3
- "version": "0.1.271",
3
+ "version": "0.1.274",
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",
@@ -55,9 +55,10 @@
55
55
  "@ai-sdk/anthropic": "4.0.96",
56
56
  "@ai-sdk/openai": "4.0.108",
57
57
  "@anthropic-ai/sdk": "0.133.0",
58
+ "@cloudflare/computer": "0.8.0",
58
59
  "@cloudflare/think": "0.23.0",
59
- "@columenlabs/lumen-ops": "0.1.271",
60
- "@columenlabs/lumen-ui-types": "0.1.271",
60
+ "@columenlabs/lumen-ops": "0.1.274",
61
+ "@columenlabs/lumen-ui-types": "0.1.274",
61
62
  "agents": "0.32.0",
62
63
  "ai": "7.0.162",
63
64
  "just-bash": "3.6.0",