@columenlabs/lumen-ai 0.1.138 → 0.1.139
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.
|
@@ -239,8 +239,6 @@ export declare class Assistant {
|
|
|
239
239
|
private compact;
|
|
240
240
|
/** How the person's cards ended since the last answer, once each. */
|
|
241
241
|
private cardNews;
|
|
242
|
-
/** The answer as later turns remember it: with what its tools returned, cut. */
|
|
243
|
-
private remember;
|
|
244
242
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
245
243
|
startSignin(caller: Caller, provider: AssistantProvider): Promise<{
|
|
246
244
|
url: string;
|
package/dist/assistant/engine.js
CHANGED
|
@@ -48,9 +48,16 @@ const MEMO_CHARS = 8000;
|
|
|
48
48
|
const ADVICE_MAX = 2;
|
|
49
49
|
const ADVICE_TOKENS = 2048;
|
|
50
50
|
const ADVISOR_BETA = 'advisor-tool-2026-03-01';
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* What later turns remember of the tools' results, so a follow-up ("list them all", "which have no
|
|
53
|
+
* milk") answers from what was read instead of calling the same tools again. Each result is kept
|
|
54
|
+
* as the model saw it (`RAN_CHARS`), at most `RAN_TURN` a turn as stored, and the whole chat sends
|
|
55
|
+
* at most `RAN_CHAT`: newest first, a call repeated later kept only in its latest turn, and past
|
|
56
|
+
* the budget an older result is named but dropped, so a long chat costs no more than a short one.
|
|
57
|
+
*/
|
|
58
|
+
const RAN_CHARS = RESULT_CHARS;
|
|
59
|
+
const RAN_TURN = 40_000;
|
|
60
|
+
const RAN_CHAT = 40_000;
|
|
54
61
|
const CHATS_LISTED = 200;
|
|
55
62
|
const WHY_TEXT = {
|
|
56
63
|
asked: 'you asked',
|
|
@@ -58,6 +65,58 @@ const WHY_TEXT = {
|
|
|
58
65
|
stuck: 'stuck',
|
|
59
66
|
check_failed: 'check failed',
|
|
60
67
|
};
|
|
68
|
+
/** JSON with object keys sorted, so the same input written in another order is the same call. */
|
|
69
|
+
function stableJson(v) {
|
|
70
|
+
return JSON.stringify(v, (_k, x) => x && typeof x === 'object' && !Array.isArray(x)
|
|
71
|
+
? Object.fromEntries(Object.entries(x).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
72
|
+
: x);
|
|
73
|
+
}
|
|
74
|
+
/** A turn's tool results as stored: each cut to what the model saw, the turn to `RAN_TURN`. */
|
|
75
|
+
function ranOf(runs) {
|
|
76
|
+
let left = RAN_TURN;
|
|
77
|
+
const ran = [];
|
|
78
|
+
for (const r of runs) {
|
|
79
|
+
if (left <= 0)
|
|
80
|
+
break;
|
|
81
|
+
const result = r.result.slice(0, Math.min(RAN_CHARS, left));
|
|
82
|
+
left -= result.length;
|
|
83
|
+
ran.push({ ...r, result: result.length < r.result.length ? `${result} …(cut)` : result });
|
|
84
|
+
}
|
|
85
|
+
return ran.length ? { ran } : {};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The history as sent: each answer followed by what its tools returned, within `RAN_CHAT` for the
|
|
89
|
+
* whole chat, newest first. A call made again later is kept only there; one past the budget is
|
|
90
|
+
* named so the model knows to call it again. Chats stored before `ran` carry it in `content`.
|
|
91
|
+
*/
|
|
92
|
+
function toolMemory(history) {
|
|
93
|
+
let left = RAN_CHAT;
|
|
94
|
+
const later = new Set();
|
|
95
|
+
const out = [];
|
|
96
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
97
|
+
const t = history[i];
|
|
98
|
+
if (!t.ran?.length) {
|
|
99
|
+
out.push({ role: t.role, content: t.content });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const lines = t.ran.map((r) => {
|
|
103
|
+
if (r.key && later.has(r.key))
|
|
104
|
+
return `- ${r.tool} → (called again later; see there)`;
|
|
105
|
+
if (r.key)
|
|
106
|
+
later.add(r.key);
|
|
107
|
+
if (left < 200)
|
|
108
|
+
return `- ${r.tool} → (no longer kept; call it again if you need it)`;
|
|
109
|
+
const kept = r.result.slice(0, left);
|
|
110
|
+
left -= kept.length;
|
|
111
|
+
return `- ${r.tool} → ${kept}${kept.length < r.result.length ? ' …(cut)' : ''}`;
|
|
112
|
+
});
|
|
113
|
+
out.push({
|
|
114
|
+
role: t.role,
|
|
115
|
+
content: `${t.content}\n\n[Tools I used and what they returned]\n${lines.join('\n')}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return out.reverse();
|
|
119
|
+
}
|
|
61
120
|
const NO_USAGE = { calls: 0, in: 0, read: 0, out: 0, usd: 0, parts: [] };
|
|
62
121
|
function addUsage(u, parts) {
|
|
63
122
|
const out = { ...u, parts: u.parts.map((p) => ({ ...p })) };
|
|
@@ -684,7 +743,14 @@ export class Assistant {
|
|
|
684
743
|
const tools = await this.openTools(caller, p);
|
|
685
744
|
const runs = [];
|
|
686
745
|
doc.writes ??= {};
|
|
687
|
-
const counts = {
|
|
746
|
+
const counts = {
|
|
747
|
+
steps: 0,
|
|
748
|
+
handoffs: 0,
|
|
749
|
+
advice: 0,
|
|
750
|
+
end: main.tier,
|
|
751
|
+
writes: doc.writes,
|
|
752
|
+
seen: new Map(),
|
|
753
|
+
};
|
|
688
754
|
let outcome = 'ok';
|
|
689
755
|
try {
|
|
690
756
|
const appSystem = typeof this.o.system === 'string' ? this.o.system : await this.o.system(caller);
|
|
@@ -738,7 +804,7 @@ export class Assistant {
|
|
|
738
804
|
? `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`
|
|
739
805
|
: '';
|
|
740
806
|
const messages = [
|
|
741
|
-
...doc.history
|
|
807
|
+
...toolMemory(doc.history),
|
|
742
808
|
{ role: 'user', content: `${page}${lead}${asked}` },
|
|
743
809
|
];
|
|
744
810
|
const escalateTool = this.escalateTool(main.tier);
|
|
@@ -791,7 +857,7 @@ export class Assistant {
|
|
|
791
857
|
emit('text_end', {});
|
|
792
858
|
}
|
|
793
859
|
doc.history.push({ role: 'user', content: `${page}${asked}` });
|
|
794
|
-
doc.history.push({ role: 'assistant', content:
|
|
860
|
+
doc.history.push({ role: 'assistant', content: answer, ...ranOf(runs) });
|
|
795
861
|
}
|
|
796
862
|
catch (err) {
|
|
797
863
|
if (signal?.aborted) {
|
|
@@ -1045,6 +1111,17 @@ export class Assistant {
|
|
|
1045
1111
|
await Promise.resolve(this.o.onStep?.({ kind: 'draft', tool: use.name, tier: session.tier.id, handoff })).catch(() => undefined);
|
|
1046
1112
|
return reply(`Drafted: a "${title}" card is on the person's screen. Nothing changed until they press Save.`);
|
|
1047
1113
|
}
|
|
1114
|
+
// The same read with the same input in one answer gives back what it gave, without running it
|
|
1115
|
+
// again or showing a second step: a model that lost track of a result reads it here, not twice.
|
|
1116
|
+
const key = f.write ? '' : `${use.name}\u0000${stableJson(use.input)}`;
|
|
1117
|
+
const again = key ? counts.seen.get(key) : undefined;
|
|
1118
|
+
if (again)
|
|
1119
|
+
return reply(`(Same call as earlier in this answer; its result again.)\n${await again}`);
|
|
1120
|
+
let settle = () => undefined;
|
|
1121
|
+
if (key)
|
|
1122
|
+
counts.seen.set(key, new Promise((done) => {
|
|
1123
|
+
settle = done;
|
|
1124
|
+
}));
|
|
1048
1125
|
const id = newId('s');
|
|
1049
1126
|
emit('step_start', { id, tool: use.name, title });
|
|
1050
1127
|
const t0 = performance.now();
|
|
@@ -1060,8 +1137,10 @@ export class Assistant {
|
|
|
1060
1137
|
out = typeof result === 'string' ? result : JSON.stringify(result);
|
|
1061
1138
|
}
|
|
1062
1139
|
catch (err) {
|
|
1063
|
-
if (signal?.aborted)
|
|
1140
|
+
if (signal?.aborted) {
|
|
1141
|
+
settle('(stopped)');
|
|
1064
1142
|
throw err;
|
|
1143
|
+
}
|
|
1065
1144
|
ok = false;
|
|
1066
1145
|
error =
|
|
1067
1146
|
err instanceof ToolError || err instanceof Error ? err.message.slice(0, 300) : 'failed';
|
|
@@ -1084,7 +1163,10 @@ export class Assistant {
|
|
|
1084
1163
|
sink.push(item);
|
|
1085
1164
|
emit('step', item);
|
|
1086
1165
|
counts.steps += 1;
|
|
1087
|
-
|
|
1166
|
+
// A change made at once can make an earlier read stale: reads after it run again.
|
|
1167
|
+
if (f.write)
|
|
1168
|
+
counts.seen.clear();
|
|
1169
|
+
runs.push({ tool: use.name, key, result: out });
|
|
1088
1170
|
await Promise.resolve(this.o.onStep?.({
|
|
1089
1171
|
kind: 'tool',
|
|
1090
1172
|
tool: use.name,
|
|
@@ -1095,7 +1177,14 @@ export class Assistant {
|
|
|
1095
1177
|
tier: session.tier.id,
|
|
1096
1178
|
handoff,
|
|
1097
1179
|
})).catch(() => undefined);
|
|
1098
|
-
const cut = out.length > RESULT_CHARS
|
|
1180
|
+
const cut = out.length > RESULT_CHARS
|
|
1181
|
+
? `${out.slice(0, RESULT_CHARS)} …(cut at ${RESULT_CHARS.toLocaleString('en-US')} of ${out.length.toLocaleString('en-US')} characters: the rest was not read; ask again for less)`
|
|
1182
|
+
: out;
|
|
1183
|
+
if (key) {
|
|
1184
|
+
settle(cut);
|
|
1185
|
+
if (!ok)
|
|
1186
|
+
counts.seen.delete(key);
|
|
1187
|
+
}
|
|
1099
1188
|
return reply(cut, !ok);
|
|
1100
1189
|
}
|
|
1101
1190
|
/** Model calls and tool rounds until the model answers; returns its words. */
|
|
@@ -1493,7 +1582,7 @@ export class Assistant {
|
|
|
1493
1582
|
if (doc.history.length < 2)
|
|
1494
1583
|
return false;
|
|
1495
1584
|
const system = `Summarise this chat between a person and ${this.name}, their assistant, so ${this.name} can carry on from the summary alone. Keep what the person wants, the facts and numbers found, decisions made and cards drafted or saved, and anything still open. Plain bullets, at most 400 words. No preamble.`;
|
|
1496
|
-
const chat = doc.history
|
|
1585
|
+
const chat = toolMemory(doc.history)
|
|
1497
1586
|
.map((t) => `${t.role === 'user' ? 'Person' : this.name}: ${t.content}`)
|
|
1498
1587
|
.join('\n\n');
|
|
1499
1588
|
let summary = '';
|
|
@@ -1554,21 +1643,6 @@ export class Assistant {
|
|
|
1554
1643
|
.map((d) => `- ${d.title} ${JSON.stringify(d.args).slice(0, 300)}: ${d.status}${d.error ? ` (${d.error})` : ''}${d.result !== undefined ? ` → ${JSON.stringify(d.result).slice(0, 300)}` : ''}`)
|
|
1555
1644
|
.join('\n')}\n\n`;
|
|
1556
1645
|
}
|
|
1557
|
-
/** The answer as later turns remember it: with what its tools returned, cut. */
|
|
1558
|
-
remember(answer, runs) {
|
|
1559
|
-
if (!runs.length)
|
|
1560
|
-
return answer;
|
|
1561
|
-
let left = RAN_TOTAL;
|
|
1562
|
-
const lines = [];
|
|
1563
|
-
for (const r of runs) {
|
|
1564
|
-
if (left <= 0)
|
|
1565
|
-
break;
|
|
1566
|
-
const line = `- ${r.tool} → ${r.result.slice(0, Math.min(RAN_CHARS, left))}`;
|
|
1567
|
-
left -= line.length;
|
|
1568
|
-
lines.push(line);
|
|
1569
|
-
}
|
|
1570
|
-
return `${answer}\n\n[Tools I used and what they returned]\n${lines.join('\n')}`;
|
|
1571
|
-
}
|
|
1572
1646
|
// ---------------------------------------------------------------- keys
|
|
1573
1647
|
/** Starts a person's ChatGPT plan sign-in: the address to open in a new tab. */
|
|
1574
1648
|
async startSignin(caller, provider) {
|
|
@@ -45,5 +45,5 @@ export interface PlainTool extends ToolDef {
|
|
|
45
45
|
export declare function plainTools(tools: PlainTool[]): ToolSource;
|
|
46
46
|
/** A tool name the model may use: letters, digits, `_` and `-`, at most 64. */
|
|
47
47
|
export declare function toolName(group: string, name: string): string;
|
|
48
|
-
/** Rows in a list result: an array, or the
|
|
48
|
+
/** Rows in a list result: an array, an object's `count`, or the one array an object holds. */
|
|
49
49
|
export declare function rowCount(v: unknown): number | undefined;
|
package/dist/assistant/tools.js
CHANGED
|
@@ -98,13 +98,17 @@ export function plainTools(tools) {
|
|
|
98
98
|
export function toolName(group, name) {
|
|
99
99
|
return `${group}__${name}`.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
100
100
|
}
|
|
101
|
-
/** Rows in a list result: an array, or the
|
|
101
|
+
/** Rows in a list result: an array, an object's `count`, or the one array an object holds. */
|
|
102
102
|
export function rowCount(v) {
|
|
103
103
|
if (Array.isArray(v))
|
|
104
104
|
return v.length;
|
|
105
|
-
if (v
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (!v || typeof v !== 'object')
|
|
106
|
+
return undefined;
|
|
107
|
+
// A list with its total says so; else the one list it holds. A record with several lists (a
|
|
108
|
+
// recipe's ingredients and steps) is one thing, not rows.
|
|
109
|
+
const o = v;
|
|
110
|
+
if (typeof o.count === 'number' && Number.isFinite(o.count))
|
|
111
|
+
return o.count;
|
|
112
|
+
const lists = Object.values(o).filter(Array.isArray);
|
|
113
|
+
return lists.length === 1 ? lists[0]?.length : undefined;
|
|
110
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.139",
|
|
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.139",
|
|
45
|
+
"@columenlabs/lumen-ui-types": "0.1.139",
|
|
46
46
|
"openai": "7.32.0",
|
|
47
47
|
"zod": "^4.3.6"
|
|
48
48
|
},
|