@lumi.ai/runner 0.15.18 → 0.15.20
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/cli.js +148 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -63,6 +63,9 @@ var ENGINES = {
|
|
|
63
63
|
claude: CLAUDE_ENGINE
|
|
64
64
|
};
|
|
65
65
|
var DEFAULT_ENGINE_ID = "claude";
|
|
66
|
+
function listEngines() {
|
|
67
|
+
return Object.values(ENGINES);
|
|
68
|
+
}
|
|
66
69
|
function isKnownEngine(id) {
|
|
67
70
|
return !!id && Object.hasOwn(ENGINES, id);
|
|
68
71
|
}
|
|
@@ -82,6 +85,8 @@ function estimateCostUsd(engineId, modelId, tokens) {
|
|
|
82
85
|
const fresh = Math.max(0, tokens.inputTokens - tokens.cachedInputTokens);
|
|
83
86
|
return (fresh * model.rates.input + tokens.cachedInputTokens * model.rates.cachedInput + tokens.outputTokens * model.rates.output) / 1e6;
|
|
84
87
|
}
|
|
88
|
+
var AGENT_ENGINE_IDS = Object.freeze(listEngines().map((e) => e.id));
|
|
89
|
+
var AGENT_MODEL_IDS = Object.freeze(listEngines().flatMap((e) => e.models.map((m) => m.id)));
|
|
85
90
|
|
|
86
91
|
// ../shared/dist/agent.js
|
|
87
92
|
var DEFAULT_AGENT_TOOLS = {
|
|
@@ -144,6 +149,41 @@ var ASSISTANT_PROFILE = Object.freeze({
|
|
|
144
149
|
})
|
|
145
150
|
});
|
|
146
151
|
|
|
152
|
+
// ../shared/dist/assistantAuthority.js
|
|
153
|
+
var ASSISTANT_TOOL_AUTHORITY = Object.freeze({
|
|
154
|
+
// Reads. Everything the asker could open in the app.
|
|
155
|
+
ship_info: "member",
|
|
156
|
+
agent_list: "member",
|
|
157
|
+
task_list: "member",
|
|
158
|
+
task_get: "member",
|
|
159
|
+
knowledge_get: "member",
|
|
160
|
+
chat_get: "member",
|
|
161
|
+
// Writes a member can already make by clicking.
|
|
162
|
+
task_create: "member",
|
|
163
|
+
task_comment: "member",
|
|
164
|
+
task_update_status: "member",
|
|
165
|
+
task_relate: "member",
|
|
166
|
+
task_assign: "member",
|
|
167
|
+
knowledge_write: "member",
|
|
168
|
+
chat_send: "member",
|
|
169
|
+
// Captain-only, because the rules say so for a direct write — and `agent_write` also bills.
|
|
170
|
+
agent_write: "captain",
|
|
171
|
+
workflow_write: "captain",
|
|
172
|
+
knowledge_folder: "captain"
|
|
173
|
+
});
|
|
174
|
+
var ASSISTANT_NEVER_REGISTER = Object.freeze([
|
|
175
|
+
"approval_decide",
|
|
176
|
+
"approval_request",
|
|
177
|
+
"memory_write",
|
|
178
|
+
"media_attach"
|
|
179
|
+
]);
|
|
180
|
+
var ASSISTANT_FROZEN_ARGS = Object.freeze({
|
|
181
|
+
agent_write: Object.freeze(["tools"])
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ../shared/dist/toolStats.js
|
|
185
|
+
var TOOL_STATS_HALF_LIFE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
186
|
+
|
|
147
187
|
// ../shared/dist/browser.js
|
|
148
188
|
var BROWSER_ONLINE_WINDOW_MS = 9e4;
|
|
149
189
|
var DEFAULT_BROWSER_CONSENT_MS = 60 * 60 * 1e3;
|
|
@@ -332,6 +372,8 @@ var COLLECTIONS = {
|
|
|
332
372
|
engineLimits: "engine_limits",
|
|
333
373
|
/** `ships/{shipId}/events/{id}` — append-only Ship-level audit log. */
|
|
334
374
|
events: "events",
|
|
375
|
+
/** §15.70: per-actor tool usage, so the assistant declares what it actually reaches for. */
|
|
376
|
+
toolStats: "toolStats",
|
|
335
377
|
/** `ships/{shipId}/secrets/{docId}` — runner creds + MCP token hash (see secrets.ts). */
|
|
336
378
|
secrets: "secrets",
|
|
337
379
|
/**
|
|
@@ -1023,7 +1065,7 @@ function mcpUrl(config2) {
|
|
|
1023
1065
|
}
|
|
1024
1066
|
|
|
1025
1067
|
// src/version.ts
|
|
1026
|
-
var RUNNER_VERSION = true ? "0.15.
|
|
1068
|
+
var RUNNER_VERSION = true ? "0.15.20" : "0.0.0-dev";
|
|
1027
1069
|
|
|
1028
1070
|
// src/auth.ts
|
|
1029
1071
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2085,16 +2127,40 @@ function detectClaudeLimit(text, now, fallbackMs) {
|
|
|
2085
2127
|
detail: text.trim().slice(0, 300)
|
|
2086
2128
|
};
|
|
2087
2129
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
2130
|
+
function expectedMcpServers(agent, extraServers = []) {
|
|
2131
|
+
const out = [];
|
|
2132
|
+
if (effectiveAgentTools(agent).workspaceMcp) {
|
|
2133
|
+
out.push({ key: WORKSPACE_MCP_KEY, label: "the Workspace MCP" });
|
|
2134
|
+
}
|
|
2135
|
+
for (const s of extraServers) out.push({ key: s.key, label: s.name || s.key });
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
function mcpConnectionFailures(initEvent, expected) {
|
|
2139
|
+
if (expected.length === 0) return [];
|
|
2140
|
+
const byKey = new Map(expected.map((e) => [e.key, e]));
|
|
2141
|
+
const found = /* @__PURE__ */ new Map();
|
|
2090
2142
|
const errors = initEvent.mcp_server_errors;
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2143
|
+
if (Array.isArray(errors)) {
|
|
2144
|
+
for (const e of errors) {
|
|
2145
|
+
if (!e || typeof e !== "object" || typeof e.name !== "string") continue;
|
|
2146
|
+
const want = byKey.get(e.name);
|
|
2147
|
+
if (want) found.set(want.key, { ...want, detail: e.message || e.type || "no reason given" });
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const servers = initEvent.mcp_servers;
|
|
2151
|
+
if (Array.isArray(servers)) {
|
|
2152
|
+
for (const s of servers) {
|
|
2153
|
+
if (!s || typeof s !== "object" || typeof s.name !== "string") continue;
|
|
2154
|
+
const want = byKey.get(s.name);
|
|
2155
|
+
if (want && s.status !== "connected" && !found.has(want.key)) {
|
|
2156
|
+
found.set(want.key, { ...want, detail: `status "${s.status ?? "unknown"}"` });
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return [...found.values()];
|
|
2161
|
+
}
|
|
2162
|
+
function workspaceRefusal(failure) {
|
|
2163
|
+
return `The Claude CLI did not load ${failure.label} ("${failure.key}"): ${failure.detail}. The session would have had none of this Ship's tools \u2014 no task_comment, no run_report, no chat_send \u2014 so it was stopped instead of run blind. If this keeps happening, update the daemon (\`npm i -g @lumi.ai/runner\`) and the Claude CLI on this machine.`;
|
|
2098
2164
|
}
|
|
2099
2165
|
function allowedTools(agent, extraServers = []) {
|
|
2100
2166
|
const granted = effectiveAgentTools(agent);
|
|
@@ -2202,6 +2268,21 @@ async function runSession(input, bin, dirs) {
|
|
|
2202
2268
|
const claudeToken = input.secrets?.claudeToken;
|
|
2203
2269
|
const env = {
|
|
2204
2270
|
...process.env,
|
|
2271
|
+
// THE INIT EVENT MUST BE TRUTHFUL, and one inherited variable is enough to make it lie.
|
|
2272
|
+
//
|
|
2273
|
+
// `MCP_CONNECTION_NONBLOCKING` lets the CLI start before its MCP servers have settled. The init
|
|
2274
|
+
// event then reports `mcp_servers: []` with no status and no error — indistinguishable from a
|
|
2275
|
+
// server that failed — while the connection completes moments later. `mcpConnectionFailures`
|
|
2276
|
+
// reads that event and can only be as honest as it is, so a daemon started from a shell that
|
|
2277
|
+
// happens to set this would either miss every blind session or refuse every healthy one.
|
|
2278
|
+
//
|
|
2279
|
+
// It is DELETED rather than set to a value, because "off" for these is absence: the CLI reads
|
|
2280
|
+
// the variable's presence. A daemon is a long-lived process started from somebody's terminal,
|
|
2281
|
+
// an npm script, or a launchd unit, and it must not inherit a decision about its own
|
|
2282
|
+
// observability from any of them. Found the hard way — every local session for a day reported
|
|
2283
|
+
// an empty server list because the shell that launched the daemon set this.
|
|
2284
|
+
MCP_CONNECTION_NONBLOCKING: void 0,
|
|
2285
|
+
MCP_SERVER_CONNECTION_BATCH_SIZE: void 0,
|
|
2205
2286
|
...claudeToken ? { CLAUDE_CODE_OAUTH_TOKEN: claudeToken } : {},
|
|
2206
2287
|
...input.githubToken ? {
|
|
2207
2288
|
GH_TOKEN: input.githubToken,
|
|
@@ -2222,6 +2303,8 @@ async function runSession(input, bin, dirs) {
|
|
|
2222
2303
|
let resultEvent = null;
|
|
2223
2304
|
let mcpProblem = null;
|
|
2224
2305
|
const workspaceRequired = effectiveAgentTools(input.agent).workspaceMcp;
|
|
2306
|
+
const expectedServers = expectedMcpServers(input.agent, input.extraMcpServers ?? []);
|
|
2307
|
+
const mcpFailed = [];
|
|
2225
2308
|
const mcpWatch = createWorkspaceMcpWatch();
|
|
2226
2309
|
const exitCode = await new Promise((resolve) => {
|
|
2227
2310
|
const child = spawn3(bin, args, { cwd: dirs.workdir, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -2259,7 +2342,14 @@ async function runSession(input, bin, dirs) {
|
|
|
2259
2342
|
if (event.type === "result") resultEvent = event;
|
|
2260
2343
|
if (event.type === "assistant") input.log("claude: assistant turn");
|
|
2261
2344
|
if (event.type === "system" && event.subtype === "init" && !mcpProblem) {
|
|
2262
|
-
|
|
2345
|
+
for (const failure of mcpConnectionFailures(event, expectedServers)) {
|
|
2346
|
+
if (failure.key === WORKSPACE_MCP_KEY) {
|
|
2347
|
+
stopBlindSession(workspaceRefusal(failure));
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
input.log(`MCP connection "${failure.key}" did not load: ${failure.detail}.`);
|
|
2351
|
+
if (!mcpFailed.includes(failure.key)) mcpFailed.push(failure.key);
|
|
2352
|
+
}
|
|
2263
2353
|
}
|
|
2264
2354
|
if (workspaceRequired && !mcpProblem) {
|
|
2265
2355
|
stopBlindSession(mcpWatch.observe(event));
|
|
@@ -2307,7 +2397,8 @@ ${stderrLines.slice(-20).join("\n")}`,
|
|
|
2307
2397
|
`,
|
|
2308
2398
|
usage,
|
|
2309
2399
|
resultText: resultText2,
|
|
2310
|
-
...limit3 ? { limit: limit3 } : {}
|
|
2400
|
+
...limit3 ? { limit: limit3 } : {},
|
|
2401
|
+
...mcpFailed.length ? { mcpFailed } : {}
|
|
2311
2402
|
};
|
|
2312
2403
|
}
|
|
2313
2404
|
var CLAUDE_DRIVER_ID = "claude";
|
|
@@ -3106,7 +3197,7 @@ async function markChatFailed(db, shipId, job, error) {
|
|
|
3106
3197
|
${error.slice(0, 500)}
|
|
3107
3198
|
\`\`\`
|
|
3108
3199
|
|
|
3109
|
-
|
|
3200
|
+
Use Try again above to run it once more.`;
|
|
3110
3201
|
await addDoc(collection6(chatRef, COLLECTIONS.chatMessages), {
|
|
3111
3202
|
author: { type: "agent", id: job.agentId },
|
|
3112
3203
|
content,
|
|
@@ -3116,8 +3207,45 @@ Write again to start a fresh run.`;
|
|
|
3116
3207
|
});
|
|
3117
3208
|
}
|
|
3118
3209
|
var REPLY_SCAN_LIMIT = 20;
|
|
3210
|
+
var NARRATION_MARKERS = [
|
|
3211
|
+
// Our own heading and the harness's own word for the block above it — see `chatStandingRules`.
|
|
3212
|
+
/\bsystem prompt\b/i,
|
|
3213
|
+
/\bstanding rules\b/i,
|
|
3214
|
+
// "Based on my available tools, …"
|
|
3215
|
+
/\bmy (?:available |current )?tools?\b/i,
|
|
3216
|
+
// "Let me check what tools I do have access to."
|
|
3217
|
+
/\btools?\b[^.?!]{0,40}\bI (?:do )?have access to\b/i,
|
|
3218
|
+
// Both orders of "<the toolbox> is <missing>", bounded to one sentence so a paragraph that
|
|
3219
|
+
// merely mentions tools and, later, something being unavailable does not match.
|
|
3220
|
+
/\b(?:mcp|tools?)\b[^.?!]{0,60}\b(?:not available|unavailable|(?:are|is)n'?t available|not connected|not loaded|no access)\b/i,
|
|
3221
|
+
/\b(?:not available|unavailable|no access|(?:do not|don'?t|does not|doesn'?t) have access)\b[^.?!]{0,60}\b(?:mcp|tools?)\b/i
|
|
3222
|
+
];
|
|
3223
|
+
function isSessionNarration(paragraph) {
|
|
3224
|
+
if (paragraph.includes("```")) return false;
|
|
3225
|
+
if (!/\b(?:i|i'm|i'll|i've|my|me)\b/i.test(paragraph)) return false;
|
|
3226
|
+
return NARRATION_MARKERS.some((re) => re.test(paragraph));
|
|
3227
|
+
}
|
|
3228
|
+
function paragraphsWithOffsets(text) {
|
|
3229
|
+
const out = [];
|
|
3230
|
+
const breaks = /\n{2,}/g;
|
|
3231
|
+
let start = 0;
|
|
3232
|
+
let m;
|
|
3233
|
+
while ((m = breaks.exec(text)) !== null) {
|
|
3234
|
+
out.push({ text: text.slice(start, m.index), start });
|
|
3235
|
+
start = m.index + m[0].length;
|
|
3236
|
+
}
|
|
3237
|
+
out.push({ text: text.slice(start), start });
|
|
3238
|
+
return out;
|
|
3239
|
+
}
|
|
3240
|
+
function stripSessionNarration(text) {
|
|
3241
|
+
const paragraphs = paragraphsWithOffsets(text);
|
|
3242
|
+
let dropped = 0;
|
|
3243
|
+
while (dropped < paragraphs.length && isSessionNarration(paragraphs[dropped].text)) dropped += 1;
|
|
3244
|
+
if (dropped === 0 || dropped === paragraphs.length) return text;
|
|
3245
|
+
return text.slice(paragraphs[dropped].start).trim();
|
|
3246
|
+
}
|
|
3119
3247
|
function backstopReplyContent(resultText2) {
|
|
3120
|
-
const text = resultText2.trim();
|
|
3248
|
+
const text = stripSessionNarration(resultText2.trim());
|
|
3121
3249
|
if (!text) {
|
|
3122
3250
|
return "I finished that run without writing a reply. Write again to start a fresh one.";
|
|
3123
3251
|
}
|
|
@@ -4402,6 +4530,7 @@ async function startDaemon() {
|
|
|
4402
4530
|
let engineId = DEFAULT_ENGINE_ID;
|
|
4403
4531
|
let transcript = "";
|
|
4404
4532
|
let resultText2 = "";
|
|
4533
|
+
let mcpConnected = [];
|
|
4405
4534
|
let usage = {
|
|
4406
4535
|
engine: DEFAULT_ENGINE_ID,
|
|
4407
4536
|
inputTokens: 0,
|
|
@@ -4574,6 +4703,8 @@ async function startDaemon() {
|
|
|
4574
4703
|
armLimitTimer();
|
|
4575
4704
|
}
|
|
4576
4705
|
resultText2 = session.resultText;
|
|
4706
|
+
const failed = new Set(session.mcpFailed ?? []);
|
|
4707
|
+
mcpConnected = extraMcpServers.map((s) => s.key).filter((k) => !failed.has(k));
|
|
4577
4708
|
if (!session.ok) failure = session.resultText || "Session failed.";
|
|
4578
4709
|
} catch (e) {
|
|
4579
4710
|
failure = e instanceof Error ? e.message : String(e);
|
|
@@ -4597,7 +4728,7 @@ async function startDaemon() {
|
|
|
4597
4728
|
// mid-thought, so it almost certainly never reached `run_report` — and whatever it had
|
|
4598
4729
|
// got to is what the next run on this task would otherwise have to rediscover.
|
|
4599
4730
|
resultText: resultText2,
|
|
4600
|
-
mcpServers:
|
|
4731
|
+
mcpServers: mcpConnected
|
|
4601
4732
|
});
|
|
4602
4733
|
const by = slot.stop.by;
|
|
4603
4734
|
if (target.kind === "chat") {
|
|
@@ -4647,7 +4778,7 @@ async function startDaemon() {
|
|
|
4647
4778
|
// §15.41. Only used when the session never called `run_report` — the ordinary path is
|
|
4648
4779
|
// that it did, and a real report always wins inside the transaction.
|
|
4649
4780
|
resultText: resultText2,
|
|
4650
|
-
mcpServers:
|
|
4781
|
+
mcpServers: mcpConnected
|
|
4651
4782
|
});
|
|
4652
4783
|
log2(`Job ${job.id} done (${usage.inputTokens}in/${usage.outputTokens}out tokens).`);
|
|
4653
4784
|
let delivery = "agent-replied";
|
|
@@ -4704,7 +4835,7 @@ async function startDaemon() {
|
|
|
4704
4835
|
// is spent — starts from the context pack alone. `error` is the tail for a human; this
|
|
4705
4836
|
// is the continuity for the next session, and they are read by different readers.
|
|
4706
4837
|
resultText: resultText2,
|
|
4707
|
-
mcpServers:
|
|
4838
|
+
mcpServers: mcpConnected
|
|
4708
4839
|
});
|
|
4709
4840
|
if (target.kind === "chat") {
|
|
4710
4841
|
await markChatFailed(sess(shipId).fb.db, shipId, { ...job, chatId: target.chatId }, failure);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|