@columenlabs/lumen-ai 0.1.139 → 0.1.140
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/dist/assistant/engine.d.ts +4 -0
- package/dist/assistant/engine.js +125 -11
- package/package.json +3 -3
|
@@ -214,6 +214,8 @@ export declare class Assistant {
|
|
|
214
214
|
/** Opens the tools the person allows: each group's, then the web's. */
|
|
215
215
|
private openTools;
|
|
216
216
|
private toolsFor;
|
|
217
|
+
/** `batch`: any of the reads, up to BATCH_MAX at once, as one step. Null with no reads. */
|
|
218
|
+
private batchTool;
|
|
217
219
|
/** One answer as server-sent events. Abort `signal` to stop it. */
|
|
218
220
|
turnStream(caller: Caller, req: AssistantMessage, signal?: AbortSignal): ReadableStream<Uint8Array>;
|
|
219
221
|
turn(caller: Caller, req: AssistantMessage, emit: Emit, signal?: AbortSignal): Promise<void>;
|
|
@@ -229,6 +231,8 @@ export declare class Assistant {
|
|
|
229
231
|
private handOff;
|
|
230
232
|
/** One tool call: a draft card for a write, else the call itself, timed. */
|
|
231
233
|
private runTool;
|
|
234
|
+
/** A batch of reads: each runs as its own call (the same read twice runs once), in parallel, shown as one step. */
|
|
235
|
+
private runBatch;
|
|
232
236
|
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
233
237
|
private loop;
|
|
234
238
|
/** The loop on OpenAI's Responses API: the same tools, Stop, caps and rounds as Claude's. */
|
package/dist/assistant/engine.js
CHANGED
|
@@ -45,6 +45,11 @@ const COMPACT_AT = 150_000;
|
|
|
45
45
|
/** A tool result handed to the model is cut past this (about 4,000 tokens). */
|
|
46
46
|
const RESULT_CHARS = 16_000;
|
|
47
47
|
const MEMO_CHARS = 8000;
|
|
48
|
+
/** The built-in tool that runs several reads at once, as one step. */
|
|
49
|
+
const BATCH = 'batch';
|
|
50
|
+
const BATCH_MAX = 25;
|
|
51
|
+
/** What one batch returns at most, all its results together. */
|
|
52
|
+
const BATCH_CHARS = 48_000;
|
|
48
53
|
const ADVICE_MAX = 2;
|
|
49
54
|
const ADVICE_TOKENS = 2048;
|
|
50
55
|
const ADVISOR_BETA = 'advisor-tool-2026-03-01';
|
|
@@ -631,14 +636,49 @@ export class Assistant {
|
|
|
631
636
|
};
|
|
632
637
|
}
|
|
633
638
|
toolsFor(fns) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
639
|
+
const batch = this.batchTool(fns);
|
|
640
|
+
return [
|
|
641
|
+
...[...fns.entries()].map(([name, f]) => ({
|
|
642
|
+
name,
|
|
643
|
+
description: `${f.draft ? "DRAFT ONLY: this call changes nothing. It puts a card on the person's screen that they Save or Dismiss. " : f.write ? 'WRITES AT ONCE: this call makes the change now; the person can undo it. ' : ''}${f.def.title ? `${f.def.title}. ` : ''}${f.def.description ?? ''}`.slice(0, 4000),
|
|
644
|
+
input_schema: {
|
|
645
|
+
type: 'object',
|
|
646
|
+
...withoutMeta(f.def.inputSchema),
|
|
647
|
+
},
|
|
648
|
+
})),
|
|
649
|
+
...(batch ? [batch] : []),
|
|
650
|
+
];
|
|
651
|
+
}
|
|
652
|
+
/** `batch`: any of the reads, up to BATCH_MAX at once, as one step. Null with no reads. */
|
|
653
|
+
batchTool(fns) {
|
|
654
|
+
if (fns.has(BATCH))
|
|
655
|
+
return null;
|
|
656
|
+
const reads = [...fns.entries()].filter(([, f]) => !f.draft && !f.write).map(([n]) => n);
|
|
657
|
+
if (!reads.length)
|
|
658
|
+
return null;
|
|
659
|
+
return {
|
|
660
|
+
name: BATCH,
|
|
661
|
+
description: `Runs up to ${BATCH_MAX} read calls at once and returns each result, numbered in order. Use it to read several records (one tool with several inputs) or several tools that do not depend on each other, in one step instead of one call at a time. Reads only.`,
|
|
637
662
|
input_schema: {
|
|
638
663
|
type: 'object',
|
|
639
|
-
|
|
664
|
+
properties: {
|
|
665
|
+
calls: {
|
|
666
|
+
type: 'array',
|
|
667
|
+
minItems: 1,
|
|
668
|
+
maxItems: BATCH_MAX,
|
|
669
|
+
items: {
|
|
670
|
+
type: 'object',
|
|
671
|
+
properties: {
|
|
672
|
+
tool: { type: 'string', enum: reads },
|
|
673
|
+
input: { type: 'object', description: "The tool's own input." },
|
|
674
|
+
},
|
|
675
|
+
required: ['tool', 'input'],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
required: ['calls'],
|
|
640
680
|
},
|
|
641
|
-
}
|
|
681
|
+
};
|
|
642
682
|
}
|
|
643
683
|
// ---------------------------------------------------------------- the turn
|
|
644
684
|
/** One answer as server-sent events. Abort `signal` to stop it. */
|
|
@@ -918,6 +958,11 @@ export class Assistant {
|
|
|
918
958
|
const lines = [
|
|
919
959
|
`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.`,
|
|
920
960
|
'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.',
|
|
961
|
+
...([...fns.values()].some((f) => !f.draft && !f.write) && !fns.has(BATCH)
|
|
962
|
+
? [
|
|
963
|
+
`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.`,
|
|
964
|
+
]
|
|
965
|
+
: []),
|
|
921
966
|
'A tool marked DRAFT ONLY makes a card the person saves or dismisses; it changes nothing by itself. Say what the card will do; never claim it is done. The next message tells you how each card ended.',
|
|
922
967
|
'A tool marked WRITES AT ONCE makes its change when you call it; the person can undo it from the chat. Change only what they asked for, and say what you changed.',
|
|
923
968
|
];
|
|
@@ -933,7 +978,7 @@ export class Assistant {
|
|
|
933
978
|
const reach = this.above(main.tier);
|
|
934
979
|
if (reach.length) {
|
|
935
980
|
const own = policy === 'any' ? reach : policy === 'next' ? reach.slice(0, 1) : [];
|
|
936
|
-
lines.push(`Tiers: you run on ${main.tier.label}. The escalate tool hands one task to a higher tier (${reach.map((t) => t.label).join(' or ')}) with the same tools; it works in its own section of the chat and returns a memo. It costs more than answering yourself.`, `Call it when the person names a tier or says "go deeper"${own.length ? `, and on your own (${own.map((t) => t.label).join(' or ')}) when the question needs judgement
|
|
981
|
+
lines.push(`Tiers: you run on ${main.tier.label}. The escalate tool hands one task to a higher tier (${reach.map((t) => t.label).join(' or ')}) with the same tools; it works in its own section of the chat and returns a memo. It costs more than answering yourself.`, `Call it when the person names a tier or says "go deeper"${own.length ? `, and on your own (${own.map((t) => t.label).join(' or ')}) when the question needs judgement a lookup cannot settle (why "judgement"), after two of your tool calls failed (why "stuck"), or when a fix of a check still fails (why "check_failed")` : ''}. Lookups you answer yourself: reading or checking many records against a plain rule (an ingredient, a date, a number, a name) is a lookup, done with ${BATCH}, never escalated.`, 'Its task must carry every fact from this chat it needs: it does not see the chat. Then answer from its memo in your own short words.');
|
|
937
982
|
}
|
|
938
983
|
if (advisor)
|
|
939
984
|
lines.push(`Advisor: the advisor tool (no input) asks ${advisor.session.tier.label} to read this chat and send you a plan; it runs no tools. Call it before a question that needs judgement or after a tool failed, then do the work yourself. At most ${ADVICE_MAX} times an answer. Ask the advisor before you escalate.`);
|
|
@@ -1086,13 +1131,17 @@ export class Assistant {
|
|
|
1086
1131
|
: reply(`Memo from ${item.label}:\n${memo.slice(0, MEMO_CHARS)}`);
|
|
1087
1132
|
}
|
|
1088
1133
|
/** One tool call: a draft card for a write, else the call itself, timed. */
|
|
1089
|
-
async runTool(use, fns, sink, emit, runs, counts, session, handoff, signal
|
|
1134
|
+
async runTool(use, fns, sink, emit, runs, counts, session, handoff, signal,
|
|
1135
|
+
/** Part of a batch: runs and is remembered, but draws no step of its own. */
|
|
1136
|
+
quiet = false) {
|
|
1090
1137
|
const reply = (content, is_error = false) => ({
|
|
1091
1138
|
type: 'tool_result',
|
|
1092
1139
|
tool_use_id: use.id,
|
|
1093
1140
|
content,
|
|
1094
1141
|
is_error,
|
|
1095
1142
|
});
|
|
1143
|
+
if (use.name === BATCH && !fns.has(BATCH) && !quiet)
|
|
1144
|
+
return this.runBatch(use, fns, sink, emit, runs, counts, session, handoff, signal);
|
|
1096
1145
|
const f = fns.get(use.name);
|
|
1097
1146
|
if (!f)
|
|
1098
1147
|
return reply(`No tool ${use.name}.`, true);
|
|
@@ -1123,7 +1172,8 @@ export class Assistant {
|
|
|
1123
1172
|
settle = done;
|
|
1124
1173
|
}));
|
|
1125
1174
|
const id = newId('s');
|
|
1126
|
-
|
|
1175
|
+
if (!quiet)
|
|
1176
|
+
emit('step_start', { id, tool: use.name, title });
|
|
1127
1177
|
const t0 = performance.now();
|
|
1128
1178
|
let out;
|
|
1129
1179
|
let ok = true;
|
|
@@ -1160,9 +1210,11 @@ export class Assistant {
|
|
|
1160
1210
|
? { write: { undo: !!this.groupById(f.group)?.undo, status: 'done' } }
|
|
1161
1211
|
: {}),
|
|
1162
1212
|
};
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1213
|
+
if (!quiet) {
|
|
1214
|
+
sink.push(item);
|
|
1215
|
+
emit('step', item);
|
|
1216
|
+
counts.steps += 1;
|
|
1217
|
+
}
|
|
1166
1218
|
// A change made at once can make an earlier read stale: reads after it run again.
|
|
1167
1219
|
if (f.write)
|
|
1168
1220
|
counts.seen.clear();
|
|
@@ -1187,6 +1239,68 @@ export class Assistant {
|
|
|
1187
1239
|
}
|
|
1188
1240
|
return reply(cut, !ok);
|
|
1189
1241
|
}
|
|
1242
|
+
/** A batch of reads: each runs as its own call (the same read twice runs once), in parallel, shown as one step. */
|
|
1243
|
+
async runBatch(use, fns, sink, emit, runs, counts, session, handoff, signal) {
|
|
1244
|
+
const reply = (content, is_error = false) => ({
|
|
1245
|
+
type: 'tool_result',
|
|
1246
|
+
tool_use_id: use.id,
|
|
1247
|
+
content,
|
|
1248
|
+
is_error,
|
|
1249
|
+
});
|
|
1250
|
+
const raw = use.input?.calls;
|
|
1251
|
+
const calls = (Array.isArray(raw) ? raw : []).map((c) => {
|
|
1252
|
+
const o = (c && typeof c === 'object' ? c : {});
|
|
1253
|
+
return {
|
|
1254
|
+
tool: typeof o.tool === 'string' ? o.tool : '',
|
|
1255
|
+
input: o.input && typeof o.input === 'object' ? o.input : {},
|
|
1256
|
+
};
|
|
1257
|
+
});
|
|
1258
|
+
if (!calls.length)
|
|
1259
|
+
return reply(`Give {calls: [{tool, input}]}: 1 to ${BATCH_MAX} reads.`, true);
|
|
1260
|
+
if (calls.length > BATCH_MAX)
|
|
1261
|
+
return reply(`At most ${BATCH_MAX} calls a batch: split them into batches.`, true);
|
|
1262
|
+
const titles = [...new Set(calls.map((c) => fns.get(c.tool)?.def.title ?? c.tool))];
|
|
1263
|
+
const title = titles.length === 1 ? `${titles[0]} ×${calls.length}` : `${calls.length} reads`;
|
|
1264
|
+
const id = newId('s');
|
|
1265
|
+
emit('step_start', { id, tool: BATCH, title });
|
|
1266
|
+
const t0 = performance.now();
|
|
1267
|
+
const done = await Promise.all(calls.map(async (c, i) => {
|
|
1268
|
+
const f = fns.get(c.tool);
|
|
1269
|
+
if (!f)
|
|
1270
|
+
return { ...c, ok: false, text: `No tool ${c.tool}.` };
|
|
1271
|
+
if (f.draft || f.write)
|
|
1272
|
+
return { ...c, ok: false, text: 'Only reads go in a batch: call this tool on its own.' };
|
|
1273
|
+
const r = await this.runTool({
|
|
1274
|
+
type: 'tool_use',
|
|
1275
|
+
id: `${use.id}_${i}`,
|
|
1276
|
+
name: c.tool,
|
|
1277
|
+
input: c.input,
|
|
1278
|
+
}, fns, sink, emit, runs, counts, session, handoff, signal, true);
|
|
1279
|
+
return { ...c, ok: !r.is_error, text: typeof r.content === 'string' ? r.content : '' };
|
|
1280
|
+
}));
|
|
1281
|
+
const failed = done.filter((d) => !d.ok).length;
|
|
1282
|
+
const item = {
|
|
1283
|
+
kind: 'step',
|
|
1284
|
+
id,
|
|
1285
|
+
tool: BATCH,
|
|
1286
|
+
title,
|
|
1287
|
+
ok: failed < done.length,
|
|
1288
|
+
ms: Math.round(performance.now() - t0),
|
|
1289
|
+
...(failed ? { error: `${failed} of ${done.length} failed` } : {}),
|
|
1290
|
+
};
|
|
1291
|
+
sink.push(item);
|
|
1292
|
+
emit('step', item);
|
|
1293
|
+
counts.steps += 1;
|
|
1294
|
+
let left = BATCH_CHARS;
|
|
1295
|
+
const parts = done.map((d, i) => {
|
|
1296
|
+
const fits = d.text.length <= left;
|
|
1297
|
+
left -= fits ? d.text.length : 0;
|
|
1298
|
+
return `[${i + 1}] ${d.tool} ${stableJson(d.input)}${d.ok ? '' : ' (failed)'}\n${fits
|
|
1299
|
+
? d.text
|
|
1300
|
+
: '(left out: the batch passed its size; read this one again in a smaller batch)'}`;
|
|
1301
|
+
});
|
|
1302
|
+
return reply(parts.join('\n\n'), failed === done.length);
|
|
1303
|
+
}
|
|
1190
1304
|
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
1191
1305
|
loop(o) {
|
|
1192
1306
|
const { provider } = o.session.model;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.140",
|
|
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",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@anthropic-ai/sdk": "0.133.0",
|
|
44
|
-
"@columenlabs/lumen-ops": "0.1.
|
|
45
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
44
|
+
"@columenlabs/lumen-ops": "0.1.140",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.140",
|
|
46
46
|
"openai": "7.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|