@columenlabs/lumen-ai 0.1.175 → 0.1.177
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 +15 -0
- package/dist/agent/agent.d.ts +62 -0
- package/dist/agent/agent.js +87 -0
- package/dist/agent/index.d.ts +9 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/models.d.ts +32 -0
- package/dist/agent/models.js +51 -0
- package/dist/agent/tools.d.ts +45 -0
- package/dist/agent/tools.js +67 -0
- package/dist/agent/usage.d.ts +29 -0
- package/dist/agent/usage.js +42 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -210,3 +210,18 @@ ctx.waitUntil(meaningStep({ def: notesFind, sql: d1Sql(env.DB), embed, index: en
|
|
|
210
210
|
- `meaningStep` embeds only the rows whose `changed` column moved past its bookmark (`mark`: get and put, kept by the app), at most 50 a step in one embedding call and one upsert, and takes rows the find hides (its `where`) out of the index. It resumes where it stopped and runs safely twice at once; `done` is false while rows are left.
|
|
211
211
|
- A person's own rows get a `namespace` each (`vectorMeaning` and `meaningStep` take one), so one index serves one app.
|
|
212
212
|
- When the index cannot answer in 3 s, `like` is left out, the rest of the find runs and the result says so.
|
|
213
|
+
|
|
214
|
+
## An agent on Cloudflare Think
|
|
215
|
+
|
|
216
|
+
`@columenlabs/lumen-ai/agent` runs one chat as a Cloudflare Think agent (`@cloudflare/think`, experimental) on the app's tiers, keys and tool groups. Think keeps the loop, the messages, streaming, resume and compaction; the kit adds the model on the payer's key, the tools at the person's access, the spend cap and the usage. It will replace `./assistant`'s own loop.
|
|
217
|
+
|
|
218
|
+
| Export | What it does |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `LumenAgent` | An abstract Think agent. The app answers `plan(turn)` (model, key, effort, instructions, tool sources, money left) and hears `settle(end)` (outcome, usage parts, context size, time) when the turn ends |
|
|
221
|
+
| `aiModel(model, key, opts?)` | The AI SDK model for a tier on an API key, or on a ChatGPT plan sign-in (`key.plan`, through `planFetch`) |
|
|
222
|
+
| `planFetch(fetch?, name?)` | Rewrites Responses calls to a plan sign-in's rules: tools in one namespace, no output cap, a forced call as the only tool |
|
|
223
|
+
| `turnSettings(model, opts)` | Output cap and effort as the provider takes them |
|
|
224
|
+
| `openAiTools(sources, opts)` | The app's tool groups as AI SDK tools: reads always, drafts and writes by access, results cut at 16k characters |
|
|
225
|
+
| `UsageMeter`, `tokenUsage(u)` | Prices each step's tokens (cache reads and writes, reasoning) into one usage part |
|
|
226
|
+
|
|
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.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ChatResponseResult, type PrepareStepContext, type StepConfig, type StepContext, Think, type ThinkModel, type TurnConfig, type TurnContext } from '@cloudflare/think';
|
|
2
|
+
/**
|
|
3
|
+
* `LumenAgent`: one chat as a Cloudflare Think agent, run on the app's tiers, keys and tool groups.
|
|
4
|
+
* Think keeps the loop, the messages, streaming, resume and compaction; the app answers `plan` for
|
|
5
|
+
* each turn (which model on whose key, the instructions, the tools at the person's access, what is
|
|
6
|
+
* left to spend) and hears `settle` when the turn ends, to record the spend.
|
|
7
|
+
*
|
|
8
|
+
* export class SageChat extends LumenAgent<Env> {
|
|
9
|
+
* plan(turn) { return sagePlan(this.env, this.name, turn); }
|
|
10
|
+
* settle(s) { return recordSpend(this.env, this.name, s); }
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
import type { AssistantEffort, UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
14
|
+
import type { ModelDef } from '../assistant/models.js';
|
|
15
|
+
import { type ModelKey } from './models.js';
|
|
16
|
+
import { type AiToolsOptions, type SourceAt } from './tools.js';
|
|
17
|
+
/** What one turn runs on, as the app decides it. */
|
|
18
|
+
export interface AgentTurnPlan {
|
|
19
|
+
/** The tier's model, and the key or sign-in it runs on. */
|
|
20
|
+
model: ModelDef;
|
|
21
|
+
key: ModelKey;
|
|
22
|
+
effort?: AssistantEffort;
|
|
23
|
+
/** Output tokens per call; left out on a plan sign-in, which takes no cap. */
|
|
24
|
+
out?: number;
|
|
25
|
+
/** The usage part's label: `Tier · Model`. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** The app's instructions and the kit's rules, joined. */
|
|
28
|
+
system: string;
|
|
29
|
+
/** The tool groups at the person's access. */
|
|
30
|
+
sources: SourceAt[];
|
|
31
|
+
/** Dollars the payer has left; at or past it the model gets no more tools. */
|
|
32
|
+
left?: number;
|
|
33
|
+
/** Most model calls in one answer; 8. */
|
|
34
|
+
maxSteps?: number;
|
|
35
|
+
/** Fetch for provider calls (a metered one). */
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
tools?: Omit<AiToolsOptions, 'maxChars'>;
|
|
38
|
+
}
|
|
39
|
+
/** How a turn ended, with its usage, for the app to record. */
|
|
40
|
+
export interface AgentTurnEnd {
|
|
41
|
+
outcome: 'ok' | 'stopped' | 'error';
|
|
42
|
+
parts: UsagePart[];
|
|
43
|
+
/** Tokens in the main model's last call: how full its context is. */
|
|
44
|
+
context?: number;
|
|
45
|
+
ms: number;
|
|
46
|
+
/** The spend cap stopped tool use in this answer. */
|
|
47
|
+
capped: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare abstract class LumenAgent<Env = unknown> extends Think<Env> {
|
|
50
|
+
#private;
|
|
51
|
+
/** The turn the app runs now, or null between turns. */
|
|
52
|
+
abstract plan(turn: TurnContext): Promise<AgentTurnPlan>;
|
|
53
|
+
/** A turn ended: record its spend and how it went. Errors here are logged, never shown. */
|
|
54
|
+
abstract settle(end: AgentTurnEnd): Promise<void>;
|
|
55
|
+
/** Never read: `beforeTurn` gives every turn its own model. */
|
|
56
|
+
getModel(): ThinkModel;
|
|
57
|
+
beforeTurn(ctx: TurnContext): Promise<TurnConfig>;
|
|
58
|
+
beforeStep(_ctx: PrepareStepContext): StepConfig | undefined;
|
|
59
|
+
onStepFinish(ctx: StepContext): void;
|
|
60
|
+
onChatResponse(result: ChatResponseResult): Promise<void>;
|
|
61
|
+
onChatError(error: unknown): unknown;
|
|
62
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Think, } from '@cloudflare/think';
|
|
2
|
+
import { aiModel, turnSettings } from './models.js';
|
|
3
|
+
import { openAiTools } from './tools.js';
|
|
4
|
+
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.';
|
|
6
|
+
export class LumenAgent extends Think {
|
|
7
|
+
#turn = null;
|
|
8
|
+
/** Never read: `beforeTurn` gives every turn its own model. */
|
|
9
|
+
getModel() {
|
|
10
|
+
return '@cf/none';
|
|
11
|
+
}
|
|
12
|
+
async beforeTurn(ctx) {
|
|
13
|
+
await this.#close();
|
|
14
|
+
const plan = await this.plan(ctx);
|
|
15
|
+
const tools = await openAiTools(plan.sources, plan.tools ?? {});
|
|
16
|
+
const free = !!plan.key.plan;
|
|
17
|
+
this.#turn = {
|
|
18
|
+
plan,
|
|
19
|
+
tools,
|
|
20
|
+
meter: new UsageMeter(plan.model, plan.label, free),
|
|
21
|
+
capped: false,
|
|
22
|
+
t0: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
model: aiModel(plan.model, plan.key, {
|
|
26
|
+
...(plan.fetch ? { fetch: plan.fetch } : {}),
|
|
27
|
+
name: this.constructor.name,
|
|
28
|
+
}),
|
|
29
|
+
instructions: plan.system,
|
|
30
|
+
tools: tools.tools,
|
|
31
|
+
maxSteps: plan.maxSteps ?? 8,
|
|
32
|
+
...turnSettings(plan.model, {
|
|
33
|
+
effort: plan.effort ?? 'default',
|
|
34
|
+
...(plan.out ? { out: plan.out } : {}),
|
|
35
|
+
plan: free,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
beforeStep(_ctx) {
|
|
40
|
+
const t = this.#turn;
|
|
41
|
+
if (!t || t.plan.left === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (t.meter.part().usd < t.plan.left)
|
|
44
|
+
return undefined;
|
|
45
|
+
t.capped = true;
|
|
46
|
+
// The SDK's step result is a union that Omit narrows to shared keys; these two exist on it.
|
|
47
|
+
return { activeTools: [], instructions: `${t.plan.system}\n\n${CAP_NOTE}` };
|
|
48
|
+
}
|
|
49
|
+
onStepFinish(ctx) {
|
|
50
|
+
const t = this.#turn;
|
|
51
|
+
if (!t)
|
|
52
|
+
return;
|
|
53
|
+
const p = t.meter.add(ctx.usage);
|
|
54
|
+
t.context = p.in + p.out;
|
|
55
|
+
}
|
|
56
|
+
async onChatResponse(result) {
|
|
57
|
+
await this.#end(result.status === 'completed' ? 'ok' : result.status === 'aborted' ? 'stopped' : 'error');
|
|
58
|
+
}
|
|
59
|
+
onChatError(error) {
|
|
60
|
+
void this.#end('error');
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
async #end(outcome) {
|
|
64
|
+
const t = this.#turn;
|
|
65
|
+
if (!t)
|
|
66
|
+
return;
|
|
67
|
+
this.#turn = null;
|
|
68
|
+
await t.tools.close();
|
|
69
|
+
try {
|
|
70
|
+
await this.settle({
|
|
71
|
+
outcome,
|
|
72
|
+
parts: [t.meter.part()],
|
|
73
|
+
...(t.context ? { context: t.context } : {}),
|
|
74
|
+
ms: Date.now() - t.t0,
|
|
75
|
+
capped: t.capped,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(JSON.stringify({ event: 'agent_settle_error', error: String(err).slice(0, 500) }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async #close() {
|
|
83
|
+
const t = this.#turn;
|
|
84
|
+
this.#turn = null;
|
|
85
|
+
await t?.tools.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assistant on Cloudflare Think: the kit's models, keys, tool groups and usage as AI SDK parts,
|
|
3
|
+
* and `LumenAgent`, a Think agent that runs one chat on them. The app keeps its tiers, tools and
|
|
4
|
+
* payer rules; Think keeps the loop, the messages, streaming, resume and compaction.
|
|
5
|
+
*/
|
|
6
|
+
export * from './agent.js';
|
|
7
|
+
export * from './models.js';
|
|
8
|
+
export * from './tools.js';
|
|
9
|
+
export * from './usage.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assistant on Cloudflare Think: the kit's models, keys, tool groups and usage as AI SDK parts,
|
|
3
|
+
* and `LumenAgent`, a Think agent that runs one chat on them. The app keeps its tiers, tools and
|
|
4
|
+
* payer rules; Think keeps the loop, the messages, streaming, resume and compaction.
|
|
5
|
+
*/
|
|
6
|
+
export * from './agent.js';
|
|
7
|
+
export * from './models.js';
|
|
8
|
+
export * from './tools.js';
|
|
9
|
+
export * from './usage.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AssistantEffort } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
import type { LanguageModel } from 'ai';
|
|
3
|
+
import type { ModelDef } from '../assistant/models.js';
|
|
4
|
+
/** The key a model runs on. `plan`: an OpenAI access token from a ChatGPT plan sign-in. */
|
|
5
|
+
export interface ModelKey {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
plan?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface AiModelOptions {
|
|
10
|
+
/** The fetch provider calls go through (a Worker's own, a metered one). */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** What a plan sign-in's tool namespace is described as (the assistant's name). */
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
|
|
17
|
+
* forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
|
|
18
|
+
* limitations). This fetch rewrites each Responses request to those rules, so the AI SDK's OpenAI
|
|
19
|
+
* provider runs on a plan unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function planFetch(base?: typeof fetch, name?: string): typeof fetch;
|
|
22
|
+
/** The AI SDK model a tier runs: its provider's client on the payer's key. */
|
|
23
|
+
export declare function aiModel(model: ModelDef, key: ModelKey, o?: AiModelOptions): LanguageModel;
|
|
24
|
+
/** A tier's effort and output cap as a turn's settings; nothing at the model's own default. */
|
|
25
|
+
export declare function turnSettings(model: ModelDef, o: {
|
|
26
|
+
effort: AssistantEffort;
|
|
27
|
+
out?: number;
|
|
28
|
+
plan?: boolean;
|
|
29
|
+
}): {
|
|
30
|
+
maxOutputTokens?: number;
|
|
31
|
+
providerOptions?: Record<string, Record<string, unknown>>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit's models as AI SDK language models, so a Think agent (or any `streamText` call) runs on
|
|
3
|
+
* the same tiers, keys and sign-ins as the assistant: Claude on an API key, OpenAI on an API key or
|
|
4
|
+
* a person's ChatGPT plan sign-in.
|
|
5
|
+
*/
|
|
6
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
|
+
/**
|
|
9
|
+
* A ChatGPT plan sign-in takes function tools only inside one namespace and no output cap, and a
|
|
10
|
+
* forced call is the one tool offered with any call required (developers.openai.com/siwc, Preview
|
|
11
|
+
* limitations). This fetch rewrites each Responses request to those rules, so the AI SDK's OpenAI
|
|
12
|
+
* provider runs on a plan unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export function planFetch(base = fetch, name = 'Assistant') {
|
|
15
|
+
return async (input, init) => {
|
|
16
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
17
|
+
if (!url.endsWith('/responses') || typeof init?.body !== 'string')
|
|
18
|
+
return base(input, init);
|
|
19
|
+
const { max_output_tokens: _cap, ...body } = JSON.parse(init.body);
|
|
20
|
+
let tools = body.tools ?? [];
|
|
21
|
+
const choice = body.tool_choice;
|
|
22
|
+
if (choice && typeof choice === 'object' && choice.type === 'function' && choice.name) {
|
|
23
|
+
tools = tools.filter((t) => t.name === choice.name);
|
|
24
|
+
body.tool_choice = 'required';
|
|
25
|
+
}
|
|
26
|
+
const fns = tools.filter((t) => t.type === 'function').map(({ strict: _, ...t }) => t);
|
|
27
|
+
const rest = tools.filter((t) => t.type !== 'function');
|
|
28
|
+
if (fns.length)
|
|
29
|
+
body.tools = [
|
|
30
|
+
...rest,
|
|
31
|
+
{ type: 'namespace', name: 'app', description: `${name}'s tools`, tools: fns },
|
|
32
|
+
];
|
|
33
|
+
return base(input, { ...init, body: JSON.stringify(body) });
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The AI SDK model a tier runs: its provider's client on the payer's key. */
|
|
37
|
+
export function aiModel(model, key, o = {}) {
|
|
38
|
+
if (model.provider === 'anthropic')
|
|
39
|
+
return createAnthropic({ apiKey: key.apiKey, ...(o.fetch ? { fetch: o.fetch } : {}) })(model.id);
|
|
40
|
+
const f = key.plan ? planFetch(o.fetch, o.name) : o.fetch;
|
|
41
|
+
return createOpenAI({ apiKey: key.apiKey, ...(f ? { fetch: f } : {}) }).responses(model.id);
|
|
42
|
+
}
|
|
43
|
+
/** A tier's effort and output cap as a turn's settings; nothing at the model's own default. */
|
|
44
|
+
export function turnSettings(model, o) {
|
|
45
|
+
const out = o.plan || !o.out ? {} : { maxOutputTokens: o.out };
|
|
46
|
+
if (o.effort === 'default')
|
|
47
|
+
return out;
|
|
48
|
+
return model.provider === 'anthropic'
|
|
49
|
+
? { ...out, providerOptions: { anthropic: { effort: o.effort } } }
|
|
50
|
+
: { ...out, providerOptions: { openai: { reasoningEffort: o.effort } } };
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit's tool sources (MCP servers, plain functions) as an AI SDK tool set, with the person's
|
|
3
|
+
* access applied: Off leaves a group out, Read keeps its reads, Read + draft adds its writes as
|
|
4
|
+
* drafts (a card the person saves; nothing changes), Read + write runs writes at once.
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantAccess } from '@columenlabs/lumen-ui-types/assistant';
|
|
7
|
+
import { type ToolSet } from 'ai';
|
|
8
|
+
import { type ToolSource } from '../assistant/tools.js';
|
|
9
|
+
/** A group's tools to open for one answer, at the person's access. */
|
|
10
|
+
export interface SourceAt {
|
|
11
|
+
group: string;
|
|
12
|
+
access: AssistantAccess;
|
|
13
|
+
source: ToolSource;
|
|
14
|
+
/** The group runs writes at once on Read + write; else a write drafts. */
|
|
15
|
+
writes?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** A write the model asked for while its group drafts: the card's content. */
|
|
18
|
+
export interface DraftCall {
|
|
19
|
+
group: string;
|
|
20
|
+
tool: string;
|
|
21
|
+
args: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface AiToolsOptions {
|
|
24
|
+
/** A draft's card was made: return what the model is told (its id, "a card is on screen"). */
|
|
25
|
+
draft?: (d: DraftCall) => Promise<unknown>;
|
|
26
|
+
/** Every call's outcome, for the app's meter: shapes only. */
|
|
27
|
+
called?: (c: {
|
|
28
|
+
group: string;
|
|
29
|
+
tool: string;
|
|
30
|
+
ok: boolean;
|
|
31
|
+
ms: number;
|
|
32
|
+
}) => void;
|
|
33
|
+
/** A result handed to the model is cut past this many characters. */
|
|
34
|
+
maxChars?: number;
|
|
35
|
+
}
|
|
36
|
+
/** Tools open for one answer, as the AI SDK's set, and how to let them go. */
|
|
37
|
+
export interface OpenedTools {
|
|
38
|
+
tools: ToolSet;
|
|
39
|
+
/** Names of the reads, for a lazy loader's catalog or `activeTools`. */
|
|
40
|
+
reads: string[];
|
|
41
|
+
failed: number;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** Opens every source at once; one that fails is counted and left out. */
|
|
45
|
+
export declare function openAiTools(sources: SourceAt[], o?: AiToolsOptions): Promise<OpenedTools>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsonSchema, tool } from 'ai';
|
|
2
|
+
import { toolName } from '../assistant/tools.js';
|
|
3
|
+
const withoutMeta = (schema) => {
|
|
4
|
+
const { $schema: _, ...rest } = schema;
|
|
5
|
+
return { type: 'object', ...rest };
|
|
6
|
+
};
|
|
7
|
+
function describe(def, kind) {
|
|
8
|
+
const lead = kind === 'draft'
|
|
9
|
+
? "DRAFT ONLY: this call changes nothing. It puts a card on the person's screen that they Save or Dismiss. "
|
|
10
|
+
: kind === 'write'
|
|
11
|
+
? 'WRITES AT ONCE: this call makes the change now; the person can undo it. '
|
|
12
|
+
: '';
|
|
13
|
+
return `${lead}${def.title ? `${def.title}. ` : ''}${def.description ?? ''}`.slice(0, 4000);
|
|
14
|
+
}
|
|
15
|
+
/** Opens every source at once; one that fails is counted and left out. */
|
|
16
|
+
export async function openAiTools(sources, o = {}) {
|
|
17
|
+
const live = sources.filter((s) => s.access !== 'off');
|
|
18
|
+
const results = await Promise.allSettled(live.map((s) => s.source()));
|
|
19
|
+
const opened = [];
|
|
20
|
+
results.forEach((r, i) => {
|
|
21
|
+
const s = live[i];
|
|
22
|
+
if (r.status === 'fulfilled' && s)
|
|
23
|
+
opened.push({ s, open: r.value });
|
|
24
|
+
});
|
|
25
|
+
const max = o.maxChars ?? 16_000;
|
|
26
|
+
const tools = {};
|
|
27
|
+
const reads = [];
|
|
28
|
+
for (const { s, open } of opened)
|
|
29
|
+
for (const def of open.tools) {
|
|
30
|
+
const canWrite = s.access === 'draft' || s.access === 'write';
|
|
31
|
+
if (!def.read && !canWrite)
|
|
32
|
+
continue;
|
|
33
|
+
const kind = def.read ? 'read' : s.access === 'write' && s.writes ? 'write' : 'draft';
|
|
34
|
+
if (kind === 'draft' && !o.draft)
|
|
35
|
+
continue;
|
|
36
|
+
const name = toolName(s.group, def.name);
|
|
37
|
+
if (kind === 'read')
|
|
38
|
+
reads.push(name);
|
|
39
|
+
tools[name] = tool({
|
|
40
|
+
description: describe(def, kind),
|
|
41
|
+
inputSchema: jsonSchema(withoutMeta(def.inputSchema)),
|
|
42
|
+
execute: async (args) => {
|
|
43
|
+
if (kind === 'draft')
|
|
44
|
+
return o.draft?.({ group: s.group, tool: def.name, args });
|
|
45
|
+
const t0 = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const out = await open.call(def.name, args);
|
|
48
|
+
o.called?.({ group: s.group, tool: def.name, ok: true, ms: Date.now() - t0 });
|
|
49
|
+
const text = typeof out === 'string' ? out : JSON.stringify(out);
|
|
50
|
+
return text.length > max ? `${text.slice(0, max)} …(cut)` : out;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
o.called?.({ group: s.group, tool: def.name, ok: false, ms: Date.now() - t0 });
|
|
54
|
+
return { error: String(err?.message ?? err).slice(0, 500) };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
tools,
|
|
61
|
+
reads,
|
|
62
|
+
failed: results.filter((r) => r.status === 'rejected').length,
|
|
63
|
+
close: async () => {
|
|
64
|
+
await Promise.all(opened.map((x) => x.open.close().catch(() => undefined)));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** An AI SDK step's usage as the kit's tokens and list-price cost. */
|
|
2
|
+
import type { UsagePart } from '@columenlabs/lumen-ui-types/assistant';
|
|
3
|
+
import type { LanguageModelUsage } from 'ai';
|
|
4
|
+
import { type ModelDef } from '../assistant/models.js';
|
|
5
|
+
/**
|
|
6
|
+
* One call's usage in the kit's terms. Input splits into plain, cache writes and cache reads where
|
|
7
|
+
* the provider says (Claude reports all three; OpenAI reports reads, its writes cost nothing extra).
|
|
8
|
+
* A field a provider leaves out counts as 0.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tokenUsage(u: LanguageModelUsage): {
|
|
11
|
+
input_tokens: number;
|
|
12
|
+
output_tokens: number;
|
|
13
|
+
cache_creation_input_tokens: number;
|
|
14
|
+
cache_read_input_tokens: number;
|
|
15
|
+
};
|
|
16
|
+
/** A running part of an answer (a tier, its advice): calls, tokens and dollars. `free` on a plan. */
|
|
17
|
+
export declare class UsageMeter {
|
|
18
|
+
readonly model: ModelDef;
|
|
19
|
+
private readonly free;
|
|
20
|
+
private p;
|
|
21
|
+
constructor(model: ModelDef, label: string, free?: boolean);
|
|
22
|
+
add(u: LanguageModelUsage): {
|
|
23
|
+
in: number;
|
|
24
|
+
out: number;
|
|
25
|
+
read: number;
|
|
26
|
+
usd: number;
|
|
27
|
+
};
|
|
28
|
+
part(): UsagePart;
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { priced } from '../assistant/models.js';
|
|
2
|
+
/**
|
|
3
|
+
* One call's usage in the kit's terms. Input splits into plain, cache writes and cache reads where
|
|
4
|
+
* the provider says (Claude reports all three; OpenAI reports reads, its writes cost nothing extra).
|
|
5
|
+
* A field a provider leaves out counts as 0.
|
|
6
|
+
*/
|
|
7
|
+
export function tokenUsage(u) {
|
|
8
|
+
const d = u.inputTokenDetails;
|
|
9
|
+
const read = d?.cacheReadTokens ?? 0;
|
|
10
|
+
const written = d?.cacheWriteTokens ?? 0;
|
|
11
|
+
const plain = d?.noCacheTokens ?? Math.max(0, (u.inputTokens ?? 0) - read - written);
|
|
12
|
+
return {
|
|
13
|
+
input_tokens: plain,
|
|
14
|
+
output_tokens: u.outputTokens ?? 0,
|
|
15
|
+
cache_creation_input_tokens: written,
|
|
16
|
+
cache_read_input_tokens: read,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** A running part of an answer (a tier, its advice): calls, tokens and dollars. `free` on a plan. */
|
|
20
|
+
export class UsageMeter {
|
|
21
|
+
model;
|
|
22
|
+
free;
|
|
23
|
+
p;
|
|
24
|
+
constructor(model, label, free = false) {
|
|
25
|
+
this.model = model;
|
|
26
|
+
this.free = free;
|
|
27
|
+
this.p = { label, calls: 0, in: 0, read: 0, out: 0, usd: 0 };
|
|
28
|
+
}
|
|
29
|
+
add(u) {
|
|
30
|
+
const t = priced(this.model, tokenUsage(u));
|
|
31
|
+
this.p.calls += 1;
|
|
32
|
+
this.p.in += t.in;
|
|
33
|
+
this.p.read += t.read;
|
|
34
|
+
this.p.out += t.out;
|
|
35
|
+
if (!this.free)
|
|
36
|
+
this.p.usd += t.usd;
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
39
|
+
part() {
|
|
40
|
+
return { ...this.p };
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.177",
|
|
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",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"./assistant": {
|
|
19
19
|
"types": "./dist/assistant/index.d.ts",
|
|
20
20
|
"default": "./dist/assistant/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./agent": {
|
|
23
|
+
"types": "./dist/agent/index.d.ts",
|
|
24
|
+
"default": "./dist/agent/index.js"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
@@ -40,9 +44,15 @@
|
|
|
40
44
|
"registry": "https://registry.npmjs.org/"
|
|
41
45
|
},
|
|
42
46
|
"dependencies": {
|
|
47
|
+
"@ai-sdk/anthropic": "4.0.96",
|
|
48
|
+
"@ai-sdk/openai": "4.0.108",
|
|
43
49
|
"@anthropic-ai/sdk": "0.133.0",
|
|
44
|
-
"@
|
|
45
|
-
"@columenlabs/lumen-
|
|
50
|
+
"@cloudflare/think": "0.23.0",
|
|
51
|
+
"@columenlabs/lumen-ops": "0.1.177",
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.177",
|
|
53
|
+
"agents": "0.32.0",
|
|
54
|
+
"ai": "7.0.162",
|
|
55
|
+
"just-bash": "3.6.0",
|
|
46
56
|
"openai": "7.32.0",
|
|
47
57
|
"zod": "^4.3.6"
|
|
48
58
|
},
|