@a-t-h-i/bot-lobby 0.6.32 → 0.6.34
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/README.md +30 -0
- package/package.json +7 -7
- package/src/desk/client-extension.ts +2 -2
- package/src/desk/ipc.ts +4 -3
- package/src/desk/session.ts +14 -1
- package/src/execution/agent-runner.ts +16 -92
- package/src/execution/delegation.ts +193 -0
- package/src/execution/live-run.ts +95 -0
- package/src/execution/pi-runner.ts +35 -9
- package/src/index.ts +5 -0
- package/src/lobby/planner.ts +9 -0
- package/src/lobby/pr-review.ts +9 -0
- package/src/lobby/quickfix.ts +16 -0
- package/src/master/master.ts +3 -1
- package/src/master/research.ts +1 -0
- package/src/pi/auto-compaction.ts +72 -0
- package/src/pi/delegation.ts +68 -0
- package/src/pi/model-support.ts +1 -1
- package/src/pi/plan-checklist.ts +18 -7
- package/src/pi/run-summary.ts +9 -0
- package/src/schemas/configuration.ts +19 -7
- package/src/schemas/findings.ts +4 -0
- package/src/schemas/task.ts +4 -0
- package/src/state/work-time.ts +4 -2
- package/src/webui/api/index.ts +2 -2
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/metrics.ts +41 -8
- package/src/webui/api/plan-facts.ts +4 -3
- package/src/webui/api/tasks.ts +1 -1
- package/src/webui/dev/fake-service.ts +1 -1
- package/src/webui/dev/fixtures.ts +1 -1
- package/src/webui/protocol.ts +4 -3
- package/src/workflow/workflow.ts +4 -1
- package/webui/dist/assets/index-B4zpEv7K.js +109 -0
- package/webui/dist/assets/index-C8hgfw6m.css +2 -0
- package/webui/dist/build.json +39 -27
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BtKUZnAE.css +0 -2
- package/webui/dist/assets/index-CmNcHve-.js +0 -108
package/src/schemas/task.ts
CHANGED
|
@@ -77,6 +77,10 @@ export const MAX_WORKER_RECORDS = 64;
|
|
|
77
77
|
/** One finished subagent run of any role, kept for `/bot-lobby runs`. */
|
|
78
78
|
export interface RunLogEntry {
|
|
79
79
|
runId: string;
|
|
80
|
+
parentRunId?: string;
|
|
81
|
+
depth?: number;
|
|
82
|
+
instruction?: string;
|
|
83
|
+
stepInstruction?: string;
|
|
80
84
|
domain: Domain;
|
|
81
85
|
role: Role;
|
|
82
86
|
status: "running" | "success" | "failed" | "cancelled" | "timeout";
|
package/src/state/work-time.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { taskDirFor } from "./persistence.ts";
|
|
|
15
15
|
/** A worker run under way: the step it works on is found from its instruction. */
|
|
16
16
|
export interface ActiveRun {
|
|
17
17
|
runId: string;
|
|
18
|
+
parentRunId?: string;
|
|
19
|
+
stepInstruction?: string;
|
|
18
20
|
instruction: string;
|
|
19
21
|
startedAt: string;
|
|
20
22
|
}
|
|
@@ -97,7 +99,7 @@ export function taskWork(root: string, configDir: string, taskId: string, now =
|
|
|
97
99
|
const noted = new Map<string, Set<string>>();
|
|
98
100
|
|
|
99
101
|
/** A worker run of the task started or ended: the steps it works on read as active while it runs. */
|
|
100
|
-
export function noteWorkerRun(root: string, configDir: string, taskId: string, run: { runId: string; instruction?: string; startedAt: string; status: string }): void {
|
|
102
|
+
export function noteWorkerRun(root: string, configDir: string, taskId: string, run: { runId: string; parentRunId?: string; stepInstruction?: string; instruction?: string; startedAt: string; status: string }): void {
|
|
101
103
|
const running = run.status === "running";
|
|
102
104
|
const ids = noted.get(taskId) ?? new Set<string>();
|
|
103
105
|
if (running === ids.has(run.runId)) return;
|
|
@@ -106,7 +108,7 @@ export function noteWorkerRun(root: string, configDir: string, taskId: string, r
|
|
|
106
108
|
noted.set(taskId, ids);
|
|
107
109
|
updateWork(root, configDir, taskId, (work) => {
|
|
108
110
|
work.active = work.active.filter((entry) => entry.runId !== run.runId);
|
|
109
|
-
if (running) work.active.push({ runId: run.runId, instruction: run.instruction ?? "", startedAt: run.startedAt });
|
|
111
|
+
if (running) work.active.push({ runId: run.runId, instruction: run.instruction ?? "", startedAt: run.startedAt, ...(run.parentRunId ? { parentRunId: run.parentRunId } : {}), ...(run.stepInstruction ? { stepInstruction: run.stepInstruction } : {}) });
|
|
110
112
|
});
|
|
111
113
|
}
|
|
112
114
|
|
package/src/webui/api/index.ts
CHANGED
|
@@ -271,8 +271,8 @@ function buildRoutes(): Record<string, Route> {
|
|
|
271
271
|
run: (body, ctx) => quickfixMovedToTask(body as { id: string }, ctx),
|
|
272
272
|
};
|
|
273
273
|
routes["metrics.get"] = {
|
|
274
|
-
schema: Type.Object({ groupBy: Type.Union([Type.Literal("model"), Type.Literal("model-kind")]), query: Type.Optional(Type.String({ maxLength: 500 })) }, { additionalProperties: false }),
|
|
275
|
-
run: (body, ctx) => metricsGet(body as
|
|
274
|
+
schema: Type.Object({ groupBy: Type.Union([Type.Literal("model"), Type.Literal("model-kind")]), query: Type.Optional(Type.String({ maxLength: 500 })), from: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), to: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$" })), timeZone: Type.Optional(Type.String({ maxLength: 100 })) }, { additionalProperties: false }),
|
|
275
|
+
run: (body, ctx) => metricsGet(body as Parameters<typeof metricsGet>[0], ctx),
|
|
276
276
|
};
|
|
277
277
|
routes["knowledge.files"] = { schema: Empty, run: (_body, ctx) => knowledgeFiles(ctx) };
|
|
278
278
|
routes["knowledge.ask"] = {
|
package/src/webui/api/lobby.ts
CHANGED
|
@@ -24,7 +24,7 @@ function planFacts(task: Task, work: WorkProjection | undefined): Pick<SnapshotT
|
|
|
24
24
|
return {
|
|
25
25
|
progress: { done: steps.filter((step) => step.status === "done").length, total: steps.length },
|
|
26
26
|
...(current ? { currentStep: current.text } : {}),
|
|
27
|
-
...(active.length > 0 ? { activeSteps: active.map((step) => ({ text: step.text, workedMs: step.workedMs })) } : {}),
|
|
27
|
+
...(active.length > 0 ? { activeSteps: active.map((step) => ({ text: step.text, workedMs: step.workedMs, activeAgentCount: step.activeAgentCount })) } : {}),
|
|
28
28
|
...clock,
|
|
29
29
|
};
|
|
30
30
|
}
|
package/src/webui/api/metrics.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* time goes, and what the classifier did — the same figures the terminal
|
|
4
4
|
* tab draws, as data. Search narrows runs exactly like the tab's `/`.
|
|
5
5
|
*/
|
|
6
|
-
import { aggregateMetrics, collectMetrics, sortGroups, summarizeClassifier, taskStats, taskTimesByModel, type
|
|
6
|
+
import { aggregateMetrics, collectMetrics, sortGroups, summarizeClassifier, taskStats, taskTimesByModel, type MetricRecord } from "../../state/metrics.ts";
|
|
7
7
|
import { filterRecords } from "../../lobby/models/metrics.ts";
|
|
8
|
-
import type { MetricsData } from "../protocol.ts";
|
|
9
|
-
import type
|
|
8
|
+
import type { Api, MetricsData } from "../protocol.ts";
|
|
9
|
+
import { fail, type ApiContext } from "./index.ts";
|
|
10
10
|
|
|
11
11
|
function timed(records: readonly MetricRecord[]): MetricRecord[] {
|
|
12
12
|
return records.filter((record) => record.status !== "cancelled" && record.durationMs > 0);
|
|
@@ -49,15 +49,48 @@ function shareOf(records: readonly MetricRecord[]): MetricsData["timeShare"]["by
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** The dashboard figures for one grouping and search. */
|
|
52
|
-
export function metricsGet(body:
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
export function metricsGet(body: Api["metrics.get"]["request"], ctx: ApiContext): MetricsData {
|
|
53
|
+
const { from, to } = body;
|
|
54
|
+
const validDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
55
|
+
if ((from === undefined) !== (to === undefined) || (from !== undefined && (!validDate(from) || !validDate(to!) || from > to!))) {
|
|
56
|
+
fail(400, "bad_request", "Choose a valid start and end date, with the start on or before the end.");
|
|
57
|
+
}
|
|
58
|
+
let formatter: Intl.DateTimeFormat;
|
|
59
|
+
try {
|
|
60
|
+
formatter = new Intl.DateTimeFormat("en", { timeZone: body.timeZone ?? "UTC", year: "numeric", month: "2-digit", day: "2-digit" });
|
|
61
|
+
} catch {
|
|
62
|
+
fail(400, "bad_request", "Choose a valid time zone.");
|
|
63
|
+
}
|
|
64
|
+
const dayOf = (timestamp: string) => {
|
|
65
|
+
const date = new Date(timestamp);
|
|
66
|
+
if (!Number.isFinite(date.getTime())) return "";
|
|
67
|
+
const parts = formatter.formatToParts(date);
|
|
68
|
+
return ["year", "month", "day"].map((part) => parts.find((item) => item.type === part)!.value).join("-");
|
|
69
|
+
};
|
|
70
|
+
const inRange = (timestamp: string) => !from || (dayOf(timestamp) >= from && dayOf(timestamp) <= to!);
|
|
71
|
+
const allTasks = ctx.service.tasks();
|
|
72
|
+
const tasks = allTasks.filter((task) => inRange(task.state === "completed" ? task.updatedAt : task.createdAt));
|
|
73
|
+
const allRecords = filterRecords(collectMetrics(ctx.service.metrics(), allTasks), body.query);
|
|
74
|
+
const records = allRecords.filter((record) => inRange(record.startedAt));
|
|
55
75
|
const stats = taskStats(tasks);
|
|
56
|
-
const classifier = summarizeClassifier(ctx.service.classifierMetrics?.() ?? [], records);
|
|
76
|
+
const classifier = summarizeClassifier((ctx.service.classifierMetrics?.() ?? []).filter((record) => inRange(record.startedAt)), records);
|
|
77
|
+
const daily = new Map<string, MetricsData["daily"][number]>();
|
|
78
|
+
for (const record of records) {
|
|
79
|
+
const date = dayOf(record.startedAt);
|
|
80
|
+
if (!date) continue;
|
|
81
|
+
const entry = daily.get(date) ?? { date, runs: 0, successes: 0, cost: 0 };
|
|
82
|
+
entry.runs += 1;
|
|
83
|
+
entry.successes += Number(record.status === "success");
|
|
84
|
+
entry.cost += record.cost ?? 0;
|
|
85
|
+
daily.set(date, entry);
|
|
86
|
+
}
|
|
87
|
+
// Include range edges without allocating an entry for every empty day.
|
|
88
|
+
for (const date of [from, to]) if (date && !daily.has(date)) daily.set(date, { date, runs: 0, successes: 0, cost: 0 });
|
|
57
89
|
return {
|
|
58
90
|
tiles: tilesOf(records, stats),
|
|
91
|
+
daily: [...daily.values()].sort((a, b) => a.date.localeCompare(b.date)),
|
|
59
92
|
groups: sortGroups(aggregateMetrics(records, body.groupBy), "runs"),
|
|
60
|
-
timeShare: { byAgent: shareOf(records), taskTimes: taskTimesByModel(tasks,
|
|
93
|
+
timeShare: { byAgent: shareOf(records), taskTimes: taskTimesByModel(tasks, allRecords) },
|
|
61
94
|
...(classifier ? { classifier } : {}),
|
|
62
95
|
};
|
|
63
96
|
}
|
|
@@ -30,7 +30,7 @@ function activeRuns(task: Task, work: WorkProjection | undefined): AgentRun[] {
|
|
|
30
30
|
const finished = new Set((task.workerRuns ?? []).map((record) => record.runId));
|
|
31
31
|
return (work?.active ?? [])
|
|
32
32
|
.filter((run) => !finished.has(run.runId))
|
|
33
|
-
.map((run) => ({ runId: run.runId, taskId: task.id, domain: "backend" as const, role: "worker" as const, status: "running" as const, instruction: run.instruction, output: "", attempts: 1, startedAt: run.startedAt }));
|
|
33
|
+
.map((run) => ({ runId: run.runId, parentRunId: run.parentRunId, stepInstruction: run.stepInstruction, taskId: task.id, domain: "backend" as const, role: "worker" as const, status: "running" as const, instruction: run.instruction, output: "", attempts: 1, startedAt: run.startedAt }));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** One step as the lobby shows it: its state, and while a worker is on it, how long they have worked on it. */
|
|
@@ -38,6 +38,7 @@ export interface StepView {
|
|
|
38
38
|
text: string;
|
|
39
39
|
status: PlanStep["status"];
|
|
40
40
|
active: boolean;
|
|
41
|
+
activeAgentCount: number;
|
|
41
42
|
/** Worker time on the step, in ms. */
|
|
42
43
|
workedMs: number;
|
|
43
44
|
}
|
|
@@ -53,7 +54,7 @@ export function stepViews(task: Task, work: WorkProjection | undefined, now = Da
|
|
|
53
54
|
const checklist = planChecklist(task.plan, runs);
|
|
54
55
|
const times = stepWork(planSteps(task.plan), runs, now);
|
|
55
56
|
return checklist.map((step, index) => {
|
|
56
|
-
const time = times[index] ?? { ms: 0, active: false };
|
|
57
|
-
return { text: step.text, status: time.active ? "current" : step.status, active: time.active, workedMs: Math.round(time.ms) };
|
|
57
|
+
const time = times[index] ?? { ms: 0, active: false, activeAgentCount: 0 };
|
|
58
|
+
return { text: step.text, status: time.active ? "current" : step.status, active: time.active, activeAgentCount: time.activeAgentCount, workedMs: Math.round(time.ms) };
|
|
58
59
|
});
|
|
59
60
|
}
|
package/src/webui/api/tasks.ts
CHANGED
|
@@ -311,7 +311,7 @@ export function tasksGet(body: { taskId: string }, ctx: ApiContext): TaskDetail
|
|
|
311
311
|
steps: stepViews(found, work, now).map((step) => ({
|
|
312
312
|
text: step.text,
|
|
313
313
|
status: step.status === "pending" ? "open" : step.status,
|
|
314
|
-
...(step.active ? { active: true, workedMs: step.workedMs } : {}),
|
|
314
|
+
...(step.active ? { active: true, workedMs: step.workedMs, activeAgentCount: step.activeAgentCount } : {}),
|
|
315
315
|
})),
|
|
316
316
|
amendments: [...found.amendments],
|
|
317
317
|
waiting: waitingOf(found),
|
|
@@ -478,7 +478,7 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
|
|
|
478
478
|
const work = fixture.mockWork?.[taskId];
|
|
479
479
|
if (!work) return undefined;
|
|
480
480
|
const now = Date.now();
|
|
481
|
-
return { workedMs: work.workedMs, running: work.running, active: (work.active ?? []).map((run) => ({ runId: run.runId, instruction: run.instruction, startedAt: new Date(now - run.startedAgoMs).toISOString() })) };
|
|
481
|
+
return { workedMs: work.workedMs, running: work.running, active: (work.active ?? []).map((run) => ({ runId: run.runId, parentRunId: run.parentRunId, stepInstruction: run.stepInstruction, instruction: run.instruction, startedAt: new Date(now - run.startedAgoMs).toISOString() })) };
|
|
482
482
|
},
|
|
483
483
|
plans: () => [...plans],
|
|
484
484
|
metrics: () => metrics.map((record) => ({ ...record })),
|
|
@@ -65,7 +65,7 @@ export interface ScenarioFixture {
|
|
|
65
65
|
/** Tasks backing `tasks.list` (absent means none). */
|
|
66
66
|
mockTasks?: Array<Record<string, unknown>>;
|
|
67
67
|
/** Work clocks by task id (`work.json` as the owner keeps it); a run's start is given as how long ago, so it reads live. */
|
|
68
|
-
mockWork?: Record<string, { workedMs: number; running: boolean; active?: Array<{ runId: string; instruction: string; startedAgoMs: number }> }>;
|
|
68
|
+
mockWork?: Record<string, { workedMs: number; running: boolean; active?: Array<{ runId: string; parentRunId?: string; stepInstruction?: string; instruction: string; startedAgoMs: number }> }>;
|
|
69
69
|
/** Saved plans backing `plans.start`/`plans.discard` and the pending rows. */
|
|
70
70
|
mockPlans?: Array<{ id: string; title: string; brief: string }>;
|
|
71
71
|
/** Archived tasks backing `tasks.archived`. */
|
package/src/webui/protocol.ts
CHANGED
|
@@ -131,7 +131,7 @@ export interface SnapshotTask {
|
|
|
131
131
|
progress?: { done: number; total: number };
|
|
132
132
|
currentStep?: string;
|
|
133
133
|
/** The steps a worker is on right now, with the worker time on each (ms, as of the snapshot). */
|
|
134
|
-
activeSteps?: Array<{ text: string; workedMs: number }>;
|
|
134
|
+
activeSteps?: Array<{ text: string; workedMs: number; activeAgentCount?: number }>;
|
|
135
135
|
/** How long the task's agents have worked on it, idle time left out. */
|
|
136
136
|
work?: WorkClock;
|
|
137
137
|
}
|
|
@@ -335,6 +335,7 @@ export interface MetricsClassifier {
|
|
|
335
335
|
export interface MetricsData {
|
|
336
336
|
tiles: MetricsTiles;
|
|
337
337
|
groups: MetricGroupInfo[];
|
|
338
|
+
daily: Array<{ date: string; runs: number; successes: number; cost: number }>;
|
|
338
339
|
timeShare: { byAgent: MetricsAgentShare[]; taskTimes: MetricsTaskTime[] };
|
|
339
340
|
classifier?: MetricsClassifier;
|
|
340
341
|
}
|
|
@@ -419,7 +420,7 @@ export interface TaskDetail {
|
|
|
419
420
|
* The plan as a checklist, in order; `current` is the step under way, `active` the steps a worker is on right now,
|
|
420
421
|
* each with its worker time (ms, as of the reply).
|
|
421
422
|
*/
|
|
422
|
-
steps: Array<{ text: string; status: "done" | "current" | "open"; active?: boolean; workedMs?: number }>;
|
|
423
|
+
steps: Array<{ text: string; status: "done" | "current" | "open"; active?: boolean; workedMs?: number; activeAgentCount?: number }>;
|
|
423
424
|
/** How long the task's agents have worked on it, idle time left out (ms, as of the reply); `running` while they work. */
|
|
424
425
|
work?: WorkClock;
|
|
425
426
|
amendments: string[];
|
|
@@ -634,7 +635,7 @@ export interface Api {
|
|
|
634
635
|
"quickfix.cancel": { request: { id: string }; result: { notice: string } };
|
|
635
636
|
"quickfix.runAnyway": { request: { id: string }; result: { notice: string } };
|
|
636
637
|
"quickfix.movedToTask": { request: { id: string }; result: { notice: string; key?: string } };
|
|
637
|
-
"metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string }; result: MetricsData };
|
|
638
|
+
"metrics.get": { request: { groupBy: "model" | "model-kind"; query?: string; from?: string; to?: string; timeZone?: string }; result: MetricsData };
|
|
638
639
|
"projects.browse": { request: { path?: string }; result: FolderListing };
|
|
639
640
|
"projects.open": { request: { path: string }; result: { project: ProjectInfo } };
|
|
640
641
|
"knowledge.files": { request: Record<string, never>; result: { files: KnowledgeFileInfo[]; models?: Partial<Record<KnowledgeAgentName, string>> } };
|
package/src/workflow/workflow.ts
CHANGED
|
@@ -2012,7 +2012,10 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2012
2012
|
// Every agent, diff and desk of a task with its own worktree works there.
|
|
2013
2013
|
cwd: taskCwd(task, deps.cwd),
|
|
2014
2014
|
onUpdate: (run) => {
|
|
2015
|
-
if (run.status !== "running")
|
|
2015
|
+
if (run.status !== "running") {
|
|
2016
|
+
finished.set(run.runId, run);
|
|
2017
|
+
if (run.parentRunId) writeFileEnsured(join(taskDirFor(deps.root, deps.configDir, task.id), "children", `${run.runId}.json`), `${JSON.stringify(run)}\n`);
|
|
2018
|
+
}
|
|
2016
2019
|
// Every window marks the steps a worker is on while it runs.
|
|
2017
2020
|
if (run.role === "worker") {
|
|
2018
2021
|
if (run.status === "running") working.add(run.runId);
|