@lumi.ai/runner 0.6.3 → 0.6.4
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 +91 -26
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -230,6 +230,8 @@ function jobTarget(job) {
|
|
|
230
230
|
return { kind: "chat", chatId: job.chatId };
|
|
231
231
|
return null;
|
|
232
232
|
}
|
|
233
|
+
var MAX_RUN_REPORT_CHARS = 6e3;
|
|
234
|
+
var MAX_RUN_REPORT_SUMMARY_CHARS = 300;
|
|
233
235
|
|
|
234
236
|
// ../shared/dist/jobProgress.js
|
|
235
237
|
var MAX_JOB_STEPS = 10;
|
|
@@ -772,7 +774,7 @@ function mcpUrl(config2) {
|
|
|
772
774
|
}
|
|
773
775
|
|
|
774
776
|
// src/version.ts
|
|
775
|
-
var RUNNER_VERSION = true ? "0.6.
|
|
777
|
+
var RUNNER_VERSION = true ? "0.6.4" : "0.0.0-dev";
|
|
776
778
|
|
|
777
779
|
// src/auth.ts
|
|
778
780
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -1196,6 +1198,13 @@ async function loadSettledBlockers(shipRef, task) {
|
|
|
1196
1198
|
return [];
|
|
1197
1199
|
}
|
|
1198
1200
|
}
|
|
1201
|
+
function reportSummaryLine(r) {
|
|
1202
|
+
if (r.summary?.trim()) return r.summary.trim();
|
|
1203
|
+
const body = r.report?.trim();
|
|
1204
|
+
if (!body) return "No report.";
|
|
1205
|
+
const firstLine2 = body.split("\n").find((l) => l.trim())?.trim() ?? body;
|
|
1206
|
+
return firstLine2.length <= MAX_RUN_REPORT_SUMMARY_CHARS ? firstLine2 : `${firstLine2.slice(0, MAX_RUN_REPORT_SUMMARY_CHARS - 1)}\u2026`;
|
|
1207
|
+
}
|
|
1199
1208
|
async function loadJobContext(db, shipId, job) {
|
|
1200
1209
|
const shipRef = doc(db, COLLECTIONS.ships, shipId);
|
|
1201
1210
|
const taskRef = doc(shipRef, COLLECTIONS.tasks, job.taskId);
|
|
@@ -1276,7 +1285,13 @@ async function loadJobContext(db, shipId, job) {
|
|
|
1276
1285
|
activity: activityDocs.map((d) => ({ id: d.id, ...d.data() })).reverse(),
|
|
1277
1286
|
// read newest-first, rendered chronologically
|
|
1278
1287
|
activityTruncated: activitySnap.docs.length > MAX_ACTIVITY_IN_PROMPT,
|
|
1279
|
-
|
|
1288
|
+
// §15.41. No longer filtered down to jobs that HAVE a report: the catalog carries a line per
|
|
1289
|
+
// previous run, and "that run left no report" is itself worth knowing — it is the difference
|
|
1290
|
+
// between a task nobody has worked and one where three runs died without saying why.
|
|
1291
|
+
previousReports: jobsSnap.docs.filter((d) => d.id !== job.id).slice(0, MAX_PREVIOUS_JOBS_READ).map((d) => {
|
|
1292
|
+
const data = d.data();
|
|
1293
|
+
return { jobId: d.id, summary: data.reportSummary ?? null, report: data.report ?? null };
|
|
1294
|
+
}).reverse()
|
|
1280
1295
|
// chronological
|
|
1281
1296
|
};
|
|
1282
1297
|
}
|
|
@@ -1301,7 +1316,11 @@ function standingRules(ship2, statuses, task, agent) {
|
|
|
1301
1316
|
// Amended with the pointer clause, which is the only thing keeping the three tiers from
|
|
1302
1317
|
// duplicating each other: without it a diligent agent writes the same fact into its report,
|
|
1303
1318
|
// its notebook and the org brain, and the report grows into a transcript of the other two.
|
|
1304
|
-
|
|
1319
|
+
// §15.41 added the summary clause. It is second, not an afterthought at the end, because that
|
|
1320
|
+
// one string is what every LATER run on this task reads instead of this report — and what a
|
|
1321
|
+
// human sees in their notification. An agent that treats it as a label writes "Run report",
|
|
1322
|
+
// and four runs later the catalog says nothing at all.
|
|
1323
|
+
"End every session by calling run_report: what you did, what remains, what the next run must know. Give it a summary too \u2014 one or two sentences that stand in for the whole report in later runs, so write the sentence you would want the next run to read first. Do not repeat what you already put in your memory or in Ship knowledge \u2014 point at it instead (the entry id, or the slug)."
|
|
1305
1324
|
].join("\n- ");
|
|
1306
1325
|
}
|
|
1307
1326
|
function formatTaskDate(millis, now = Date.now()) {
|
|
@@ -1341,26 +1360,17 @@ function buildPrompt(ctx, reason, mcpServers = [], tokenRepos, githubUnavailable
|
|
|
1341
1360
|
const parts = [];
|
|
1342
1361
|
const statuses = shipTaskStatuses(ctx.ship);
|
|
1343
1362
|
const playbook = usableWorkflow(ctx.workflow);
|
|
1363
|
+
let preamble = null;
|
|
1344
1364
|
if (reason === "activity") {
|
|
1345
|
-
|
|
1346
|
-
"# Why this session started\n\nSomeone posted on your task after your last run. Read the newest activity below" + (playbook ? ", then follow your playbook below." : ", respond via task_comment, do any follow-up work it asks for, and set the task status honestly if it needs to change.")
|
|
1347
|
-
);
|
|
1365
|
+
preamble = "# Why this session started\n\nSomeone posted on your task after your last run. Read the newest activity below" + (playbook ? ", then follow your playbook above." : ", respond via task_comment, do any follow-up work it asks for, and set the task status honestly if it needs to change.");
|
|
1348
1366
|
} else if (reason === "watch") {
|
|
1349
|
-
|
|
1350
|
-
"# Why this session started\n\nOne of your playbooks watches the board, and this task matched it. Nobody assigned it to you and nobody else is working it right now \u2014 you are picking it up.\n\n**Leave it in a status that says what happened.** The board is how the watch decides what still needs attention, so a task you leave in the same status will come back to you on the next scan" + (playbook ? ", which your playbook below is written for." : ".")
|
|
1351
|
-
);
|
|
1367
|
+
preamble = "# Why this session started\n\nOne of your playbooks watches the board, and this task matched it. Nobody assigned it to you and nobody else is working it right now \u2014 you are picking it up.\n\n**Leave it in a status that says what happened.** The board is how the watch decides what still needs attention, so a task you leave in the same status will come back to you on the next scan" + (playbook ? ", which your playbook above is written for." : ".");
|
|
1352
1368
|
} else if (reason === "resume_after_approval") {
|
|
1353
|
-
|
|
1354
|
-
"# 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."
|
|
1355
|
-
);
|
|
1369
|
+
preamble = "# 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.";
|
|
1356
1370
|
} else if (reason === "unblocked") {
|
|
1357
|
-
|
|
1358
|
-
'# 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.")
|
|
1359
|
-
);
|
|
1371
|
+
preamble = '# 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 above." : "'s work.");
|
|
1360
1372
|
} else if (reason === "schedule") {
|
|
1361
|
-
|
|
1362
|
-
"# 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." : ".")
|
|
1363
|
-
);
|
|
1373
|
+
preamble = "# 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 above is the work, and the task's description is only a record of why it exists." : ".");
|
|
1364
1374
|
}
|
|
1365
1375
|
parts.push(`# Who you are
|
|
1366
1376
|
|
|
@@ -1385,6 +1395,7 @@ This is the playbook for this kind of work \u2014 follow it. Your contract and t
|
|
|
1385
1395
|
${playbook.instructions}`
|
|
1386
1396
|
);
|
|
1387
1397
|
}
|
|
1398
|
+
if (preamble) parts.push(preamble);
|
|
1388
1399
|
const t = ctx.task;
|
|
1389
1400
|
const now = Date.now();
|
|
1390
1401
|
const statusLabel = statusById(statuses, t.status)?.label ?? t.status;
|
|
@@ -1436,12 +1447,28 @@ ${p.description || "(no description)"}`
|
|
|
1436
1447
|
${note2}${feed}`);
|
|
1437
1448
|
}
|
|
1438
1449
|
if (ctx.previousReports.length > 0) {
|
|
1439
|
-
const
|
|
1450
|
+
const runs = ctx.previousReports;
|
|
1451
|
+
const latest = runs[runs.length - 1];
|
|
1452
|
+
const older = runs.slice(0, -1);
|
|
1453
|
+
const sections = [];
|
|
1454
|
+
if (older.length > 0) {
|
|
1455
|
+
const lines = older.map((r) => `- (\`${r.jobId}\`) ${reportSummaryLine(r)}`).join("\n");
|
|
1456
|
+
sections.push(
|
|
1457
|
+
`## Earlier runs
|
|
1440
1458
|
|
|
1441
|
-
|
|
1459
|
+
Oldest first, one line each. To read one in full, call \`task_get\` with this task's id and \`reportJobId\` set to the id in brackets.
|
|
1460
|
+
|
|
1461
|
+
${lines}`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
sections.push(
|
|
1465
|
+
`## Most recent run (\`${latest.jobId}\`)
|
|
1466
|
+
|
|
1467
|
+
${latest.report ?? "That run left no report."}`
|
|
1468
|
+
);
|
|
1442
1469
|
parts.push(`# Reports from previous runs on this task
|
|
1443
1470
|
|
|
1444
|
-
${
|
|
1471
|
+
${sections.join("\n\n")}`);
|
|
1445
1472
|
}
|
|
1446
1473
|
const githubRepos = tokenRepos ?? ctx.agent.tools.github.repos;
|
|
1447
1474
|
if (ctx.agent.tools.github.enabled && githubUnavailable) {
|
|
@@ -1517,7 +1544,10 @@ async function loadChatContext(db, shipId, job) {
|
|
|
1517
1544
|
limit2(MAX_CHAT_REPORTS + 1)
|
|
1518
1545
|
)
|
|
1519
1546
|
);
|
|
1520
|
-
previousReports = jobsSnap.docs.filter((d) => d.id !== job.id).slice(0, MAX_CHAT_REPORTS).map((d) =>
|
|
1547
|
+
previousReports = jobsSnap.docs.filter((d) => d.id !== job.id).slice(0, MAX_CHAT_REPORTS).map((d) => {
|
|
1548
|
+
const data = d.data();
|
|
1549
|
+
return { jobId: d.id, summary: data.reportSummary ?? null, report: data.report ?? null };
|
|
1550
|
+
}).reverse();
|
|
1521
1551
|
} catch {
|
|
1522
1552
|
previousReports = [];
|
|
1523
1553
|
}
|
|
@@ -1553,7 +1583,7 @@ function chatStandingRules(ship2, agent) {
|
|
|
1553
1583
|
scopeRule,
|
|
1554
1584
|
registerRule,
|
|
1555
1585
|
routingRule,
|
|
1556
|
-
"End the session by calling run_report: what was discussed and anything the next run must know. Do not repeat what you already put in your memory or in Ship knowledge \u2014 point at it instead."
|
|
1586
|
+
"End the session by calling run_report: what was discussed and anything the next run must know. Give it a summary too \u2014 one or two sentences that stand in for the whole report once this conversation is long enough to scroll out of the window. Do not repeat what you already put in your memory or in Ship knowledge \u2014 point at it instead."
|
|
1557
1587
|
].join("\n- ");
|
|
1558
1588
|
}
|
|
1559
1589
|
function buildChatPrompt(ctx, mcpServers = [], tokenRepos, githubUnavailable) {
|
|
@@ -1576,15 +1606,22 @@ ${ctx.agent.contract}`);
|
|
|
1576
1606
|
|
|
1577
1607
|
- ${chatStandingRules(ctx.ship, ctx.agent)}`);
|
|
1578
1608
|
if (ctx.previousReports.length > 0) {
|
|
1579
|
-
const
|
|
1609
|
+
const runs = ctx.previousReports;
|
|
1610
|
+
const latest = runs[runs.length - 1];
|
|
1611
|
+
const older = runs.slice(0, -1);
|
|
1612
|
+
const sections = [];
|
|
1613
|
+
if (older.length > 0) {
|
|
1614
|
+
sections.push(older.map((r) => `- ${reportSummaryLine(r)}`).join("\n"));
|
|
1615
|
+
}
|
|
1616
|
+
sections.push(`## Most recent run
|
|
1580
1617
|
|
|
1581
|
-
${
|
|
1618
|
+
${latest.report ?? "That run left no report."}`);
|
|
1582
1619
|
parts.push(
|
|
1583
1620
|
`# Earlier in this chat
|
|
1584
1621
|
|
|
1585
1622
|
The conversation is longer than the window below. These are your own notes from previous runs on this chat, oldest first.
|
|
1586
1623
|
|
|
1587
|
-
${
|
|
1624
|
+
${sections.join("\n\n")}`
|
|
1588
1625
|
);
|
|
1589
1626
|
}
|
|
1590
1627
|
const title = ctx.chat.title?.trim();
|
|
@@ -2445,6 +2482,19 @@ async function uploadTranscript(storage, shipId, jobId, redacted) {
|
|
|
2445
2482
|
function utcDay(millis) {
|
|
2446
2483
|
return new Date(millis).toISOString().slice(0, 10);
|
|
2447
2484
|
}
|
|
2485
|
+
function backstopReportContent(resultText) {
|
|
2486
|
+
const text = resultText.trim();
|
|
2487
|
+
if (!text) {
|
|
2488
|
+
const none = "This run ended without writing a report, and left no final text to fall back on.";
|
|
2489
|
+
return { report: none, summary: none };
|
|
2490
|
+
}
|
|
2491
|
+
const marker = "\n\n\u2026(truncated)";
|
|
2492
|
+
const report4 = text.length <= MAX_RUN_REPORT_CHARS ? text : `${text.slice(0, MAX_RUN_REPORT_CHARS - marker.length)}${marker}`;
|
|
2493
|
+
const firstLine2 = text.split("\n").find((l) => l.trim())?.trim() ?? text;
|
|
2494
|
+
const ellipsis = "\u2026";
|
|
2495
|
+
const summary = firstLine2.length <= MAX_RUN_REPORT_SUMMARY_CHARS ? firstLine2 : `${firstLine2.slice(0, MAX_RUN_REPORT_SUMMARY_CHARS - ellipsis.length)}${ellipsis}`;
|
|
2496
|
+
return { report: report4, summary };
|
|
2497
|
+
}
|
|
2448
2498
|
async function finalizeJob(db, shipId, job, input) {
|
|
2449
2499
|
const shipRef = doc6(db, COLLECTIONS.ships, shipId);
|
|
2450
2500
|
const jobRef = doc6(shipRef, COLLECTIONS.jobs, job.id);
|
|
@@ -2452,6 +2502,7 @@ async function finalizeJob(db, shipId, job, input) {
|
|
|
2452
2502
|
const usageRef = doc6(shipRef, COLLECTIONS.usageDaily, utcDay(now));
|
|
2453
2503
|
await runTransaction(db, async (tx) => {
|
|
2454
2504
|
const usageSnap = await tx.get(usageRef);
|
|
2505
|
+
const jobSnap = await tx.get(jobRef);
|
|
2455
2506
|
const totals = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
|
|
2456
2507
|
const byAgent = usageSnap.data()?.byAgent ?? {};
|
|
2457
2508
|
const agentAgg = {
|
|
@@ -2459,7 +2510,10 @@ async function finalizeJob(db, shipId, job, input) {
|
|
|
2459
2510
|
...byAgent[job.agentId] ?? {}
|
|
2460
2511
|
};
|
|
2461
2512
|
const u = input.usage;
|
|
2513
|
+
const wroteReport = !!jobSnap.data()?.report;
|
|
2514
|
+
const backstop = !wroteReport && input.resultText !== void 0 ? backstopReportContent(input.resultText) : null;
|
|
2462
2515
|
tx.update(jobRef, {
|
|
2516
|
+
...backstop ? { report: backstop.report, reportSummary: backstop.summary } : {},
|
|
2463
2517
|
status: input.status,
|
|
2464
2518
|
endedAt: now,
|
|
2465
2519
|
usage: u,
|
|
@@ -4036,6 +4090,10 @@ async function startDaemon() {
|
|
|
4036
4090
|
status: "stopped",
|
|
4037
4091
|
usage,
|
|
4038
4092
|
transcriptPath,
|
|
4093
|
+
// §15.41. A stopped run is the case that needs the backstop MOST: it was cut off
|
|
4094
|
+
// mid-thought, so it almost certainly never reached `run_report` — and whatever it had
|
|
4095
|
+
// got to is what the next run on this task would otherwise have to rediscover.
|
|
4096
|
+
resultText,
|
|
4039
4097
|
mcpServers: extraMcpServers.map((s) => s.key)
|
|
4040
4098
|
});
|
|
4041
4099
|
const by = slot.stop.by;
|
|
@@ -4077,6 +4135,9 @@ async function startDaemon() {
|
|
|
4077
4135
|
status: "done",
|
|
4078
4136
|
usage,
|
|
4079
4137
|
transcriptPath,
|
|
4138
|
+
// §15.41. Only used when the session never called `run_report` — the ordinary path is
|
|
4139
|
+
// that it did, and a real report always wins inside the transaction.
|
|
4140
|
+
resultText,
|
|
4080
4141
|
mcpServers: extraMcpServers.map((s) => s.key)
|
|
4081
4142
|
});
|
|
4082
4143
|
log2(`Job ${job.id} done (${usage.inputTokens}in/${usage.outputTokens}out tokens).`);
|
|
@@ -4130,6 +4191,10 @@ async function startDaemon() {
|
|
|
4130
4191
|
usage,
|
|
4131
4192
|
transcriptPath,
|
|
4132
4193
|
error: failure,
|
|
4194
|
+
// §15.41. A failed run still did work, and the retry — or the next run after the retry
|
|
4195
|
+
// is spent — starts from the context pack alone. `error` is the tail for a human; this
|
|
4196
|
+
// is the continuity for the next session, and they are read by different readers.
|
|
4197
|
+
resultText,
|
|
4133
4198
|
mcpServers: extraMcpServers.map((s) => s.key)
|
|
4134
4199
|
});
|
|
4135
4200
|
if (target.kind === "chat") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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.",
|