@a-t-h-i/bot-lobby 0.1.0 → 0.3.0
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 +26 -6
- package/package.json +4 -2
- package/prompts/master.md +5 -0
- package/src/execution/pi-runner.ts +22 -10
- package/src/pi/activity.ts +3 -0
- package/src/pi/events.ts +25 -3
- package/src/pi/expressions.ts +46 -9
- package/src/pi/mascot-art.ts +150 -33
- package/src/pi/tools.ts +2 -2
- package/src/pi/ui.ts +135 -25
- package/src/pi/zen-large.ts +198 -27
- package/src/pi/zen.ts +227 -60
- package/src/schemas/task.ts +18 -0
- package/src/workflow/workflow.ts +25 -1
package/src/pi/zen.ts
CHANGED
|
@@ -67,9 +67,12 @@ const PREFIX_CHARS = 32;
|
|
|
67
67
|
const MIN_PREFIX = 8;
|
|
68
68
|
const HEADER_LINE = /^\s*(?:#{1,6}\s+\S.*|\*\*[^*]+\*\*)\s*$/;
|
|
69
69
|
const STEP_SECTION = /sequence|steps|order/i;
|
|
70
|
-
const NUMBERED_STEP_LINE =
|
|
71
|
-
const
|
|
72
|
-
const TOP_LEVEL_BULLET = /^[*-]\s+(.*\S)\s*$/;
|
|
70
|
+
const NUMBERED_STEP_LINE = /^(\s*)(\d+[.)])\s+(.*\S)\s*$/;
|
|
71
|
+
const BULLET_LINE = /^(\s*)([*-])\s+(.*\S)\s*$/;
|
|
72
|
+
const TOP_LEVEL_BULLET = /^()([*-])\s+(.*\S)\s*$/;
|
|
73
|
+
/** `### Step 2: Wire the API`, `**Step 2 — Wire the API**`, `Phase 3) Tests`: one plan step per heading. */
|
|
74
|
+
const STEP_HEADING = /^\s*(?:#{1,6}\s+)?(?:\*\*)?\s*(?:step|phase|stage)\s+#?\d+\s*(?:\*\*)?\s*[:.)\u2014\u2013-]\s*(?:\*\*)?\s*(.*?)\s*(?:\*\*)?\s*$/i;
|
|
75
|
+
const MIN_STEP_HEADINGS = 2;
|
|
73
76
|
|
|
74
77
|
export type PlanStepStatus = "done" | "current" | "pending";
|
|
75
78
|
|
|
@@ -78,20 +81,54 @@ export interface PlanStep {
|
|
|
78
81
|
status: PlanStepStatus;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
interface ListItem {
|
|
85
|
+
indent: number;
|
|
86
|
+
/** Column where the item's text starts; deeper items are nested under it. */
|
|
87
|
+
content: number;
|
|
88
|
+
text: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ItemMatcher = (line: string) => ListItem | undefined;
|
|
92
|
+
|
|
93
|
+
function indentOf(line: string): number {
|
|
94
|
+
return /^\s*/.exec(line)![0].replace(/\t/g, " ").length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function itemMatcher(pattern: RegExp): ItemMatcher {
|
|
98
|
+
return (line) => {
|
|
99
|
+
const match = pattern.exec(line);
|
|
100
|
+
if (!match) return undefined;
|
|
101
|
+
const indent = indentOf(match[1]!);
|
|
102
|
+
return { indent, content: indent + match[2]!.length + 1, text: match[3]! };
|
|
103
|
+
};
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
const numberedItem = itemMatcher(NUMBERED_STEP_LINE);
|
|
107
|
+
const bulletItem = itemMatcher(BULLET_LINE);
|
|
108
|
+
const topBulletItem = itemMatcher(TOP_LEVEL_BULLET);
|
|
109
|
+
|
|
110
|
+
/** Numbered `1.`/`1)` text, or a bullet inside a step section; undefined otherwise. */
|
|
111
|
+
function sectionItem(line: string): ListItem | undefined {
|
|
112
|
+
return numberedItem(line) ?? bulletItem(line);
|
|
88
113
|
}
|
|
89
114
|
|
|
90
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Top-level list items only. As in CommonMark, an item indented to its parent's
|
|
117
|
+
* text column is a sub-point of that step, so nested bullets or a nested `1.`
|
|
118
|
+
* list never inflate the checklist. Prose at a shallower indent ends the parent.
|
|
119
|
+
*/
|
|
120
|
+
function collectSteps(lines: readonly string[], accept: ItemMatcher): string[] {
|
|
91
121
|
const found: string[] = [];
|
|
122
|
+
let parent: number | undefined;
|
|
92
123
|
for (const line of lines) {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
124
|
+
const item = accept(line);
|
|
125
|
+
if (!item) {
|
|
126
|
+
if (parent !== undefined && line.trim() && indentOf(line) < parent) parent = undefined;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (parent !== undefined && item.indent >= parent) continue;
|
|
130
|
+
found.push(item.text);
|
|
131
|
+
parent = item.content;
|
|
95
132
|
}
|
|
96
133
|
return found;
|
|
97
134
|
}
|
|
@@ -108,37 +145,143 @@ function stepSection(lines: readonly string[]): string[] | undefined {
|
|
|
108
145
|
return body;
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
/** `Step N` headings, when the plan is structured as one heading per step. */
|
|
149
|
+
function headingSteps(lines: readonly string[]): string[] {
|
|
150
|
+
const found: string[] = [];
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const match = STEP_HEADING.exec(line);
|
|
153
|
+
if (match) found.push(match[1] || line.replace(/[#*]/g, "").trim());
|
|
154
|
+
}
|
|
155
|
+
return found.length >= MIN_STEP_HEADINGS ? found : [];
|
|
156
|
+
}
|
|
157
|
+
|
|
111
158
|
/**
|
|
112
|
-
* Step texts from a free-form plan, capped.
|
|
113
|
-
* numbered lines
|
|
114
|
-
*
|
|
159
|
+
* Step texts from a free-form plan, capped. `Step N` headings win; then a
|
|
160
|
+
* `sequence|steps|order` section; then numbered lines; when none exists,
|
|
161
|
+
* top-level bullets are the last resort, so unrelated bullet lists under other
|
|
162
|
+
* headers never leak into the checklist. Only the shallowest items count.
|
|
115
163
|
*/
|
|
116
164
|
export function planSteps(plan: string): string[] {
|
|
117
165
|
const lines = plan.split("\n");
|
|
166
|
+
const headings = headingSteps(lines);
|
|
167
|
+
if (headings.length > 0) return headings.slice(0, MAX_PLAN_STEPS);
|
|
118
168
|
const section = stepSection(lines);
|
|
119
169
|
if (section) {
|
|
120
|
-
const sectioned = collectSteps(section,
|
|
170
|
+
const sectioned = collectSteps(section, sectionItem);
|
|
121
171
|
if (sectioned.length > 0) return sectioned.slice(0, MAX_PLAN_STEPS);
|
|
122
172
|
}
|
|
123
|
-
const numbered = collectSteps(lines,
|
|
173
|
+
const numbered = collectSteps(lines, numberedItem);
|
|
124
174
|
if (numbered.length > 0) return numbered.slice(0, MAX_PLAN_STEPS);
|
|
125
|
-
return collectSteps(lines,
|
|
175
|
+
return collectSteps(lines, topBulletItem).slice(0, MAX_PLAN_STEPS);
|
|
126
176
|
}
|
|
127
177
|
|
|
178
|
+
/* -------------------------------------------------------------------------
|
|
179
|
+
* Step matching. A worker instruction names its step explicitly ("Step 3: ...")
|
|
180
|
+
* or is scored against every step by shared paths, a shared opening phrase and
|
|
181
|
+
* word overlap. Plans reuse file paths across steps, so near-ties go to the
|
|
182
|
+
* earliest step still open instead of the first step that ever mentioned the
|
|
183
|
+
* path -- otherwise every later instruction re-matches step 1 and the tracker
|
|
184
|
+
* never moves.
|
|
185
|
+
* ---------------------------------------------------------------------- */
|
|
186
|
+
|
|
187
|
+
const STEP_REFERENCE = /\bsteps?\s*#?\s*(\d+)(?:\s*(?:-|\u2013|\u2014|to|through|thru|and|&)\s*#?\s*(\d+))?/gi;
|
|
188
|
+
/** Text allowed before a step reference that labels the instruction itself ("Now implement step 3:"). */
|
|
189
|
+
const LEADING_LABEL = /^[\s\W]*(?:(?:now|next|then|please|implement|do|complete|execute|start|begin|finish|work on|continue with|proceed with|plan)\s+)*(?:the\s+)?$/i;
|
|
190
|
+
const MIN_SCORE = 0.5;
|
|
191
|
+
const NEAR_TIE = 0.35;
|
|
192
|
+
const PATH_WEIGHT = 0.75;
|
|
193
|
+
const PREFIX_WEIGHT = 1;
|
|
194
|
+
const WORD = /[a-z0-9_][a-z0-9_./-]*[a-z0-9_]/g;
|
|
195
|
+
const MIN_WORD = 3;
|
|
196
|
+
const STOP_WORDS = new Set([
|
|
197
|
+
"the", "and", "for", "with", "that", "this", "from", "into", "then", "when", "each", "use", "make",
|
|
198
|
+
"sure", "new", "all", "any", "its", "are", "not", "but", "now", "via", "per", "our", "your", "you",
|
|
199
|
+
"has", "have", "will", "should", "must", "also", "only", "step", "steps", "plan", "approved",
|
|
200
|
+
"implement", "please", "add", "update", "change", "changes", "file", "files", "code",
|
|
201
|
+
]);
|
|
202
|
+
|
|
128
203
|
function normalize(text: string): string {
|
|
129
204
|
return text.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
130
205
|
}
|
|
131
206
|
|
|
132
|
-
function
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
207
|
+
function significantWords(text: string): Set<string> {
|
|
208
|
+
const words = new Set<string>();
|
|
209
|
+
for (const word of normalize(text).match(WORD) ?? []) {
|
|
210
|
+
if (word.length >= MIN_WORD && !STOP_WORDS.has(word)) words.add(word);
|
|
211
|
+
}
|
|
212
|
+
return words;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Scoring context for one instruction, built once per run and reused for every step. */
|
|
216
|
+
interface InstructionIndex {
|
|
217
|
+
text: string;
|
|
218
|
+
words: Set<string>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function indexInstruction(instruction: string): InstructionIndex {
|
|
222
|
+
return { text: normalize(instruction), words: significantWords(instruction) };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function prefixMatches(step: string, hay: string): boolean {
|
|
226
|
+
const tail = normalize(step.replace(/`[^`]+`/g, " ").replace(/^[\s:;,.\u2014\u2013-]+/, ""));
|
|
137
227
|
return tail.length >= MIN_PREFIX && hay.includes(tail.slice(0, PREFIX_CHARS));
|
|
138
228
|
}
|
|
139
229
|
|
|
230
|
+
function pathShare(step: string, hay: string): number {
|
|
231
|
+
const paths = [...step.matchAll(/`([^`]+)`/g)].map((match) => normalize(match[1]!)).filter((path) => path.length > 0);
|
|
232
|
+
if (paths.length === 0) return 0;
|
|
233
|
+
return paths.filter((path) => hay.includes(path)).length / paths.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function wordShare(step: string, words: ReadonlySet<string>): number {
|
|
237
|
+
const own = significantWords(step);
|
|
238
|
+
if (own.size === 0) return 0;
|
|
239
|
+
let shared = 0;
|
|
240
|
+
for (const word of own) if (words.has(word)) shared += 1;
|
|
241
|
+
return shared / own.size;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** How strongly an instruction targets one step; 0 when it shares nothing. */
|
|
245
|
+
function stepScore(step: string, instruction: InstructionIndex): number {
|
|
246
|
+
const prefix = prefixMatches(step, instruction.text) ? PREFIX_WEIGHT : 0;
|
|
247
|
+
return prefix + PATH_WEIGHT * pathShare(step, instruction.text) + wordShare(step, instruction.words);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Highest 0-based step an instruction labels itself with ("Step 3: ...", "steps 2-4"),
|
|
252
|
+
* or -1. A reference counts when it opens the instruction or is the only one
|
|
253
|
+
* named, so "building on step 1, now do step 3" does not jump back to step 1.
|
|
254
|
+
*/
|
|
255
|
+
export function explicitStepIndex(instruction: string, count: number): number {
|
|
256
|
+
const refs = [...instruction.matchAll(STEP_REFERENCE)];
|
|
257
|
+
if (refs.length === 0) return -1;
|
|
258
|
+
const first = refs[0]!;
|
|
259
|
+
const leading = LEADING_LABEL.test(instruction.slice(0, first.index ?? 0)) ? first : undefined;
|
|
260
|
+
const distinct = new Set(refs.map((ref) => ref[0].toLowerCase().replace(/\s+/g, "")));
|
|
261
|
+
const chosen = leading ?? (distinct.size === 1 ? refs[0] : undefined);
|
|
262
|
+
if (!chosen) return -1;
|
|
263
|
+
const last = Math.max(Number(chosen[1]), Number(chosen[2] ?? chosen[1]));
|
|
264
|
+
return last >= 1 && last <= count ? last - 1 : -1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The step an instruction targets, given the steps already completed; -1 when
|
|
269
|
+
* nothing matches. Among near-tied candidates the earliest open step wins.
|
|
270
|
+
*/
|
|
271
|
+
export function targetStep(steps: readonly string[], instruction: string | undefined, completed: ReadonlySet<number> = new Set()): number {
|
|
272
|
+
if (!instruction || steps.length === 0) return -1;
|
|
273
|
+
const explicit = explicitStepIndex(instruction, steps.length);
|
|
274
|
+
if (explicit >= 0) return explicit;
|
|
275
|
+
const index = indexInstruction(instruction);
|
|
276
|
+
const scores = steps.map((step) => stepScore(step, index));
|
|
277
|
+
const best = Math.max(...scores);
|
|
278
|
+
if (best < MIN_SCORE) return -1;
|
|
279
|
+
const near = scores.findIndex((score, at) => score >= best - NEAR_TIE && score >= MIN_SCORE && !completed.has(at));
|
|
280
|
+
return near >= 0 ? near : scores.indexOf(best);
|
|
281
|
+
}
|
|
282
|
+
|
|
140
283
|
/** The latest worker run carrying an instruction; its step is the current one. */
|
|
141
|
-
export function latestWorkerRun(runs: AgentRun[]): AgentRun | undefined {
|
|
284
|
+
export function latestWorkerRun(runs: readonly AgentRun[]): AgentRun | undefined {
|
|
142
285
|
let latest: AgentRun | undefined;
|
|
143
286
|
for (const run of runs) {
|
|
144
287
|
if (run.role !== "worker" || !run.instruction) continue;
|
|
@@ -147,11 +290,6 @@ export function latestWorkerRun(runs: AgentRun[]): AgentRun | undefined {
|
|
|
147
290
|
return latest;
|
|
148
291
|
}
|
|
149
292
|
|
|
150
|
-
/** Index of the plan step an instruction targets, -1 when nothing matches. */
|
|
151
|
-
export function currentStepIndex(steps: readonly string[], instruction: string | undefined): number {
|
|
152
|
-
return instruction ? steps.findIndex((step) => matchesInstruction(step, instruction)) : -1;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
293
|
function stepStatus(index: number, current: number): PlanStepStatus {
|
|
156
294
|
if (current < 0) return index === 0 ? "current" : "pending";
|
|
157
295
|
if (index < current) return "done";
|
|
@@ -168,37 +306,58 @@ function markThrough(completed: Set<number>, index: number): void {
|
|
|
168
306
|
for (let step = 0; step <= index; step += 1) completed.add(step);
|
|
169
307
|
}
|
|
170
308
|
|
|
309
|
+
function byStart(runs: readonly AgentRun[]): AgentRun[] {
|
|
310
|
+
const time = (run: AgentRun) => {
|
|
311
|
+
const at = Date.parse(run.startedAt);
|
|
312
|
+
return Number.isFinite(at) ? at : 0;
|
|
313
|
+
};
|
|
314
|
+
return runs
|
|
315
|
+
.map((run, order) => ({ run, order }))
|
|
316
|
+
.sort((a, b) => time(a.run) - time(b.run) || a.order - b.order)
|
|
317
|
+
.map((entry) => entry.run);
|
|
318
|
+
}
|
|
319
|
+
|
|
171
320
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* and a failure can never tick one off.
|
|
321
|
+
* Replays worker runs in start order. A successful run completes everything up
|
|
322
|
+
* to its target step; a run that matches nothing -- the common case for a
|
|
323
|
+
* reworded instruction -- completes the next still-open step, so the count only
|
|
324
|
+
* grows and a failure can never tick one off. The latest worker run's own target
|
|
325
|
+
* is reported separately so a running or failed step reads current.
|
|
176
326
|
*/
|
|
177
|
-
function
|
|
327
|
+
function replaySteps(steps: readonly string[], runs: readonly AgentRun[]): { completed: number; latest: number } {
|
|
178
328
|
const completed = new Set<number>();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
329
|
+
const latestRun = latestWorkerRun(runs);
|
|
330
|
+
let latest = -1;
|
|
331
|
+
for (const run of byStart(runs)) {
|
|
332
|
+
if (run.role !== "worker") continue;
|
|
333
|
+
const matched = targetStep(steps, run.instruction, completed);
|
|
334
|
+
if (run === latestRun) latest = matched >= 0 && run.status === "success" ? matched + 1 : matched;
|
|
335
|
+
if (run.status !== "success") continue;
|
|
182
336
|
const target = matched >= 0 ? matched : nextOpenStep(steps, completed);
|
|
183
337
|
if (target >= 0) markThrough(completed, target);
|
|
184
338
|
}
|
|
185
|
-
return completed.size;
|
|
339
|
+
return { completed: completed.size, latest };
|
|
186
340
|
}
|
|
187
341
|
|
|
342
|
+
/** One-entry memo: the panel asks for the same checklist several times per frame. */
|
|
343
|
+
let checklistMemo: { plan: string; runs: readonly AgentRun[]; steps: PlanStep[] } | undefined;
|
|
344
|
+
|
|
188
345
|
/**
|
|
189
|
-
* Done/current/pending per plan step
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
346
|
+
* Done/current/pending per plan step. A succeeded run completes its step, so the
|
|
347
|
+
* next step becomes current (and the last step reads done); running, failed,
|
|
348
|
+
* cancelled and timeout runs keep it current. Progress is monotonic: steps
|
|
349
|
+
* already completed by successful worker runs stay done, and a later call that
|
|
350
|
+
* names an earlier step can never tick it back. Memoized on the exact plan text
|
|
351
|
+
* and runs array, so callers must not mutate either.
|
|
194
352
|
*/
|
|
195
|
-
export function planChecklist(plan: string, runs: AgentRun[]): PlanStep[] {
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
353
|
+
export function planChecklist(plan: string, runs: readonly AgentRun[]): PlanStep[] {
|
|
354
|
+
if (checklistMemo && checklistMemo.plan === plan && checklistMemo.runs === runs) return checklistMemo.steps;
|
|
355
|
+
const texts = planSteps(plan);
|
|
356
|
+
const replay = replaySteps(texts, runs);
|
|
357
|
+
const current = Math.max(replay.completed, replay.latest);
|
|
358
|
+
const steps = texts.map((text, index) => ({ text, status: stepStatus(index, current) }));
|
|
359
|
+
checklistMemo = { plan, runs, steps };
|
|
360
|
+
return steps;
|
|
202
361
|
}
|
|
203
362
|
|
|
204
363
|
/** Up to `count` consecutive step indexes centered on the current step, hard-capped at `max`. */
|
|
@@ -211,7 +370,7 @@ export function checklistWindow(steps: readonly PlanStep[], count: number, max =
|
|
|
211
370
|
return Array.from({ length: size }, (_value, offset) => start + offset);
|
|
212
371
|
}
|
|
213
372
|
|
|
214
|
-
function
|
|
373
|
+
function checklistRow(step: PlanStep, ordinal: number): string {
|
|
215
374
|
const icon = step.status === "done" ? "✓" : step.status === "current" ? "◐" : "○";
|
|
216
375
|
return ` ${icon} ${ordinal}. ${step.text}`;
|
|
217
376
|
}
|
|
@@ -329,7 +488,7 @@ function tailLines(task: Task, runs: AgentRun[], now: number, tick: number, step
|
|
|
329
488
|
|
|
330
489
|
function checklistLines(steps: PlanStep[], count: number): string[] {
|
|
331
490
|
if (steps.length === 0 || count <= 0) return [];
|
|
332
|
-
return checklistWindow(steps, count).map((index) =>
|
|
491
|
+
return checklistWindow(steps, count).map((index) => checklistRow(steps[index]!, index + 1));
|
|
333
492
|
}
|
|
334
493
|
|
|
335
494
|
function compactPanel(
|
|
@@ -378,9 +537,9 @@ function sceneSlots(metrics: SceneMetrics, expressions: ExpressionFrames): Large
|
|
|
378
537
|
}));
|
|
379
538
|
}
|
|
380
539
|
|
|
381
|
-
function oracleSlot(task: Task, expressions: ExpressionFrames): LargeSceneInput["oracle"] {
|
|
540
|
+
function oracleSlot(task: Task, expressions: ExpressionFrames, motion: OracleMotion): LargeSceneInput["oracle"] {
|
|
382
541
|
const pose = oraclePose(task);
|
|
383
|
-
return { pose, frame: expressions.oracle ?? REST_FRAME };
|
|
542
|
+
return { pose, frame: expressions.oracle ?? REST_FRAME, ...motion };
|
|
384
543
|
}
|
|
385
544
|
|
|
386
545
|
function sceneTasks(steps: readonly PlanStep[]): LargeTaskRow[] {
|
|
@@ -397,8 +556,7 @@ function sceneInput(
|
|
|
397
556
|
quiet: boolean,
|
|
398
557
|
tick: number,
|
|
399
558
|
steps: PlanStep[],
|
|
400
|
-
|
|
401
|
-
oracleActivity: string | undefined,
|
|
559
|
+
opts: PanelOptions,
|
|
402
560
|
): LargeSceneInput {
|
|
403
561
|
const metrics = sceneMetrics(task, runs, now);
|
|
404
562
|
const alert = taskAlert(task);
|
|
@@ -411,10 +569,11 @@ function sceneInput(
|
|
|
411
569
|
tick,
|
|
412
570
|
done: metrics.done,
|
|
413
571
|
total: metrics.total,
|
|
414
|
-
slots: sceneSlots(metrics, expressions),
|
|
572
|
+
slots: sceneSlots(metrics, opts.expressions ?? {}),
|
|
415
573
|
tasks: sceneTasks(steps),
|
|
416
|
-
oracle: oracleSlot(task, expressions),
|
|
417
|
-
oracleActivity,
|
|
574
|
+
oracle: oracleSlot(task, opts.expressions ?? {}, opts.oracleMotion ?? {}),
|
|
575
|
+
oracleActivity: opts.oracleActivity,
|
|
576
|
+
caption: task.paused ? "task paused" : SCENE_PROPS[task.state],
|
|
418
577
|
alert: alert?.text,
|
|
419
578
|
alertKind: alert?.kind,
|
|
420
579
|
};
|
|
@@ -429,8 +588,16 @@ export interface PanelOptions {
|
|
|
429
588
|
theme?: PanelTheme;
|
|
430
589
|
/** Caller-scheduled expression frame per slot and the oracle; absent means rest. */
|
|
431
590
|
expressions?: ExpressionFrames;
|
|
432
|
-
/** Live master activity word for the oracle
|
|
591
|
+
/** Live master activity word for the oracle's speech bubble; absent means it waits on the user. */
|
|
433
592
|
oracleActivity?: string;
|
|
593
|
+
/** Caller-clocked oracle animation: the expression's sub-step and the lip-sync shape. */
|
|
594
|
+
oracleMotion?: OracleMotion;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** The oracle's clocked animation state beyond its expression frame (see expressions.ts). */
|
|
598
|
+
export interface OracleMotion {
|
|
599
|
+
phase?: number;
|
|
600
|
+
talk?: number;
|
|
434
601
|
}
|
|
435
602
|
|
|
436
603
|
/**
|
|
@@ -453,7 +620,7 @@ export function panelLines(
|
|
|
453
620
|
const steps = planChecklist(task.plan ?? "", runs);
|
|
454
621
|
const budget = largeLineBudget(opts.rows ?? DEFAULT_ROWS);
|
|
455
622
|
if (width >= LARGE_MIN_WIDTH && budget >= MIN_LARGE_LINES) {
|
|
456
|
-
const scene = largeLines(sceneInput(task, runs, now, quiet, tick, steps, opts
|
|
623
|
+
const scene = largeLines(sceneInput(task, runs, now, quiet, tick, steps, opts), width, budget, opts.theme);
|
|
457
624
|
return scene.map((line) => truncateToWidth(line, width));
|
|
458
625
|
}
|
|
459
626
|
return compactPanel(task, runs, now, quiet, tick, steps, opts, width);
|
package/src/schemas/task.ts
CHANGED
|
@@ -54,6 +54,22 @@ export interface ReviewRecord {
|
|
|
54
54
|
requiredChanges: string[];
|
|
55
55
|
createdAt: string;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One finished worker delegation, kept on the task so the plan checklist can be
|
|
59
|
+
* replayed after a reload instead of resetting to the first step.
|
|
60
|
+
*/
|
|
61
|
+
export interface WorkerRunRecord {
|
|
62
|
+
runId: string;
|
|
63
|
+
domain: Domain;
|
|
64
|
+
instruction: string;
|
|
65
|
+
status: "running" | "success" | "failed" | "cancelled" | "timeout";
|
|
66
|
+
startedAt: string;
|
|
67
|
+
finishedAt?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Upper bound on persisted worker records; plans cap at 50 steps. */
|
|
71
|
+
export const MAX_WORKER_RECORDS = 64;
|
|
72
|
+
|
|
57
73
|
export interface Task {
|
|
58
74
|
id: string;
|
|
59
75
|
title: string;
|
|
@@ -71,6 +87,8 @@ export interface Task {
|
|
|
71
87
|
blockers: Blocker[];
|
|
72
88
|
decisions: Decision[];
|
|
73
89
|
approvals: Approval[];
|
|
90
|
+
/** Worker delegations in start order; absent on tasks created before tracking. */
|
|
91
|
+
workerRuns?: WorkerRunRecord[];
|
|
74
92
|
createdAt: string;
|
|
75
93
|
updatedAt: string;
|
|
76
94
|
/** The pi session (ctx.sessionManager id) that owns this task; absent on legacy tasks. */
|
package/src/workflow/workflow.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import type { BotLobbyConfig } from "../schemas/configuration.ts";
|
|
3
3
|
import type { AgentRun, Pushback, ResearchResult, ReviewResult } from "../schemas/findings.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MAX_WORKER_RECORDS,
|
|
6
|
+
TASK_STATES,
|
|
7
|
+
TERMINAL_STATES,
|
|
8
|
+
taskRequest,
|
|
9
|
+
type Approval,
|
|
10
|
+
type ApprovalKind,
|
|
11
|
+
type Task,
|
|
12
|
+
type TaskState,
|
|
13
|
+
type WorkerRunRecord,
|
|
14
|
+
} from "../schemas/task.ts";
|
|
5
15
|
import { isDomain, type Domain } from "../schemas/agent.ts";
|
|
6
16
|
import { transition } from "../state/task-state.ts";
|
|
7
17
|
import { ownerlessTask, readTaskArtifact, removeTaskScratchpads, saveTask, selectTask, taskDirFor, taskReadDirs } from "../state/persistence.ts";
|
|
@@ -495,6 +505,19 @@ function workerRequest(deps: WorkflowDeps, task: Task, domain: Domain, instructi
|
|
|
495
505
|
};
|
|
496
506
|
}
|
|
497
507
|
|
|
508
|
+
/** Remember a worker delegation so the checklist replays it after a reload. */
|
|
509
|
+
function recordWorkerRun(task: Task, run: AgentRun): void {
|
|
510
|
+
const record: WorkerRunRecord = {
|
|
511
|
+
runId: run.runId,
|
|
512
|
+
domain: run.domain,
|
|
513
|
+
instruction: run.instruction ?? "",
|
|
514
|
+
status: run.status,
|
|
515
|
+
startedAt: run.startedAt,
|
|
516
|
+
...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),
|
|
517
|
+
};
|
|
518
|
+
task.workerRuns = [...(task.workerRuns ?? []), record].slice(-MAX_WORKER_RECORDS);
|
|
519
|
+
}
|
|
520
|
+
|
|
498
521
|
async function handleImplement(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
|
|
499
522
|
requireState(task, ["planning", "implementing", "reviewing"]);
|
|
500
523
|
const domain = parseDomain(params.domain, "implement");
|
|
@@ -504,6 +527,7 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
504
527
|
if (!task.domains.includes(domain)) task.domains.push(domain);
|
|
505
528
|
if (task.state !== "implementing") transition(task, "implementing");
|
|
506
529
|
const outcome = await runWorker(workerRequest(deps, task, domain, instruction), deps.runProcess ?? spawnPiProcess);
|
|
530
|
+
recordWorkerRun(task, outcome.run);
|
|
507
531
|
const approvals = recordWorkerApprovals(task, outcome, deps.config);
|
|
508
532
|
const pushback = recordPushback(task, outcome);
|
|
509
533
|
task.blockers = [...task.blockers.filter((blocker) => blocker.domain !== domain), ...outcome.result.blockers];
|