@lumi.ai/runner 0.5.3 → 0.5.5
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 +145 -24
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
deleteField,
|
|
11
11
|
doc as doc7,
|
|
12
12
|
getDoc as getDoc5,
|
|
13
|
-
getDocs as
|
|
13
|
+
getDocs as getDocs4,
|
|
14
14
|
onSnapshot as onSnapshot2,
|
|
15
|
-
orderBy as
|
|
16
|
-
query as
|
|
15
|
+
orderBy as orderBy4,
|
|
16
|
+
query as query4,
|
|
17
17
|
runTransaction as runTransaction2,
|
|
18
18
|
setDoc as setDoc2,
|
|
19
19
|
updateDoc as updateDoc2,
|
|
20
|
-
where as
|
|
20
|
+
where as where4
|
|
21
21
|
} from "firebase/firestore";
|
|
22
22
|
|
|
23
23
|
// ../shared/dist/engines/claude.js
|
|
@@ -117,6 +117,7 @@ function effectiveAgentTools(agent) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// ../shared/dist/chat.js
|
|
120
|
+
var MAX_CHAT_MESSAGE_CHARS = 8e3;
|
|
120
121
|
var MAX_CHAT_MESSAGES_IN_PROMPT = 40;
|
|
121
122
|
|
|
122
123
|
// ../shared/dist/collections.js
|
|
@@ -474,6 +475,11 @@ var DEFAULT_SHIP_SETTINGS = {
|
|
|
474
475
|
};
|
|
475
476
|
var SHIP_INVITE_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
476
477
|
|
|
478
|
+
// ../shared/dist/taskRelation.js
|
|
479
|
+
function taskBlockedBy(task) {
|
|
480
|
+
return Array.isArray(task?.blockedBy) ? task.blockedBy : [];
|
|
481
|
+
}
|
|
482
|
+
|
|
477
483
|
// ../shared/dist/usage.js
|
|
478
484
|
var EMPTY_USAGE_TOTALS = {
|
|
479
485
|
inputTokens: 0,
|
|
@@ -575,7 +581,7 @@ function mcpUrl(config2) {
|
|
|
575
581
|
}
|
|
576
582
|
|
|
577
583
|
// src/version.ts
|
|
578
|
-
var RUNNER_VERSION = true ? "0.5.
|
|
584
|
+
var RUNNER_VERSION = true ? "0.5.5" : "0.0.0-dev";
|
|
579
585
|
|
|
580
586
|
// src/auth.ts
|
|
581
587
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -958,6 +964,38 @@ async function withFirestoreRetry(read, sleep2 = (ms) => new Promise((r) => setT
|
|
|
958
964
|
// src/jobs/contextPack.ts
|
|
959
965
|
var MAX_ACTIVITY_IN_PROMPT = 40;
|
|
960
966
|
var MAX_PREVIOUS_JOBS_READ = 5;
|
|
967
|
+
var MAX_BLOCKERS_IN_PROMPT = 20;
|
|
968
|
+
async function loadSettledBlockers(shipRef, task) {
|
|
969
|
+
const ids = taskBlockedBy(task).slice(0, MAX_BLOCKERS_IN_PROMPT);
|
|
970
|
+
if (ids.length === 0) return [];
|
|
971
|
+
try {
|
|
972
|
+
return await Promise.all(
|
|
973
|
+
ids.map(async (id) => {
|
|
974
|
+
const ref = doc(shipRef, COLLECTIONS.tasks, id);
|
|
975
|
+
const [snap, resultSnap] = await Promise.all([
|
|
976
|
+
getDoc(ref),
|
|
977
|
+
getDocs(
|
|
978
|
+
query(
|
|
979
|
+
collection(ref, COLLECTIONS.activity),
|
|
980
|
+
where("kind", "==", "result"),
|
|
981
|
+
orderBy("createdAt", "desc"),
|
|
982
|
+
limit(1)
|
|
983
|
+
)
|
|
984
|
+
).catch(() => null)
|
|
985
|
+
]);
|
|
986
|
+
const data = snap.data();
|
|
987
|
+
return {
|
|
988
|
+
id,
|
|
989
|
+
title: data?.title ?? id,
|
|
990
|
+
status: data?.status ?? "unknown",
|
|
991
|
+
result: resultSnap?.docs[0]?.data()?.content ?? null
|
|
992
|
+
};
|
|
993
|
+
})
|
|
994
|
+
);
|
|
995
|
+
} catch {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
}
|
|
961
999
|
async function loadJobContext(db, shipId, job) {
|
|
962
1000
|
const shipRef = doc(db, COLLECTIONS.ships, shipId);
|
|
963
1001
|
const taskRef = doc(shipRef, COLLECTIONS.tasks, job.taskId);
|
|
@@ -1025,11 +1063,13 @@ async function loadJobContext(db, shipId, job) {
|
|
|
1025
1063
|
}
|
|
1026
1064
|
const activityDocs = activitySnap.docs.slice(0, MAX_ACTIVITY_IN_PROMPT);
|
|
1027
1065
|
const agent = { id: agentSnap.id, ...agentSnap.data() };
|
|
1066
|
+
const settledBlockers = job.reason === "unblocked" ? await loadSettledBlockers(shipRef, task) : [];
|
|
1028
1067
|
return {
|
|
1029
1068
|
ship: { id: shipSnap.id, ...shipSnap.data() },
|
|
1030
1069
|
agent,
|
|
1031
1070
|
task,
|
|
1032
1071
|
parentTask,
|
|
1072
|
+
settledBlockers,
|
|
1033
1073
|
workflow,
|
|
1034
1074
|
memory: readMemory(agent.memory),
|
|
1035
1075
|
knowledgeIndex: indexSnap?.exists() ? indexSnap.data() : null,
|
|
@@ -1113,6 +1153,10 @@ function buildPrompt(ctx, reason, mcpServers = []) {
|
|
|
1113
1153
|
parts.push(
|
|
1114
1154
|
"# Why this session started\n\nYou asked a captain for permission and stopped. They have now answered \u2014 their decision is the newest entry in the task activity below. Read it first.\n\n**If they approved it, do that thing now** \u2014 the permission is granted for this task and may be single-use, so do not ask again for the same thing. **If they refused, do not retry and do not look for a way around it**: say what you will do instead, or hand the task back with task_assign."
|
|
1115
1155
|
);
|
|
1156
|
+
} else if (reason === "unblocked") {
|
|
1157
|
+
parts.push(
|
|
1158
|
+
'# Why this session started\n\nThis task was blocked and is not any more: everything it was waiting on is now done. Nobody has just assigned it to you \u2014 you have had it all along, and the work has become startable.\n\n**Read "What you were waiting on" below before you do anything else.** It carries what those tasks produced, which is the input this work was held up for; starting without it means redoing or contradicting somebody else' + (playbook ? ", then follow your playbook below." : "'s work.")
|
|
1159
|
+
);
|
|
1116
1160
|
} else if (reason === "schedule") {
|
|
1117
1161
|
parts.push(
|
|
1118
1162
|
"# Why this session started\n\nThis is a scheduled run: one of your own playbooks created this task on its cron and assigned it to you. It is routine work, not a request from a person, so nobody is waiting on a reply" + (playbook ? " \u2014 the playbook below is the work, and the task's description is only a record of why it exists." : ".")
|
|
@@ -1158,6 +1202,17 @@ ${t.description || "(no description)"}
|
|
|
1158
1202
|
|
|
1159
1203
|
Labels: ${t.labels.join(", ") || "none"} \xB7 Status: ${statusLabel} (\`${t.status}\`)` + dates
|
|
1160
1204
|
);
|
|
1205
|
+
if (ctx.settledBlockers.length > 0) {
|
|
1206
|
+
const blockers = ctx.settledBlockers.map((b) => {
|
|
1207
|
+
const label = statusById(statuses, b.status)?.label ?? b.status;
|
|
1208
|
+
return `## ${b.title} (id: ${b.id}) \u2014 ${label}
|
|
1209
|
+
|
|
1210
|
+
` + (b.result ? b.result : "It left no result note. Open it with task_get if you need to know what it did.");
|
|
1211
|
+
}).join("\n\n");
|
|
1212
|
+
parts.push(`# What you were waiting on
|
|
1213
|
+
|
|
1214
|
+
${blockers}`);
|
|
1215
|
+
}
|
|
1161
1216
|
if (ctx.parentTask) {
|
|
1162
1217
|
const p = ctx.parentTask;
|
|
1163
1218
|
const parentStatus = statusById(statuses, p.status)?.label ?? p.status;
|
|
@@ -1810,8 +1865,13 @@ import {
|
|
|
1810
1865
|
addDoc,
|
|
1811
1866
|
collection as collection4,
|
|
1812
1867
|
doc as doc6,
|
|
1868
|
+
getDocs as getDocs3,
|
|
1869
|
+
limit as fsLimit,
|
|
1870
|
+
orderBy as orderBy3,
|
|
1871
|
+
query as query3,
|
|
1813
1872
|
runTransaction,
|
|
1814
|
-
updateDoc
|
|
1873
|
+
updateDoc,
|
|
1874
|
+
where as where3
|
|
1815
1875
|
} from "firebase/firestore";
|
|
1816
1876
|
import { ref as storageRef, uploadBytes } from "firebase/storage";
|
|
1817
1877
|
function redactTranscript(transcript, knownSecrets) {
|
|
@@ -1937,6 +1997,41 @@ Write again to start a fresh run.`;
|
|
|
1937
1997
|
createdAt: now
|
|
1938
1998
|
});
|
|
1939
1999
|
}
|
|
2000
|
+
var REPLY_SCAN_LIMIT = 20;
|
|
2001
|
+
function backstopReplyContent(resultText) {
|
|
2002
|
+
const text = resultText.trim();
|
|
2003
|
+
if (!text) {
|
|
2004
|
+
return "I finished that run without writing a reply. Write again to start a fresh one.";
|
|
2005
|
+
}
|
|
2006
|
+
if (text.length <= MAX_CHAT_MESSAGE_CHARS) return text;
|
|
2007
|
+
const marker = "\n\n\u2026(truncated)";
|
|
2008
|
+
return `${text.slice(0, MAX_CHAT_MESSAGE_CHARS - marker.length)}${marker}`;
|
|
2009
|
+
}
|
|
2010
|
+
async function ensureChatReply(db, shipId, job, resultText) {
|
|
2011
|
+
const chatRef = doc6(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
|
|
2012
|
+
const messagesCol = collection4(chatRef, COLLECTIONS.chatMessages);
|
|
2013
|
+
const snap = await getDocs3(
|
|
2014
|
+
query3(
|
|
2015
|
+
messagesCol,
|
|
2016
|
+
where3("createdAt", ">=", job.startedAt || 0),
|
|
2017
|
+
orderBy3("createdAt", "desc"),
|
|
2018
|
+
fsLimit(REPLY_SCAN_LIMIT)
|
|
2019
|
+
)
|
|
2020
|
+
);
|
|
2021
|
+
const replied = snap.docs.some((d) => {
|
|
2022
|
+
const author = d.data().author;
|
|
2023
|
+
return author?.type === "agent" && author.id === job.agentId;
|
|
2024
|
+
});
|
|
2025
|
+
if (replied) return "agent-replied";
|
|
2026
|
+
const content = backstopReplyContent(resultText);
|
|
2027
|
+
await addDoc(messagesCol, {
|
|
2028
|
+
author: { type: "agent", id: job.agentId },
|
|
2029
|
+
content,
|
|
2030
|
+
chars: content.length,
|
|
2031
|
+
createdAt: Date.now()
|
|
2032
|
+
});
|
|
2033
|
+
return resultText.trim() ? "posted-final-text" : "posted-silence-note";
|
|
2034
|
+
}
|
|
1940
2035
|
|
|
1941
2036
|
// src/daemon.ts
|
|
1942
2037
|
var HEARTBEAT_MS = 3e4;
|
|
@@ -2056,11 +2151,11 @@ async function startDaemon() {
|
|
|
2056
2151
|
needsRefill.delete(shipId);
|
|
2057
2152
|
if (!serving.has(shipId) || approved.get(shipId) !== true) continue;
|
|
2058
2153
|
try {
|
|
2059
|
-
const snap = await
|
|
2060
|
-
|
|
2154
|
+
const snap = await getDocs4(
|
|
2155
|
+
query4(
|
|
2061
2156
|
collection5(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
2062
|
-
|
|
2063
|
-
|
|
2157
|
+
where4("status", "==", "queued"),
|
|
2158
|
+
orderBy4("createdAt", "asc")
|
|
2064
2159
|
)
|
|
2065
2160
|
);
|
|
2066
2161
|
for (const d of snap.docs) {
|
|
@@ -2088,10 +2183,10 @@ async function startDaemon() {
|
|
|
2088
2183
|
console.error(`${what} listener error (${shipId}):`, e.message);
|
|
2089
2184
|
};
|
|
2090
2185
|
for (const shipId of serving) {
|
|
2091
|
-
const q =
|
|
2186
|
+
const q = query4(
|
|
2092
2187
|
collection5(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
|
|
2093
|
-
|
|
2094
|
-
|
|
2188
|
+
where4("status", "==", "queued"),
|
|
2189
|
+
orderBy4("createdAt", "asc")
|
|
2095
2190
|
);
|
|
2096
2191
|
unsubsByShip.set(shipId, [
|
|
2097
2192
|
onSnapshot2(
|
|
@@ -2342,6 +2437,7 @@ async function startDaemon() {
|
|
|
2342
2437
|
let sessionLimit = null;
|
|
2343
2438
|
let engineId = DEFAULT_ENGINE_ID;
|
|
2344
2439
|
let transcript = "";
|
|
2440
|
+
let resultText = "";
|
|
2345
2441
|
let usage = {
|
|
2346
2442
|
engine: DEFAULT_ENGINE_ID,
|
|
2347
2443
|
inputTokens: 0,
|
|
@@ -2442,6 +2538,7 @@ async function startDaemon() {
|
|
|
2442
2538
|
});
|
|
2443
2539
|
armLimitTimer();
|
|
2444
2540
|
}
|
|
2541
|
+
resultText = session.resultText;
|
|
2445
2542
|
if (!session.ok) failure = session.resultText || "Session failed.";
|
|
2446
2543
|
} catch (e) {
|
|
2447
2544
|
failure = e instanceof Error ? e.message : String(e);
|
|
@@ -2487,9 +2584,33 @@ async function startDaemon() {
|
|
|
2487
2584
|
mcpServers: extraMcpServers.map((s) => s.key)
|
|
2488
2585
|
});
|
|
2489
2586
|
log2(`Job ${job.id} done (${usage.inputTokens}in/${usage.outputTokens}out tokens).`);
|
|
2587
|
+
let delivery = "agent-replied";
|
|
2588
|
+
if (target.kind === "chat") {
|
|
2589
|
+
try {
|
|
2590
|
+
delivery = await ensureChatReply(
|
|
2591
|
+
sess(shipId).fb.db,
|
|
2592
|
+
shipId,
|
|
2593
|
+
{ ...job, chatId: target.chatId },
|
|
2594
|
+
resultText
|
|
2595
|
+
);
|
|
2596
|
+
if (delivery === "posted-final-text") {
|
|
2597
|
+
log2(
|
|
2598
|
+
`Job ${job.id}: the agent never called chat_send \u2014 posted its final text to chat ${target.chatId} instead.`
|
|
2599
|
+
);
|
|
2600
|
+
} else if (delivery === "posted-silence-note") {
|
|
2601
|
+
log2(
|
|
2602
|
+
`Job ${job.id}: the agent neither called chat_send nor produced any text \u2014 posted a note to chat ${target.chatId} so the thread is not silent.`
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
} catch (e) {
|
|
2606
|
+
log2(
|
|
2607
|
+
`Job ${job.id}: could not check whether the agent replied in chat ${target.chatId} \u2014 ${e instanceof Error ? e.message : e}`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2490
2611
|
notify(
|
|
2491
2612
|
"Crew job finished",
|
|
2492
|
-
target.kind === "task" ? `Task ${target.taskId} is done.` : "Agent replied in a chat."
|
|
2613
|
+
target.kind === "task" ? `Task ${target.taskId} is done.` : delivery === "agent-replied" ? "Agent replied in a chat." : "Agent finished a chat run without replying \u2014 its answer was posted for it."
|
|
2493
2614
|
);
|
|
2494
2615
|
} else if (transient) {
|
|
2495
2616
|
await releaseJob(
|
|
@@ -2830,7 +2951,7 @@ function setParallel(config2, value, ship2) {
|
|
|
2830
2951
|
|
|
2831
2952
|
// src/cli/commands/doctor.ts
|
|
2832
2953
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2833
|
-
import { collection as collection6, doc as doc8, getDoc as getDoc6, getDocs as
|
|
2954
|
+
import { collection as collection6, doc as doc8, getDoc as getDoc6, getDocs as getDocs5 } from "firebase/firestore";
|
|
2834
2955
|
|
|
2835
2956
|
// src/service.ts
|
|
2836
2957
|
import { spawnSync } from "node:child_process";
|
|
@@ -3045,10 +3166,10 @@ function serviceStatus() {
|
|
|
3045
3166
|
};
|
|
3046
3167
|
}
|
|
3047
3168
|
if (process.platform === "win32") {
|
|
3048
|
-
const
|
|
3049
|
-
if (!
|
|
3169
|
+
const query6 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
|
|
3170
|
+
if (!query6.ok) return { state: "not-installed", detail: "No scheduled task installed." };
|
|
3050
3171
|
return {
|
|
3051
|
-
state: /\bRunning\b/i.test(
|
|
3172
|
+
state: /\bRunning\b/i.test(query6.out) ? "running" : "installed",
|
|
3052
3173
|
detail: "Scheduled task installed (runs at logon).",
|
|
3053
3174
|
unitPath: WINDOWS_TASK
|
|
3054
3175
|
};
|
|
@@ -3285,7 +3406,7 @@ async function checkShips(config2) {
|
|
|
3285
3406
|
}
|
|
3286
3407
|
let agents = [];
|
|
3287
3408
|
try {
|
|
3288
|
-
const snap = await
|
|
3409
|
+
const snap = await getDocs5(collection6(fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
|
|
3289
3410
|
agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
3290
3411
|
} catch {
|
|
3291
3412
|
}
|
|
@@ -3810,9 +3931,9 @@ import {
|
|
|
3810
3931
|
doc as doc10,
|
|
3811
3932
|
getCountFromServer as getCountFromServer2,
|
|
3812
3933
|
getDoc as getDoc8,
|
|
3813
|
-
getDocs as
|
|
3814
|
-
query as
|
|
3815
|
-
where as
|
|
3934
|
+
getDocs as getDocs6,
|
|
3935
|
+
query as query5,
|
|
3936
|
+
where as where5
|
|
3816
3937
|
} from "firebase/firestore";
|
|
3817
3938
|
async function runStatus() {
|
|
3818
3939
|
const config2 = loadConfig();
|
|
@@ -3852,7 +3973,7 @@ async function runStatus() {
|
|
|
3852
3973
|
let queued = 0;
|
|
3853
3974
|
try {
|
|
3854
3975
|
const counted = await getCountFromServer2(
|
|
3855
|
-
|
|
3976
|
+
query5(collection7(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
|
|
3856
3977
|
);
|
|
3857
3978
|
queued = counted.data().count;
|
|
3858
3979
|
} catch {
|
|
@@ -3861,7 +3982,7 @@ async function runStatus() {
|
|
|
3861
3982
|
const usageSnap = await getDoc8(doc10(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
|
|
3862
3983
|
const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
3863
3984
|
const now = Date.now();
|
|
3864
|
-
const limitsSnap = await
|
|
3985
|
+
const limitsSnap = await getDocs6(collection7(shipRef, COLLECTIONS.engineLimits));
|
|
3865
3986
|
const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
|
|
3866
3987
|
ships.push({
|
|
3867
3988
|
shipId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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.",
|