@columenlabs/lumen-ai 0.1.177 → 0.1.178
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 +14 -0
- package/dist/agent/agent.d.ts +37 -6
- package/dist/agent/agent.js +94 -15
- package/dist/agent/models.d.ts +2 -2
- package/dist/agent/tools.d.ts +9 -0
- package/dist/agent/tools.js +3 -0
- package/dist/agent/usage.d.ts +8 -1
- package/dist/agent/usage.js +5 -1
- package/dist/assistant/engine.d.ts +28 -0
- package/dist/assistant/engine.js +243 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -225,3 +225,17 @@ ctx.waitUntil(meaningStep({ def: notesFind, sql: d1Sql(env.DB), embed, index: en
|
|
|
225
225
|
| `UsageMeter`, `tokenUsage(u)` | Prices each step's tokens (cache reads and writes, reasoning) into one usage part |
|
|
226
226
|
|
|
227
227
|
A ChatGPT plan sign-in is free to the app (`priced` with `free`). Nothing here holds a key or a token: the app passes them per turn.
|
|
228
|
+
|
|
229
|
+
An app with an `Assistant` gets the whole plan from it: `assistant.agentTurn(caller, { message, tier?, context?, refs?, time_zone? })` returns `{ plan, settle }` with the person's tier and model on whoever pays, the tool groups at their access, the kit's rules, the bearings and the page before the message, `escalate` to a higher tier (its own usage part, inside the same cap) and Claude's advisor. `settle` adds the spend to the payer's month and reports the turn through `onStep`. It throws `AssistantError` when nobody can pay. Drafts, Undo and compaction still run only in `turn`.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
class Chat extends LumenAgent<Env> {
|
|
233
|
+
#settle?: (end: AgentTurnEnd) => Promise<void>;
|
|
234
|
+
async plan(turn: TurnContext) {
|
|
235
|
+
const { plan, settle } = await assistant.agentTurn(caller, { message: lastText(turn), ...turn.body });
|
|
236
|
+
this.#settle = settle;
|
|
237
|
+
return plan;
|
|
238
|
+
}
|
|
239
|
+
settle(end: AgentTurnEnd) { return this.#settle?.(end) ?? Promise.resolve(); }
|
|
240
|
+
}
|
|
241
|
+
```
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ChatResponseResult, type PrepareStepContext, type StepConfig, type StepContext, Think, type ThinkModel, type TurnConfig, type TurnContext } from '@cloudflare/think';
|
|
2
1
|
/**
|
|
3
2
|
* `LumenAgent`: one chat as a Cloudflare Think agent, run on the app's tiers, keys and tool groups.
|
|
4
3
|
* Think keeps the loop, the messages, streaming, resume and compaction; the app answers `plan` for
|
|
@@ -9,11 +8,24 @@ import { type ChatResponseResult, type PrepareStepContext, type StepConfig, type
|
|
|
9
8
|
* plan(turn) { return sagePlan(this.env, this.name, turn); }
|
|
10
9
|
* settle(s) { return recordSpend(this.env, this.name, s); }
|
|
11
10
|
* }
|
|
11
|
+
*
|
|
12
|
+
* An `Assistant` gives both: `assistant.agentTurn(caller, input)` returns a plan and its settle.
|
|
12
13
|
*/
|
|
14
|
+
import { type ChatResponseResult, type PrepareStepContext, type StepConfig, type StepContext, Think, type ThinkModel, type TurnConfig, type TurnContext } from '@cloudflare/think';
|
|
13
15
|
import type { AssistantEffort, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
16
|
+
import type { ModelMessage, ToolSet } from 'ai';
|
|
14
17
|
import type { ModelDef } from '../assistant/models.js';
|
|
15
18
|
import { type ModelKey } from './models.js';
|
|
16
|
-
import { type AiToolsOptions, type SourceAt } from './tools.js';
|
|
19
|
+
import { type AiToolsOptions, type OpenedTools, type SourceAt } from './tools.js';
|
|
20
|
+
import { UsageMeter } from './usage.js';
|
|
21
|
+
/** What a plan's extra tools get: the open tools, a meter of their own and the cap. */
|
|
22
|
+
export interface TurnTools {
|
|
23
|
+
opened: OpenedTools;
|
|
24
|
+
/** A new part of this answer's usage (a hand off), counted against the cap. */
|
|
25
|
+
meter(model: ModelDef, label: string, free?: boolean): UsageMeter;
|
|
26
|
+
/** Spent this answer reached what the payer has left. */
|
|
27
|
+
over(): boolean;
|
|
28
|
+
}
|
|
17
29
|
/** What one turn runs on, as the app decides it. */
|
|
18
30
|
export interface AgentTurnPlan {
|
|
19
31
|
/** The tier's model, and the key or sign-in it runs on. */
|
|
@@ -24,10 +36,23 @@ export interface AgentTurnPlan {
|
|
|
24
36
|
out?: number;
|
|
25
37
|
/** The usage part's label: `Tier · Model`. */
|
|
26
38
|
label: string;
|
|
27
|
-
/** The app's instructions and the kit's rules, joined. */
|
|
28
|
-
system: string;
|
|
39
|
+
/** The app's instructions and the kit's rules, joined; a function sees the tools opened. */
|
|
40
|
+
system: string | ((t: OpenedTools) => string);
|
|
41
|
+
/** Put before the person's newest message for the model only (bearings, the page). */
|
|
42
|
+
lead?: string;
|
|
29
43
|
/** The tool groups at the person's access. */
|
|
30
44
|
sources: SourceAt[];
|
|
45
|
+
/** Tools beside the groups' (hand off), made once those are open. */
|
|
46
|
+
more?: (t: TurnTools) => ToolSet;
|
|
47
|
+
/** Call this tool first (a message handed to a higher tier). */
|
|
48
|
+
first?: string;
|
|
49
|
+
/** Claude's advisor: a bigger Claude reads the chat and sends a plan. */
|
|
50
|
+
advisor?: {
|
|
51
|
+
model: ModelDef;
|
|
52
|
+
label: string;
|
|
53
|
+
maxUses: number;
|
|
54
|
+
maxTokens: number;
|
|
55
|
+
};
|
|
31
56
|
/** Dollars the payer has left; at or past it the model gets no more tools. */
|
|
32
57
|
left?: number;
|
|
33
58
|
/** Most model calls in one answer; 8. */
|
|
@@ -39,23 +64,29 @@ export interface AgentTurnPlan {
|
|
|
39
64
|
/** How a turn ended, with its usage, for the app to record. */
|
|
40
65
|
export interface AgentTurnEnd {
|
|
41
66
|
outcome: 'ok' | 'stopped' | 'error';
|
|
67
|
+
/** The main model's part first, then advice and hand offs; parts with no calls left out. */
|
|
42
68
|
parts: UsagePart[];
|
|
43
69
|
/** Tokens in the main model's last call: how full its context is. */
|
|
44
70
|
context?: number;
|
|
71
|
+
/** Model calls of the main tier. */
|
|
72
|
+
steps: number;
|
|
45
73
|
ms: number;
|
|
46
74
|
/** The spend cap stopped tool use in this answer. */
|
|
47
75
|
capped: boolean;
|
|
48
76
|
}
|
|
77
|
+
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.";
|
|
78
|
+
/** The messages with `lead` put before the newest message the person wrote. */
|
|
79
|
+
export declare function withLead(messages: ModelMessage[], lead: string | undefined): ModelMessage[];
|
|
49
80
|
export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
|
|
50
81
|
#private;
|
|
51
|
-
/** The turn the app runs now,
|
|
82
|
+
/** The turn the app runs now. Throw to refuse it (no key, no money); the error is shown. */
|
|
52
83
|
abstract plan(turn: TurnContext): Promise<AgentTurnPlan>;
|
|
53
84
|
/** A turn ended: record its spend and how it went. Errors here are logged, never shown. */
|
|
54
85
|
abstract settle(end: AgentTurnEnd): Promise<void>;
|
|
55
86
|
/** Never read: `beforeTurn` gives every turn its own model. */
|
|
56
87
|
getModel(): ThinkModel;
|
|
57
88
|
beforeTurn(ctx: TurnContext): Promise<TurnConfig>;
|
|
58
|
-
beforeStep(
|
|
89
|
+
beforeStep(ctx: PrepareStepContext): StepConfig | undefined;
|
|
59
90
|
onStepFinish(ctx: StepContext): void;
|
|
60
91
|
onChatResponse(result: ChatResponseResult): Promise<void>;
|
|
61
92
|
onChatError(error: unknown): unknown;
|
package/dist/agent/agent.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LumenAgent`: one chat as a Cloudflare Think agent, run on the app's tiers, keys and tool groups.
|
|
3
|
+
* Think keeps the loop, the messages, streaming, resume and compaction; the app answers `plan` for
|
|
4
|
+
* each turn (which model on whose key, the instructions, the tools at the person's access, what is
|
|
5
|
+
* left to spend) and hears `settle` when the turn ends, to record the spend.
|
|
6
|
+
*
|
|
7
|
+
* export class SageChat extends LumenAgent<Env> {
|
|
8
|
+
* plan(turn) { return sagePlan(this.env, this.name, turn); }
|
|
9
|
+
* settle(s) { return recordSpend(this.env, this.name, s); }
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* An `Assistant` gives both: `assistant.agentTurn(caller, input)` returns a plan and its settle.
|
|
13
|
+
*/
|
|
14
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
1
15
|
import { Think, } from '@cloudflare/think';
|
|
2
16
|
import { aiModel, turnSettings } from './models.js';
|
|
3
17
|
import { openAiTools } from './tools.js';
|
|
4
18
|
import { UsageMeter } from './usage.js';
|
|
5
|
-
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.';
|
|
19
|
+
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.';
|
|
20
|
+
/** The messages with `lead` put before the newest message the person wrote. */
|
|
21
|
+
export function withLead(messages, lead) {
|
|
22
|
+
if (!lead)
|
|
23
|
+
return messages;
|
|
24
|
+
let i = messages.length - 1;
|
|
25
|
+
while (i >= 0 && messages[i]?.role !== 'user')
|
|
26
|
+
i -= 1;
|
|
27
|
+
const m = messages[i];
|
|
28
|
+
if (!m || m.role !== 'user')
|
|
29
|
+
return messages;
|
|
30
|
+
const content = typeof m.content === 'string'
|
|
31
|
+
? `${lead}${m.content}`
|
|
32
|
+
: [{ type: 'text', text: lead }, ...m.content];
|
|
33
|
+
return [...messages.slice(0, i), { ...m, content }, ...messages.slice(i + 1)];
|
|
34
|
+
}
|
|
6
35
|
export class LumenAgent extends Think {
|
|
7
36
|
#turn = null;
|
|
8
37
|
/** Never read: `beforeTurn` gives every turn its own model. */
|
|
@@ -12,22 +41,52 @@ export class LumenAgent extends Think {
|
|
|
12
41
|
async beforeTurn(ctx) {
|
|
13
42
|
await this.#close();
|
|
14
43
|
const plan = await this.plan(ctx);
|
|
15
|
-
const
|
|
44
|
+
const opened = await openAiTools(plan.sources, plan.tools ?? {});
|
|
16
45
|
const free = !!plan.key.plan;
|
|
46
|
+
const main = new UsageMeter(plan.model, plan.label, free);
|
|
47
|
+
const meters = [main];
|
|
48
|
+
const advice = plan.advisor
|
|
49
|
+
? new UsageMeter(plan.advisor.model, plan.advisor.label, free)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (advice)
|
|
52
|
+
meters.push(advice);
|
|
53
|
+
const system = typeof plan.system === 'string' ? plan.system : plan.system(opened);
|
|
17
54
|
this.#turn = {
|
|
18
55
|
plan,
|
|
19
|
-
|
|
20
|
-
|
|
56
|
+
system,
|
|
57
|
+
tools: opened,
|
|
58
|
+
main,
|
|
59
|
+
...(advice ? { advice } : {}),
|
|
60
|
+
meters,
|
|
61
|
+
steps: 0,
|
|
21
62
|
capped: false,
|
|
22
63
|
t0: Date.now(),
|
|
23
64
|
};
|
|
65
|
+
const more = plan.more?.({
|
|
66
|
+
opened,
|
|
67
|
+
over: () => plan.left !== undefined && this.#spent() >= plan.left,
|
|
68
|
+
meter: (model, label, f = free) => {
|
|
69
|
+
const m = new UsageMeter(model, label, f);
|
|
70
|
+
meters.push(m);
|
|
71
|
+
return m;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const advisor = {};
|
|
75
|
+
if (plan.advisor)
|
|
76
|
+
advisor.advisor = anthropic.tools.advisor_20260301({
|
|
77
|
+
model: plan.advisor.model.id,
|
|
78
|
+
maxUses: plan.advisor.maxUses,
|
|
79
|
+
maxTokens: plan.advisor.maxTokens,
|
|
80
|
+
});
|
|
24
81
|
return {
|
|
25
82
|
model: aiModel(plan.model, plan.key, {
|
|
26
83
|
...(plan.fetch ? { fetch: plan.fetch } : {}),
|
|
27
84
|
name: this.constructor.name,
|
|
28
85
|
}),
|
|
29
|
-
instructions:
|
|
30
|
-
|
|
86
|
+
instructions: system,
|
|
87
|
+
messages: withLead(ctx.messages, plan.lead),
|
|
88
|
+
tools: { ...opened.tools, ...more, ...advisor },
|
|
89
|
+
...(plan.first ? { toolChoice: { type: 'tool', toolName: plan.first } } : {}),
|
|
31
90
|
maxSteps: plan.maxSteps ?? 8,
|
|
32
91
|
...turnSettings(plan.model, {
|
|
33
92
|
effort: plan.effort ?? 'default',
|
|
@@ -36,22 +95,38 @@ export class LumenAgent extends Think {
|
|
|
36
95
|
}),
|
|
37
96
|
};
|
|
38
97
|
}
|
|
39
|
-
beforeStep(
|
|
98
|
+
beforeStep(ctx) {
|
|
40
99
|
const t = this.#turn;
|
|
41
|
-
if (!t
|
|
42
|
-
return undefined;
|
|
43
|
-
if (t.meter.part().usd < t.plan.left)
|
|
100
|
+
if (!t)
|
|
44
101
|
return undefined;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
102
|
+
// The SDK's step result is a union that Omit narrows to shared keys; these exist on it.
|
|
103
|
+
if (t.plan.left !== undefined && this.#spent() >= t.plan.left) {
|
|
104
|
+
t.capped = true;
|
|
105
|
+
return { activeTools: [], instructions: `${t.system}\n\n${CAP_NOTE}` };
|
|
106
|
+
}
|
|
107
|
+
// A forced first call is for the first step only.
|
|
108
|
+
if (t.plan.first && ctx.stepNumber > 0)
|
|
109
|
+
return { toolChoice: 'auto' };
|
|
110
|
+
return undefined;
|
|
48
111
|
}
|
|
49
112
|
onStepFinish(ctx) {
|
|
50
113
|
const t = this.#turn;
|
|
51
114
|
if (!t)
|
|
52
115
|
return;
|
|
53
|
-
|
|
116
|
+
t.steps += 1;
|
|
117
|
+
const p = t.main.add(ctx.usage);
|
|
54
118
|
t.context = p.in + p.out;
|
|
119
|
+
// Claude's advisor bills apart from the step's own usage: its calls are listed one by one.
|
|
120
|
+
const meta = ctx
|
|
121
|
+
.providerMetadata;
|
|
122
|
+
for (const it of meta?.anthropic?.iterations ?? [])
|
|
123
|
+
if (it.type === 'advisor_message' && t.advice)
|
|
124
|
+
t.advice.addTokens({
|
|
125
|
+
input_tokens: it.inputTokens ?? 0,
|
|
126
|
+
output_tokens: it.outputTokens ?? 0,
|
|
127
|
+
cache_read_input_tokens: it.cacheReadInputTokens ?? 0,
|
|
128
|
+
cache_creation_input_tokens: it.cacheCreationInputTokens ?? 0,
|
|
129
|
+
});
|
|
55
130
|
}
|
|
56
131
|
async onChatResponse(result) {
|
|
57
132
|
await this.#end(result.status === 'completed' ? 'ok' : result.status === 'aborted' ? 'stopped' : 'error');
|
|
@@ -60,6 +135,9 @@ export class LumenAgent extends Think {
|
|
|
60
135
|
void this.#end('error');
|
|
61
136
|
return error;
|
|
62
137
|
}
|
|
138
|
+
#spent() {
|
|
139
|
+
return (this.#turn?.meters ?? []).reduce((a, m) => a + m.part().usd, 0);
|
|
140
|
+
}
|
|
63
141
|
async #end(outcome) {
|
|
64
142
|
const t = this.#turn;
|
|
65
143
|
if (!t)
|
|
@@ -69,8 +147,9 @@ export class LumenAgent extends Think {
|
|
|
69
147
|
try {
|
|
70
148
|
await this.settle({
|
|
71
149
|
outcome,
|
|
72
|
-
parts:
|
|
150
|
+
parts: t.meters.map((m) => m.part()).filter((p) => p.calls),
|
|
73
151
|
...(t.context ? { context: t.context } : {}),
|
|
152
|
+
steps: t.steps,
|
|
74
153
|
ms: Date.now() - t.t0,
|
|
75
154
|
capped: t.capped,
|
|
76
155
|
});
|
package/dist/agent/models.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantEffort } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
-
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { JSONValue, LanguageModel } 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 {
|
|
@@ -28,5 +28,5 @@ export declare function turnSettings(model: ModelDef, o: {
|
|
|
28
28
|
plan?: boolean;
|
|
29
29
|
}): {
|
|
30
30
|
maxOutputTokens?: number;
|
|
31
|
-
providerOptions?: Record<string, Record<string,
|
|
31
|
+
providerOptions?: Record<string, Record<string, JSONValue>>;
|
|
32
32
|
};
|
package/dist/agent/tools.d.ts
CHANGED
|
@@ -33,11 +33,20 @@ export interface AiToolsOptions {
|
|
|
33
33
|
/** A result handed to the model is cut past this many characters. */
|
|
34
34
|
maxChars?: number;
|
|
35
35
|
}
|
|
36
|
+
/** One tool offered this answer, as the instructions describe the set. */
|
|
37
|
+
export interface ToolInfo {
|
|
38
|
+
name: string;
|
|
39
|
+
group: string;
|
|
40
|
+
kind: 'read' | 'draft' | 'write';
|
|
41
|
+
find: boolean;
|
|
42
|
+
}
|
|
36
43
|
/** Tools open for one answer, as the AI SDK's set, and how to let them go. */
|
|
37
44
|
export interface OpenedTools {
|
|
38
45
|
tools: ToolSet;
|
|
39
46
|
/** Names of the reads, for a lazy loader's catalog or `activeTools`. */
|
|
40
47
|
reads: string[];
|
|
48
|
+
/** Each tool offered: its name, group, kind and whether it is an app's find. */
|
|
49
|
+
info: ToolInfo[];
|
|
41
50
|
failed: number;
|
|
42
51
|
close(): Promise<void>;
|
|
43
52
|
}
|
package/dist/agent/tools.js
CHANGED
|
@@ -25,6 +25,7 @@ export async function openAiTools(sources, o = {}) {
|
|
|
25
25
|
const max = o.maxChars ?? 16_000;
|
|
26
26
|
const tools = {};
|
|
27
27
|
const reads = [];
|
|
28
|
+
const info = [];
|
|
28
29
|
for (const { s, open } of opened)
|
|
29
30
|
for (const def of open.tools) {
|
|
30
31
|
const canWrite = s.access === 'draft' || s.access === 'write';
|
|
@@ -36,6 +37,7 @@ export async function openAiTools(sources, o = {}) {
|
|
|
36
37
|
const name = toolName(s.group, def.name);
|
|
37
38
|
if (kind === 'read')
|
|
38
39
|
reads.push(name);
|
|
40
|
+
info.push({ name, group: s.group, kind, find: !!def.find });
|
|
39
41
|
tools[name] = tool({
|
|
40
42
|
description: describe(def, kind),
|
|
41
43
|
inputSchema: jsonSchema(withoutMeta(def.inputSchema)),
|
|
@@ -59,6 +61,7 @@ export async function openAiTools(sources, o = {}) {
|
|
|
59
61
|
return {
|
|
60
62
|
tools,
|
|
61
63
|
reads,
|
|
64
|
+
info,
|
|
62
65
|
failed: results.filter((r) => r.status === 'rejected').length,
|
|
63
66
|
close: async () => {
|
|
64
67
|
await Promise.all(opened.map((x) => x.open.close().catch(() => undefined)));
|
package/dist/agent/usage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** An AI SDK step's usage as the kit's tokens and list-price cost. */
|
|
2
2
|
import type { UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
3
3
|
import type { LanguageModelUsage } from 'ai';
|
|
4
|
-
import { type ModelDef } from '../assistant/models.js';
|
|
4
|
+
import { type ModelDef, type TokenUsage } from '../assistant/models.js';
|
|
5
5
|
/**
|
|
6
6
|
* One call's usage in the kit's terms. Input splits into plain, cache writes and cache reads where
|
|
7
7
|
* the provider says (Claude reports all three; OpenAI reports reads, its writes cost nothing extra).
|
|
@@ -25,5 +25,12 @@ export declare class UsageMeter {
|
|
|
25
25
|
read: number;
|
|
26
26
|
usd: number;
|
|
27
27
|
};
|
|
28
|
+
/** Tokens a provider reports apart from the step's usage (Claude's advisor calls). */
|
|
29
|
+
addTokens(u: TokenUsage): {
|
|
30
|
+
in: number;
|
|
31
|
+
out: number;
|
|
32
|
+
read: number;
|
|
33
|
+
usd: number;
|
|
34
|
+
};
|
|
28
35
|
part(): UsagePart;
|
|
29
36
|
}
|
package/dist/agent/usage.js
CHANGED
|
@@ -27,7 +27,11 @@ export class UsageMeter {
|
|
|
27
27
|
this.p = { label, calls: 0, in: 0, read: 0, out: 0, usd: 0 };
|
|
28
28
|
}
|
|
29
29
|
add(u) {
|
|
30
|
-
|
|
30
|
+
return this.addTokens(tokenUsage(u));
|
|
31
|
+
}
|
|
32
|
+
/** Tokens a provider reports apart from the step's usage (Claude's advisor calls). */
|
|
33
|
+
addTokens(u) {
|
|
34
|
+
const t = priced(this.model, u);
|
|
31
35
|
this.p.calls += 1;
|
|
32
36
|
this.p.in += t.in;
|
|
33
37
|
this.p.read += t.read;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AllowanceRequest, AllowanceState, AllowanceUnit, AssistantAccess, AssistantChat, AssistantChatRow, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantStatus, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
import type { AgentTurnEnd, AgentTurnPlan } from '../agent/agent.js';
|
|
2
3
|
import { type BearingsExtra, type BearingsNow } from './bearings.js';
|
|
3
4
|
import { type ModelDef, type TaskDef, type TierDef } from './models.js';
|
|
4
5
|
import { State } from './state.js';
|
|
@@ -173,6 +174,18 @@ export declare class AssistantError extends Error {
|
|
|
173
174
|
constructor(message: string, code: string, status?: number);
|
|
174
175
|
}
|
|
175
176
|
type Emit = (event: string, data: unknown) => void;
|
|
177
|
+
/** What a Think turn calls this turn: what one turn asks besides the message itself. */
|
|
178
|
+
export interface AgentTurnInput {
|
|
179
|
+
message: string;
|
|
180
|
+
context?: AssistantMessage['context'];
|
|
181
|
+
refs?: AssistantMessage['refs'];
|
|
182
|
+
/** The tier the person picked for this message (`@`). */
|
|
183
|
+
tier?: string;
|
|
184
|
+
/** The browser's IANA zone, for the bearings. */
|
|
185
|
+
time_zone?: string;
|
|
186
|
+
/** When the chat's last message was, for "since you last asked". */
|
|
187
|
+
last_at?: number;
|
|
188
|
+
}
|
|
176
189
|
export declare class Assistant {
|
|
177
190
|
private readonly o;
|
|
178
191
|
readonly name: string;
|
|
@@ -203,6 +216,19 @@ export declare class Assistant {
|
|
|
203
216
|
private mainTier;
|
|
204
217
|
private access;
|
|
205
218
|
private groupById;
|
|
219
|
+
/**
|
|
220
|
+
* One turn as a Think agent's plan (`LumenAgent` in `@columenlabs/lumen-ai/agent`): the person's
|
|
221
|
+
* tier and its model on whoever pays, the tool groups at their access, the kit's rules, the
|
|
222
|
+
* bearings and the page, hand off to a higher tier and Claude's advisor. `settle` records the
|
|
223
|
+
* spend and the turn. Throws `AssistantError` when nobody can pay (its text says what to do).
|
|
224
|
+
* Drafts, Undo and compaction stay with `turn` for now.
|
|
225
|
+
*/
|
|
226
|
+
agentTurn(caller: Caller, input: AgentTurnInput): Promise<{
|
|
227
|
+
plan: AgentTurnPlan;
|
|
228
|
+
settle: (end: AgentTurnEnd) => Promise<void>;
|
|
229
|
+
}>;
|
|
230
|
+
/** The escalate tool of a Think turn: one task to a higher tier, its memo back. */
|
|
231
|
+
private agentEscalate;
|
|
206
232
|
status(caller: Caller): Promise<AssistantStatus>;
|
|
207
233
|
private get unit();
|
|
208
234
|
private approver;
|
|
@@ -222,6 +248,8 @@ export declare class Assistant {
|
|
|
222
248
|
purge(): Promise<number>;
|
|
223
249
|
private writeChat;
|
|
224
250
|
/** Opens the tools the person allows: each group's, then the web's. */
|
|
251
|
+
/** The tool groups (and the web) at the person's access, not opened yet. */
|
|
252
|
+
private sourcesOf;
|
|
225
253
|
private openTools;
|
|
226
254
|
private toolsFor;
|
|
227
255
|
/** `batch`: any of the reads, up to BATCH_MAX at once, as one step. Null with no reads. */
|
package/dist/assistant/engine.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import Anthropic from '@anthropic-ai/sdk';
|
|
20
20
|
import { HAND_OFF_WHYS } from '@columenlabs/lumen-ui-types/assistant';
|
|
21
|
+
import { generateText, jsonSchema, stepCountIs, tool } from 'ai';
|
|
21
22
|
import OpenAI from 'openai';
|
|
23
|
+
import { aiModel, turnSettings } from '../agent/models.js';
|
|
22
24
|
import { bearings } from './bearings.js';
|
|
23
25
|
import { CHATGPT_REDIRECT, SigninError, finishChatgpt, startChatgpt, testChatgpt, } from './chatgpt.js';
|
|
24
26
|
import { ALL_MODELS, DEFAULT_OUT, Meter, PROVIDERS, effortParams, } from './models.js';
|
|
@@ -229,6 +231,29 @@ const withoutMeta = (schema) => {
|
|
|
229
231
|
const shows = (output) => output.some((b) => b.type === 'function_call' ||
|
|
230
232
|
(b.type === 'message' && b.content.some((c) => c.type === 'output_text' && c.text)));
|
|
231
233
|
const isApiError = (err) => err instanceof Anthropic.APIError || err instanceof OpenAI.APIError;
|
|
234
|
+
/**
|
|
235
|
+
* The tools of a Think turn as the rules describe them. Think calls several tools in one step on
|
|
236
|
+
* its own, so the kit's batch tool is not offered and its line is left out (`batch`).
|
|
237
|
+
*/
|
|
238
|
+
function openedSummary(t) {
|
|
239
|
+
return {
|
|
240
|
+
size: t.info.length,
|
|
241
|
+
find: t.info.some((i) => i.find),
|
|
242
|
+
reads: t.reads.length > 0,
|
|
243
|
+
batch: true,
|
|
244
|
+
web: t.info.some((i) => i.group === 'web'),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function summarize(fns) {
|
|
248
|
+
const all = [...fns.values()];
|
|
249
|
+
return {
|
|
250
|
+
size: fns.size,
|
|
251
|
+
find: all.some((f) => f.def.find),
|
|
252
|
+
reads: all.some((f) => !f.draft && !f.write),
|
|
253
|
+
batch: fns.has(BATCH),
|
|
254
|
+
web: all.some((f) => f.group === 'web'),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
232
257
|
export class Assistant {
|
|
233
258
|
o;
|
|
234
259
|
name;
|
|
@@ -338,6 +363,211 @@ export class Assistant {
|
|
|
338
363
|
groupById(id) {
|
|
339
364
|
return this.groups.find((g) => g.id === id);
|
|
340
365
|
}
|
|
366
|
+
// ---------------------------------------------------------------- a turn on Think
|
|
367
|
+
/**
|
|
368
|
+
* One turn as a Think agent's plan (`LumenAgent` in `@columenlabs/lumen-ai/agent`): the person's
|
|
369
|
+
* tier and its model on whoever pays, the tool groups at their access, the kit's rules, the
|
|
370
|
+
* bearings and the page, hand off to a higher tier and Claude's advisor. `settle` records the
|
|
371
|
+
* spend and the turn. Throws `AssistantError` when nobody can pay (its text says what to do).
|
|
372
|
+
* Drafts, Undo and compaction stay with `turn` for now.
|
|
373
|
+
*/
|
|
374
|
+
async agentTurn(caller, input) {
|
|
375
|
+
const message = input.message.trim();
|
|
376
|
+
const picked = await this.mainTier(caller);
|
|
377
|
+
const lower = input.tier && !this.above(picked).some((t) => t.id === input.tier)
|
|
378
|
+
? this.tierById(input.tier)
|
|
379
|
+
: undefined;
|
|
380
|
+
const main = await this.session(caller, lower ?? picked);
|
|
381
|
+
const { paying, wait } = await this.state.paying(caller.id, main.model.provider);
|
|
382
|
+
if (!paying) {
|
|
383
|
+
const code = wait ?? 'not_connected';
|
|
384
|
+
throw new AssistantError(waitText(code, main.model.provider, !!this.o.allowance), code, 402);
|
|
385
|
+
}
|
|
386
|
+
const keyOf = (provider) => {
|
|
387
|
+
const apiKey = paying.keys[provider];
|
|
388
|
+
return apiKey ? { apiKey, ...(paying.plans[provider] ? { plan: true } : {}) } : null;
|
|
389
|
+
};
|
|
390
|
+
const key = keyOf(main.model.provider) ?? { apiKey: paying.apiKey };
|
|
391
|
+
const p = await this.state.person(caller.id);
|
|
392
|
+
const policy = p.hand_off ?? 'next';
|
|
393
|
+
const advisor = await this.advisorFor(caller, p.advice, main);
|
|
394
|
+
const handTo = input.tier ? this.above(main.tier).find((t) => t.id === input.tier) : undefined;
|
|
395
|
+
const appSystem = typeof this.o.system === 'string' ? this.o.system : await this.o.system(caller);
|
|
396
|
+
const asked = await this.expand(caller, message);
|
|
397
|
+
const told = await bearings({
|
|
398
|
+
timeZone: input.time_zone,
|
|
399
|
+
fallbackZone: this.o.timeZone,
|
|
400
|
+
lastAt: input.last_at,
|
|
401
|
+
app: this.o.app,
|
|
402
|
+
...(this.o.bearings ? { extras: (b) => this.o.bearings?.(b, caller) } : {}),
|
|
403
|
+
});
|
|
404
|
+
const page = told +
|
|
405
|
+
(input.refs?.length
|
|
406
|
+
? `The person points at these parts: they are what "this" means, and when the person asks for a change they are the only parts to change. Read anything else you need (the rest of the page, other records, your tools) for context, but leave it as it is unless they ask.\n${JSON.stringify(input.refs).slice(0, 16000)}\n\n`
|
|
407
|
+
: '') +
|
|
408
|
+
(input.context
|
|
409
|
+
? `Page: ${input.context.label}${input.context.note === undefined ? '' : `\n${JSON.stringify(input.context.note).slice(0, 4000)}`}\n\n`
|
|
410
|
+
: '');
|
|
411
|
+
const command = asked === message ? '' : `The person typed ${message.split(/\s/)[0]}: ${asked}\n\n`;
|
|
412
|
+
const reach = this.above(main.tier);
|
|
413
|
+
const counts = { handoffs: 0, end: main.tier };
|
|
414
|
+
const rulesFor = (s, t) => `${appSystem}\n\n${this.rules(s, policy, s ? advisor : null, openedSummary(t), t.failed)}`;
|
|
415
|
+
const plan = {
|
|
416
|
+
model: main.model,
|
|
417
|
+
key,
|
|
418
|
+
effort: main.effort,
|
|
419
|
+
out: main.tier.out ?? DEFAULT_OUT,
|
|
420
|
+
label: main.meter.label,
|
|
421
|
+
system: (t) => rulesFor(main, t),
|
|
422
|
+
lead: `${page}${handTo && reach.length
|
|
423
|
+
? `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`
|
|
424
|
+
: ''}${command}`,
|
|
425
|
+
sources: await this.sourcesOf(caller, p),
|
|
426
|
+
left: paying.left,
|
|
427
|
+
maxSteps: MAX_ROUNDS,
|
|
428
|
+
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
|
429
|
+
...(handTo && reach.length ? { first: 'escalate' } : {}),
|
|
430
|
+
...(advisor
|
|
431
|
+
? {
|
|
432
|
+
advisor: {
|
|
433
|
+
model: advisor.session.model,
|
|
434
|
+
label: advisor.session.meter.label,
|
|
435
|
+
maxUses: ADVICE_MAX,
|
|
436
|
+
maxTokens: ADVICE_TOKENS,
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
440
|
+
tools: {
|
|
441
|
+
called: (c) => void Promise.resolve(this.o.onStep?.({
|
|
442
|
+
kind: 'tool',
|
|
443
|
+
tool: toolName(c.group, c.tool),
|
|
444
|
+
ok: c.ok,
|
|
445
|
+
ms: c.ms,
|
|
446
|
+
tier: main.tier.id,
|
|
447
|
+
handoff: false,
|
|
448
|
+
})).catch(() => undefined),
|
|
449
|
+
},
|
|
450
|
+
more: (tt) => reach.length
|
|
451
|
+
? {
|
|
452
|
+
escalate: this.agentEscalate(tt, {
|
|
453
|
+
caller,
|
|
454
|
+
message: `${page}${asked}`,
|
|
455
|
+
asked: this.askedFor(message, main.tier),
|
|
456
|
+
handTo,
|
|
457
|
+
policy,
|
|
458
|
+
main,
|
|
459
|
+
keyOf,
|
|
460
|
+
counts,
|
|
461
|
+
system: (t) => rulesFor(null, t),
|
|
462
|
+
}),
|
|
463
|
+
}
|
|
464
|
+
: {},
|
|
465
|
+
};
|
|
466
|
+
const settle = async (end) => {
|
|
467
|
+
await this.state.addSpend(caller.id, paying.payer, end.parts).catch(() => undefined);
|
|
468
|
+
const total = end.parts.reduce((a, m) => ({
|
|
469
|
+
calls: a.calls + m.calls,
|
|
470
|
+
in: a.in + m.in,
|
|
471
|
+
out: a.out + m.out,
|
|
472
|
+
usd: a.usd + m.usd,
|
|
473
|
+
}), { calls: 0, in: 0, out: 0, usd: 0 });
|
|
474
|
+
await Promise.resolve(this.o.onStep?.({
|
|
475
|
+
kind: 'turn',
|
|
476
|
+
outcome: end.outcome,
|
|
477
|
+
ms: end.ms,
|
|
478
|
+
tier: main.tier.id,
|
|
479
|
+
end_tier: counts.end.id,
|
|
480
|
+
handoffs: counts.handoffs,
|
|
481
|
+
advice: advisor
|
|
482
|
+
? (end.parts.find((x) => x.label === advisor.session.meter.label)?.calls ?? 0)
|
|
483
|
+
: 0,
|
|
484
|
+
steps: end.steps,
|
|
485
|
+
...total,
|
|
486
|
+
})).catch(() => undefined);
|
|
487
|
+
};
|
|
488
|
+
return { plan, settle };
|
|
489
|
+
}
|
|
490
|
+
/** The escalate tool of a Think turn: one task to a higher tier, its memo back. */
|
|
491
|
+
agentEscalate(tt, c) {
|
|
492
|
+
const reach = this.above(c.main.tier);
|
|
493
|
+
return tool({
|
|
494
|
+
description: 'Hands one task to a higher tier with the same tools. It works on its own and returns a memo of at most 200 words.',
|
|
495
|
+
inputSchema: jsonSchema({
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
tier: { type: 'string', enum: reach.map((t) => t.id) },
|
|
499
|
+
why: { type: 'string', enum: [...HAND_OFF_WHYS] },
|
|
500
|
+
task: {
|
|
501
|
+
type: 'string',
|
|
502
|
+
description: 'The task, with every fact from the chat it needs.',
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
required: ['tier', 'why', 'task'],
|
|
506
|
+
}),
|
|
507
|
+
execute: async (input, { abortSignal }) => {
|
|
508
|
+
const tier = reach.find((t) => t.id === input.tier);
|
|
509
|
+
const why = HAND_OFF_WHYS.find((w) => w === input.why);
|
|
510
|
+
const task = typeof input.task === 'string' ? input.task.trim() : '';
|
|
511
|
+
if (!tier || !why || !task)
|
|
512
|
+
return {
|
|
513
|
+
error: `Give {tier: ${reach.map((t) => `"${t.id}"`).join(' | ')}, why: ${HAND_OFF_WHYS.map((w) => `"${w}"`).join(' | ')}, task}.`,
|
|
514
|
+
};
|
|
515
|
+
const asked = new Set(c.asked);
|
|
516
|
+
if (c.handTo)
|
|
517
|
+
asked.add(c.handTo.id);
|
|
518
|
+
const own = c.policy === 'any' ? reach : c.policy === 'next' ? reach.slice(0, 1) : [];
|
|
519
|
+
const may = new Set([...own.map((t) => t.id), ...asked]);
|
|
520
|
+
if (!may.has(tier.id))
|
|
521
|
+
return {
|
|
522
|
+
error: may.size
|
|
523
|
+
? `Not ${tier.id} now: you may call ${[...may].join(' or ')}.`
|
|
524
|
+
: 'The person has not asked for a higher tier: answer yourself.',
|
|
525
|
+
};
|
|
526
|
+
if (tt.over())
|
|
527
|
+
return { error: 'No money left for a higher tier this answer: answer yourself.' };
|
|
528
|
+
const sub = await this.session(c.caller, tier);
|
|
529
|
+
const key = c.keyOf(sub.model.provider);
|
|
530
|
+
const provider = PROVIDERS[sub.model.provider].label;
|
|
531
|
+
if (!key)
|
|
532
|
+
return {
|
|
533
|
+
error: `${tier.label} runs on ${provider} and no ${provider} key is set for whoever pays this answer: answer yourself.`,
|
|
534
|
+
};
|
|
535
|
+
const meter = tt.meter(sub.model, sub.meter.label, !!key.plan);
|
|
536
|
+
c.counts.handoffs += 1;
|
|
537
|
+
if (this.tiers.indexOf(tier) > this.tiers.indexOf(c.counts.end))
|
|
538
|
+
c.counts.end = tier;
|
|
539
|
+
const shown = asked.has(tier.id) ? 'asked' : why;
|
|
540
|
+
try {
|
|
541
|
+
const r = await generateText({
|
|
542
|
+
model: aiModel(sub.model, key, {
|
|
543
|
+
...(this.o.fetch ? { fetch: this.o.fetch } : {}),
|
|
544
|
+
name: this.name,
|
|
545
|
+
}),
|
|
546
|
+
instructions: c.system(tt.opened),
|
|
547
|
+
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.`,
|
|
548
|
+
tools: tt.opened.tools,
|
|
549
|
+
stopWhen: stepCountIs(MAX_ROUNDS),
|
|
550
|
+
prepareStep: () => (tt.over() ? { activeTools: [] } : undefined),
|
|
551
|
+
onStepFinish: (s) => {
|
|
552
|
+
meter.add(s.usage);
|
|
553
|
+
},
|
|
554
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
555
|
+
...turnSettings(sub.model, {
|
|
556
|
+
effort: sub.effort,
|
|
557
|
+
out: tier.out ?? DEFAULT_OUT,
|
|
558
|
+
plan: !!key.plan,
|
|
559
|
+
}),
|
|
560
|
+
});
|
|
561
|
+
return { memo: `Memo from ${sub.meter.label}:\n${r.text.slice(0, MEMO_CHARS)}` };
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
if (abortSignal?.aborted)
|
|
565
|
+
throw err;
|
|
566
|
+
return { error: `${sub.meter.label} failed: ${shownError(err).message}` };
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
}
|
|
341
571
|
// ---------------------------------------------------------------- status and settings
|
|
342
572
|
async status(caller) {
|
|
343
573
|
const main = await this.mainTier(caller);
|
|
@@ -583,8 +813,8 @@ export class Assistant {
|
|
|
583
813
|
}
|
|
584
814
|
// ---------------------------------------------------------------- tools
|
|
585
815
|
/** Opens the tools the person allows: each group's, then the web's. */
|
|
586
|
-
|
|
587
|
-
|
|
816
|
+
/** The tool groups (and the web) at the person's access, not opened yet. */
|
|
817
|
+
async sourcesOf(caller, p) {
|
|
588
818
|
const sources = [];
|
|
589
819
|
for (const g of this.groups) {
|
|
590
820
|
const access = this.access(p, g);
|
|
@@ -592,7 +822,7 @@ export class Assistant {
|
|
|
592
822
|
continue;
|
|
593
823
|
const s = g.tools(caller);
|
|
594
824
|
for (const source of Array.isArray(s) ? s : [s])
|
|
595
|
-
sources.push({ group: g.id, access, source });
|
|
825
|
+
sources.push({ group: g.id, access, source, ...(g.writes ? { writes: true } : {}) });
|
|
596
826
|
}
|
|
597
827
|
if (p.web ?? true) {
|
|
598
828
|
if (this.o.web?.fetch !== false)
|
|
@@ -610,6 +840,11 @@ export class Assistant {
|
|
|
610
840
|
source: scrapeqlTools(sq.url, token, sq.fetch ?? this.o.fetch),
|
|
611
841
|
});
|
|
612
842
|
}
|
|
843
|
+
return sources;
|
|
844
|
+
}
|
|
845
|
+
async openTools(caller, p) {
|
|
846
|
+
const opened = [];
|
|
847
|
+
const sources = await this.sourcesOf(caller, p);
|
|
613
848
|
const results = await Promise.allSettled(sources.map((s) => s.source()));
|
|
614
849
|
results.forEach((r, i) => {
|
|
615
850
|
const s = sources[i];
|
|
@@ -796,7 +1031,7 @@ export class Assistant {
|
|
|
796
1031
|
let outcome = 'ok';
|
|
797
1032
|
try {
|
|
798
1033
|
const appSystem = typeof this.o.system === 'string' ? this.o.system : await this.o.system(caller);
|
|
799
|
-
const kitRules = this.rules(main, policy, advisor, tools.fns, tools.failed);
|
|
1034
|
+
const kitRules = this.rules(main, policy, advisor, summarize(tools.fns), tools.failed);
|
|
800
1035
|
const system = [
|
|
801
1036
|
{ type: 'text', text: appSystem },
|
|
802
1037
|
{ type: 'text', text: kitRules, cache_control: { type: 'ephemeral' } },
|
|
@@ -805,7 +1040,7 @@ export class Assistant {
|
|
|
805
1040
|
{ type: 'text', text: appSystem },
|
|
806
1041
|
{
|
|
807
1042
|
type: 'text',
|
|
808
|
-
text: this.rules(null, policy, null, tools.fns, tools.failed),
|
|
1043
|
+
text: this.rules(null, policy, null, summarize(tools.fns), tools.failed),
|
|
809
1044
|
cache_control: { type: 'ephemeral' },
|
|
810
1045
|
},
|
|
811
1046
|
];
|
|
@@ -960,12 +1195,12 @@ export class Assistant {
|
|
|
960
1195
|
const lines = [
|
|
961
1196
|
`You are ${this.name}. Each message starts with <bearings>: the time now in the person's zone and where they are. Take today, now and how long ago from it; never guess the date. It is for you only: never quote it or start an answer with it.`,
|
|
962
1197
|
'Answer in short plain sentences. Use the tools for facts; never guess a number a tool can give you. Call several tools at once when they do not depend on each other.',
|
|
963
|
-
...(
|
|
1198
|
+
...(fns.find
|
|
964
1199
|
? [
|
|
965
1200
|
'Questions about an app\'s records go through its find tool (the tool named <app>_find): put every condition of the question in one call (filters, words to include or leave out, which fields to look in), ask only for the fields the answer needs, then judge the rest yourself from what it returns. Never list or open records one by one to filter them. A loose ask (a mood, an occasion, "something light", "good for a party") goes in its like field when the tool has one; then pick from what it returns yourself. How many match is the result\'s count: say that number (a like search has no count, only the nearest rows: never give a number of matches for it), and name each row in same_name, which are different records that share a name. Rows a find already returned you use again only when they answer the whole new question; a new condition (another ingredient, a kind of dish, how quick) is a new find call.',
|
|
966
1201
|
]
|
|
967
1202
|
: []),
|
|
968
|
-
...(
|
|
1203
|
+
...(fns.reads && !fns.batch
|
|
969
1204
|
? [
|
|
970
1205
|
`To read several records, or one tool with several inputs, put the reads in one ${BATCH} call: one step for all of them, never one call at a time. Results you already have from earlier in the chat you use, without reading them again.`,
|
|
971
1206
|
]
|
|
@@ -979,7 +1214,7 @@ export class Assistant {
|
|
|
979
1214
|
lines.push('No tools are on now: the person can turn them on in Settings ▸ Tools.');
|
|
980
1215
|
if (failed)
|
|
981
1216
|
lines.push(`${failed} tool source${failed === 1 ? '' : 's'} could not be reached this turn.`);
|
|
982
|
-
if (
|
|
1217
|
+
if (fns.web)
|
|
983
1218
|
lines.push('Web: use web__fetch first for APIs and plain pages; the browser tools only for pages that need JavaScript or after a fetch fails.');
|
|
984
1219
|
if (main) {
|
|
985
1220
|
const reach = this.above(main.tier);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.178",
|
|
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",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"@ai-sdk/openai": "4.0.108",
|
|
49
49
|
"@anthropic-ai/sdk": "0.133.0",
|
|
50
50
|
"@cloudflare/think": "0.23.0",
|
|
51
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
51
|
+
"@columenlabs/lumen-ops": "0.1.178",
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.178",
|
|
53
53
|
"agents": "0.32.0",
|
|
54
54
|
"ai": "7.0.162",
|
|
55
55
|
"just-bash": "3.6.0",
|