@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/README.md
CHANGED
|
@@ -117,10 +117,21 @@ While the owning session has a task active, its transcript switches to a zen vie
|
|
|
117
117
|
and the built-in spinner are hidden, and a widget above the editor animates the
|
|
118
118
|
task. At 72 columns and wider it draws a large scene: a header box with the task
|
|
119
119
|
title and state in its top border, a progress bar, and a metadata row with
|
|
120
|
-
elapsed time, quiet-mode hint and task id;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
elapsed time, quiet-mode hint and task id; an oracle tower with a twinkling
|
|
121
|
+
aura (drifting z's while dormant), a radiant orb crown, two window eyes, a
|
|
122
|
+
seven-column mouth and its ORC door. Its pupils look around: they move
|
|
123
|
+
left, centre or right in each window and turn up `◓`, ahead `◉` or down `◒`.
|
|
124
|
+
While agents work it looks down at them, taking turns between them; while it
|
|
125
|
+
talks it looks at you; otherwise its eyes wander the room and keep returning
|
|
126
|
+
to you. It keeps a straight, serious face (`───`), tightening into a frown
|
|
127
|
+
only when the task is blocked, and its mouth lip-syncs as a voice waveform
|
|
128
|
+
for ~2 s whenever it says something new. Every 6–12 s it blinks (lids
|
|
129
|
+
stepping down and up) or scans the whole room; asleep, it peeks one eye open
|
|
130
|
+
or snores. Beside the crown, the oracle's speech
|
|
131
|
+
bubble, its tail on the orb, says what the master is doing (`⠋ delegating`,
|
|
132
|
+
`⠋ thinking`, `· your turn` once its turn ends, `· dormant` when paused) above
|
|
133
|
+
who is at work (`→ DEV · QA`), the current step (`step 3 of 7`) or the task
|
|
134
|
+
phase (`awaiting your approval`);
|
|
124
135
|
four animated slots — DEV, DESIGN, RESEARCH and QA — each with a status face, a
|
|
125
136
|
caption and two status rows: while running, a braille spinner beside the agent's
|
|
126
137
|
live one-word activity (for example `⠋ reading` or `⠋ editing`) with its elapsed
|
|
@@ -131,13 +142,22 @@ terminals keep the
|
|
|
131
142
|
boxed banner, header and compact animated strip, whose working line names the
|
|
132
143
|
newest running agent's activity and elapsed time. Each sprite rests on one calm
|
|
133
144
|
face and, independently every 20–30 s, briefly blinks (~500 ms) or emotes
|
|
134
|
-
(~2 s, stepping through its kaomoji frames)
|
|
145
|
+
(~2 s, stepping through its kaomoji frames), all on one
|
|
135
146
|
adaptive clock — 250 ms while work is live, 1 s when idle and ~120 ms while an
|
|
136
|
-
expression plays — and their faces, colours and words follow each agent's status
|
|
147
|
+
expression plays or the oracle talks — and their faces, colours and words follow each agent's status
|
|
137
148
|
(working, idle, done, failed). The large scene's rest and blink frames stay the
|
|
138
149
|
five-column ASCII eyes, while its emote frames are status-aware kaomoji: nervous
|
|
139
150
|
while working, happy when done (QA flexes and dances), scared on failure. The
|
|
140
151
|
header progress bar is plan-derived.
|
|
152
|
+
|
|
153
|
+
The checklist follows the workers through the plan. Plan steps are read from
|
|
154
|
+
`Step N` headings, a `Steps`/`Sequence`/`Order` section, or numbered lines, and
|
|
155
|
+
only top-level items count (sub-points nested under a step never inflate it).
|
|
156
|
+
Each worker instruction is matched to a step by an explicit label
|
|
157
|
+
(`Step 3: ...`, `steps 2-4`) or, failing that, by shared paths, its opening
|
|
158
|
+
phrase and word overlap, with near-ties going to the earliest open step so a
|
|
159
|
+
file path reused across steps cannot pin progress to step 1. Every worker
|
|
160
|
+
delegation is recorded on the task, so progress survives a reload.
|
|
141
161
|
New tasks get a <=3-word title derived from the request (for example "create
|
|
142
162
|
landing page") plus an id `TASK-<slug>` built from the full request, so the banner
|
|
143
163
|
and header stay concise; older `TASK-<timestamp>` tasks keep loading untouched.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"pi": {
|
|
26
|
-
"extensions": [
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./src/index.ts"
|
|
28
|
+
]
|
|
27
29
|
},
|
|
28
30
|
"scripts": {
|
|
29
31
|
"typecheck": "tsc --noEmit",
|
package/prompts/master.md
CHANGED
|
@@ -47,6 +47,11 @@ Assign work to the correct domain; never ask one domain to do another's. A
|
|
|
47
47
|
cross-domain dependency is reported to you, and you decide whether another
|
|
48
48
|
domain needs a task.
|
|
49
49
|
|
|
50
|
+
Write the plan's steps as a numbered list under a `## Steps` heading, and open
|
|
51
|
+
each `implement` task with its step number (`Step 3: ...`, or `Steps 3-4: ...`
|
|
52
|
+
when one delegation covers several) so the user's checklist tracks progress
|
|
53
|
+
exactly.
|
|
54
|
+
|
|
50
55
|
## Research
|
|
51
56
|
|
|
52
57
|
Summon the researcher with `orchestrate action=research` (a `domain` and an
|
|
@@ -77,9 +77,12 @@ export interface StreamCollector {
|
|
|
77
77
|
* them.
|
|
78
78
|
*/
|
|
79
79
|
export function createStreamCollector(onEvent?: (event: PiStreamEvent) => void): StreamCollector {
|
|
80
|
-
|
|
80
|
+
const partial: string[] = [];
|
|
81
81
|
const kept: string[] = [];
|
|
82
82
|
const keep = (line: string) => {
|
|
83
|
+
// Most lines are per-token `message_update` deltas or large tool results;
|
|
84
|
+
// skip the parse for any line that cannot be one of the two kept events.
|
|
85
|
+
if (!line.includes('"tool_execution_start"') && !line.includes('"message_end"')) return;
|
|
83
86
|
let raw: unknown;
|
|
84
87
|
try {
|
|
85
88
|
raw = JSON.parse(line);
|
|
@@ -94,15 +97,21 @@ export function createStreamCollector(onEvent?: (event: PiStreamEvent) => void):
|
|
|
94
97
|
if (event.type === "message_end" && event.message?.role === "assistant") kept.push(line);
|
|
95
98
|
};
|
|
96
99
|
return {
|
|
100
|
+
// Scan only the new chunk for line breaks; a long line split over many
|
|
101
|
+
// chunks is joined once, when its newline finally arrives.
|
|
97
102
|
push(chunk) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
let start = 0;
|
|
104
|
+
let newline = chunk.indexOf("\n");
|
|
105
|
+
while (newline >= 0) {
|
|
106
|
+
const tail = chunk.slice(start, newline);
|
|
107
|
+
keep(partial.length > 0 ? partial.splice(0).join("") + tail : tail);
|
|
108
|
+
start = newline + 1;
|
|
109
|
+
newline = chunk.indexOf("\n", start);
|
|
110
|
+
}
|
|
111
|
+
if (start < chunk.length) partial.push(chunk.slice(start));
|
|
102
112
|
},
|
|
103
113
|
finish() {
|
|
104
|
-
if (
|
|
105
|
-
pending = "";
|
|
114
|
+
if (partial.length > 0) keep(partial.splice(0).join(""));
|
|
106
115
|
return kept.join("\n");
|
|
107
116
|
},
|
|
108
117
|
};
|
|
@@ -199,10 +208,13 @@ function wireOutput(
|
|
|
199
208
|
onEvent?: (event: PiStreamEvent) => void,
|
|
200
209
|
): () => void {
|
|
201
210
|
const collector = createStreamCollector(onEvent);
|
|
202
|
-
|
|
203
|
-
proc.
|
|
211
|
+
// Decode as UTF-8 streams so a multi-byte character split across chunks survives.
|
|
212
|
+
proc.stdout?.setEncoding("utf8");
|
|
213
|
+
proc.stderr?.setEncoding("utf8");
|
|
214
|
+
proc.stdout?.on("data", (chunk: string) => collector.push(chunk));
|
|
215
|
+
proc.stderr?.on("data", (chunk: string) => {
|
|
204
216
|
// Keep stderr head-bounded: it only feeds exit-code error messages.
|
|
205
|
-
if (buffers.stderr.length < MAX_STREAM_CHARS) buffers.stderr += chunk
|
|
217
|
+
if (buffers.stderr.length < MAX_STREAM_CHARS) buffers.stderr += chunk;
|
|
206
218
|
});
|
|
207
219
|
return () => {
|
|
208
220
|
buffers.stdout = collector.finish();
|
package/src/pi/activity.ts
CHANGED
|
@@ -22,6 +22,9 @@ const ACTIVITY_WORDS: Record<string, string> = {
|
|
|
22
22
|
|
|
23
23
|
const FALLBACK_WORD = "working";
|
|
24
24
|
|
|
25
|
+
/** What the oracle says between its own tool calls while a master turn runs. */
|
|
26
|
+
export const ORACLE_THINKING = "thinking";
|
|
27
|
+
|
|
25
28
|
export function activityWord(toolName: string): string {
|
|
26
29
|
return ACTIVITY_WORDS[toolName.trim().toLowerCase()] ?? FALLBACK_WORD;
|
|
27
30
|
}
|
package/src/pi/events.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { cancelAllRuns } from "../execution/agent-runner.ts";
|
|
|
8
8
|
import { describeTask } from "../workflow/workflow.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
10
|
import { applyStatus, clearStatus, isMinimized, setMinimized, setOracleActivity } from "./ui.ts";
|
|
11
|
-
import { oracleActivityWord } from "./activity.ts";
|
|
11
|
+
import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
|
|
12
12
|
import { isSubagentProcess, visibleTools } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
14
|
import { taskRequest, type Task } from "../schemas/task.ts";
|
|
@@ -43,9 +43,31 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
// The oracle
|
|
46
|
+
// The oracle's speech bubble mirrors the master's own turn: thinking between
|
|
47
|
+
// tool calls, the tool's word during one, and silent (your turn) once it ends.
|
|
48
|
+
// Subagents report into runs instead.
|
|
49
|
+
// Parallel tool calls: the newest still-running call keeps the word.
|
|
50
|
+
const inFlight = new Map<string, string>();
|
|
51
|
+
const showOracle = () => setOracleActivity([...inFlight.values()].at(-1) ?? ORACLE_THINKING);
|
|
52
|
+
pi.on("agent_start", () => {
|
|
53
|
+
if (isSubagentProcess()) return;
|
|
54
|
+
inFlight.clear();
|
|
55
|
+
showOracle();
|
|
56
|
+
});
|
|
47
57
|
pi.on("tool_execution_start", (event) => {
|
|
48
|
-
if (
|
|
58
|
+
if (isSubagentProcess()) return;
|
|
59
|
+
inFlight.set(event.toolCallId, oracleActivityWord(event.toolName, event.args));
|
|
60
|
+
showOracle();
|
|
61
|
+
});
|
|
62
|
+
pi.on("tool_execution_end", (event) => {
|
|
63
|
+
if (isSubagentProcess()) return;
|
|
64
|
+
inFlight.delete(event.toolCallId);
|
|
65
|
+
showOracle();
|
|
66
|
+
});
|
|
67
|
+
pi.on("agent_end", () => {
|
|
68
|
+
if (isSubagentProcess()) return;
|
|
69
|
+
inFlight.clear();
|
|
70
|
+
setOracleActivity(undefined);
|
|
49
71
|
});
|
|
50
72
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
51
73
|
cancelAllRuns();
|
package/src/pi/expressions.ts
CHANGED
|
@@ -44,9 +44,20 @@ function unit(rng: () => number): number {
|
|
|
44
44
|
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
export
|
|
49
|
-
|
|
47
|
+
/** Resting window between expressions, milliseconds. */
|
|
48
|
+
export interface ExpressionGap {
|
|
49
|
+
min: number;
|
|
50
|
+
max: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Agent slots rest 20-30 s between expressions. */
|
|
54
|
+
export const SLOT_GAP: ExpressionGap = { min: BLINK_MIN_MS, max: BLINK_MAX_MS };
|
|
55
|
+
/** The oracle is the scene's centrepiece: it blinks and emotes every 6-12 s. */
|
|
56
|
+
export const ORACLE_GAP: ExpressionGap = { min: 6_000, max: 12_000 };
|
|
57
|
+
|
|
58
|
+
/** Random resting gap before the next expression, inside `gap` (the slot window by default). */
|
|
59
|
+
export function nextGap(rng: () => number, gap: ExpressionGap = SLOT_GAP): number {
|
|
60
|
+
return gap.min + Math.round(unit(rng) * (gap.max - gap.min));
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
/** Whether the next expression blinks (~65%) or emotes (~35%). */
|
|
@@ -55,9 +66,9 @@ export function pickEvent(rng: () => number): "blink" | "emote" {
|
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/** A freshly rested expression that fires for the first time after one random gap. */
|
|
58
|
-
export function createExpression(now: number, rng: () => number): ExpressionState {
|
|
69
|
+
export function createExpression(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
|
|
59
70
|
const start = Number.isFinite(now) ? now : 0;
|
|
60
|
-
return { nextAt: start + nextGap(rng), until: start, startedAt: start, frame: REST_FRAME };
|
|
71
|
+
return { nextAt: start + nextGap(rng, gap), until: start, startedAt: start, frame: REST_FRAME };
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
/** True while `now` is inside a playing blink or emote. */
|
|
@@ -74,17 +85,43 @@ export function anyPlaying(states: readonly ExpressionState[], now: number): boo
|
|
|
74
85
|
* Advance one sprite to `now`: start a blink or emote once the gap has elapsed,
|
|
75
86
|
* drop back to the rest frame when it ends, and change nothing in between.
|
|
76
87
|
*/
|
|
77
|
-
export function advanceExpression(
|
|
88
|
+
export function advanceExpression(
|
|
89
|
+
state: ExpressionState,
|
|
90
|
+
now: number,
|
|
91
|
+
rng: () => number,
|
|
92
|
+
gap: ExpressionGap = SLOT_GAP,
|
|
93
|
+
): ExpressionState {
|
|
78
94
|
if (!Number.isFinite(now)) return state;
|
|
79
95
|
if (now < state.until) return state.frame >= EMOTE_FRAME ? steppedEmote(state, now) : state;
|
|
80
96
|
if (now < state.nextAt) return state.frame === REST_FRAME ? state : { ...state, frame: REST_FRAME };
|
|
81
|
-
return play(now, rng);
|
|
97
|
+
return play(now, rng, gap);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
function play(now: number, rng: () => number): ExpressionState {
|
|
100
|
+
function play(now: number, rng: () => number, gap: ExpressionGap): ExpressionState {
|
|
85
101
|
const blink = pickEvent(rng) === "blink";
|
|
86
102
|
const until = now + (blink ? BLINK_MS : EMOTE_MS);
|
|
87
|
-
return { nextAt: until + nextGap(rng), until, startedAt: now, frame: blink ? BLINK_FRAME : EMOTE_FRAME };
|
|
103
|
+
return { nextAt: until + nextGap(rng, gap), until, startedAt: now, frame: blink ? BLINK_FRAME : EMOTE_FRAME };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One sub-step of a playing expression; the oracle's blink and glance step through these. */
|
|
107
|
+
export const PHASE_MS = FAST_TICK_MS;
|
|
108
|
+
|
|
109
|
+
/** Sub-steps elapsed in the playing expression, 0 while resting or on a bad clock. */
|
|
110
|
+
export function expressionPhase(state: ExpressionState, now: number): number {
|
|
111
|
+
if (!isPlaying(state, now)) return 0;
|
|
112
|
+
const elapsed = now - state.startedAt;
|
|
113
|
+
return Number.isFinite(elapsed) && elapsed > 0 ? Math.floor(elapsed / PHASE_MS) : 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** How long the oracle lip-syncs after it says something new, and one mouth shape's hold. */
|
|
117
|
+
export const TALK_MS = 1800;
|
|
118
|
+
export const TALK_STEP_MS = FAST_TICK_MS;
|
|
119
|
+
|
|
120
|
+
/** Mouth-shape index while the oracle is still talking about what it said at `since`, else undefined. */
|
|
121
|
+
export function talkFrame(since: number | undefined, now: number): number | undefined {
|
|
122
|
+
if (since === undefined || !Number.isFinite(since) || !Number.isFinite(now)) return undefined;
|
|
123
|
+
const elapsed = now - since;
|
|
124
|
+
return elapsed >= 0 && elapsed < TALK_MS ? Math.floor(elapsed / TALK_STEP_MS) : undefined;
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
/** Emote frame for `now`: one step per EMOTE_STEP_MS, clamped to the last frame. */
|
package/src/pi/mascot-art.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { TaskState } from "../schemas/task.ts";
|
|
|
6
6
|
* that `zen.ts` and `zen-large.ts` compose. Every glyph is exactly one visible
|
|
7
7
|
* column so the art stays narrow/ambiguous-width safe: the non-ASCII glyphs are
|
|
8
8
|
* the lobby `⌂` in `BANNER_NARROW`, the panel's ◐/✓/✗ status family, and the
|
|
9
|
-
* box-drawing, bar and
|
|
9
|
+
* box-drawing, bar, eye and aura glyphs of the large scene (─ │ ┌ ┐ └ ┘ ╭ ╮ ╰ ╯ ╲ ╱ ╶ ├ ┤ ┬ ┴ █ ▓ ▒ ░ ▁-▇ ◉ ◍ ◎ ◌ ○ ◓ ◒ ✦ ✧).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -105,35 +105,147 @@ export const ORACLE_COLORS: Record<OraclePose, { color: PanelColor; bold: boolea
|
|
|
105
105
|
dormant: { color: "dim", bold: false },
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
/** One oracle frame: the
|
|
108
|
+
/** One oracle frame: the crown orb, both pupils and the seven-column mouth. */
|
|
109
109
|
export interface OracleFrame {
|
|
110
110
|
orb: string;
|
|
111
|
+
/**
|
|
112
|
+
* One-column pupil drawn inside its three-column eye window: `OPEN_EYE` takes
|
|
113
|
+
* the gaze's pupil, `CLOSED_EYE` fills the window as a lid, anything else is drawn as is.
|
|
114
|
+
*/
|
|
111
115
|
winL: string;
|
|
112
116
|
winR: string;
|
|
113
117
|
/** Exactly seven one-column glyphs; never moves the tower geometry. */
|
|
114
118
|
mouth: string;
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/** Oracle expression frame indexes, matching the shared schedule (see expressions.ts). */
|
|
122
|
+
export const ORACLE_REST = 0;
|
|
123
|
+
export const ORACLE_BLINK = 1;
|
|
124
|
+
/** Both emote frames are the look-around; the second only dims the orb. */
|
|
125
|
+
export const ORACLE_LOOK = 2;
|
|
126
|
+
|
|
127
|
+
/** An open eye: its pupil comes from where the oracle is looking. */
|
|
128
|
+
export const OPEN_EYE = "◉";
|
|
129
|
+
/** A closed eye: the lid fills the whole window. */
|
|
130
|
+
export const CLOSED_EYE = "─";
|
|
131
|
+
/** A half-closed lid, mid-blink or half-asleep. */
|
|
132
|
+
export const HALF_LID = "◒";
|
|
133
|
+
/** Inner width of each eye window; the pupil moves across it. */
|
|
134
|
+
export const EYE_WIDTH = 3;
|
|
135
|
+
|
|
136
|
+
/** The oracle's straight, serious mouth: it never idles on a smile. */
|
|
137
|
+
export const ORACLE_MOUTH = " ─── ";
|
|
138
|
+
|
|
117
139
|
/**
|
|
118
|
-
* Indexed oracle expressions per pose:
|
|
119
|
-
*
|
|
120
|
-
*
|
|
140
|
+
* Indexed oracle expressions per pose: rest, blink, then the two emote steps.
|
|
141
|
+
* Orchestrating, the oracle keeps a straight face and its emotes are a look
|
|
142
|
+
* around the room; dormant, it sleeps, peeks one eye open and snores (o, then O).
|
|
143
|
+
* While it talks the mouth lip-syncs instead.
|
|
121
144
|
*/
|
|
122
145
|
export const ORACLE_FRAMES: Record<OraclePose, readonly OracleFrame[]> = {
|
|
123
146
|
orchestrating: [
|
|
124
|
-
{ orb: "◉", winL:
|
|
125
|
-
{ orb: "◉", winL:
|
|
126
|
-
{ orb: "◎", winL:
|
|
127
|
-
{ orb: "◍", winL:
|
|
147
|
+
{ orb: "◉", winL: OPEN_EYE, winR: OPEN_EYE, mouth: ORACLE_MOUTH },
|
|
148
|
+
{ orb: "◉", winL: CLOSED_EYE, winR: CLOSED_EYE, mouth: ORACLE_MOUTH },
|
|
149
|
+
{ orb: "◎", winL: OPEN_EYE, winR: OPEN_EYE, mouth: ORACLE_MOUTH },
|
|
150
|
+
{ orb: "◍", winL: OPEN_EYE, winR: OPEN_EYE, mouth: ORACLE_MOUTH },
|
|
128
151
|
],
|
|
129
152
|
dormant: [
|
|
130
|
-
{ orb: "◌", winL:
|
|
131
|
-
{ orb: "◌", winL:
|
|
132
|
-
{ orb: "○", winL:
|
|
133
|
-
{ orb: "○", winL:
|
|
153
|
+
{ orb: "◌", winL: CLOSED_EYE, winR: CLOSED_EYE, mouth: ORACLE_MOUTH },
|
|
154
|
+
{ orb: "◌", winL: HALF_LID, winR: CLOSED_EYE, mouth: ORACLE_MOUTH },
|
|
155
|
+
{ orb: "○", winL: CLOSED_EYE, winR: CLOSED_EYE, mouth: " ─o─ " },
|
|
156
|
+
{ orb: "○", winL: CLOSED_EYE, winR: CLOSED_EYE, mouth: " ─O─ " },
|
|
134
157
|
],
|
|
135
158
|
};
|
|
136
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Lid positions stepped through by a blink (one per `PHASE_MS`, the last held):
|
|
162
|
+
* half-lidded, shut, shut, half-lidded. Dormant, the "blink" is a sleepy peek:
|
|
163
|
+
* the left eye cracks open, looks, and drifts shut again.
|
|
164
|
+
*/
|
|
165
|
+
export const ORACLE_BLINKS: Record<OraclePose, readonly (readonly [string, string])[]> = {
|
|
166
|
+
orchestrating: [[HALF_LID, HALF_LID], [CLOSED_EYE, CLOSED_EYE], [CLOSED_EYE, CLOSED_EYE], [HALF_LID, HALF_LID]],
|
|
167
|
+
dormant: [[HALF_LID, CLOSED_EYE], [OPEN_EYE, CLOSED_EYE], [OPEN_EYE, CLOSED_EYE], [OPEN_EYE, CLOSED_EYE], [HALF_LID, CLOSED_EYE], [CLOSED_EYE, CLOSED_EYE]],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Where the pupils look. `x` moves the pupil across its three-column window
|
|
172
|
+
* (left, centre, right); `y` swaps its glyph, since the window is one row:
|
|
173
|
+
* `◓` looks up, `◉` straight ahead, `◒` down.
|
|
174
|
+
*/
|
|
175
|
+
export interface Gaze {
|
|
176
|
+
x: -1 | 0 | 1;
|
|
177
|
+
y: -1 | 0 | 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const GAZE_AHEAD: Gaze = { x: 0, y: 0 };
|
|
181
|
+
|
|
182
|
+
/** The pupil glyph for each vertical gaze. */
|
|
183
|
+
export const ORACLE_PUPILS: Record<Gaze["y"], string> = { [-1]: "◓", 0: "◉", 1: "◒" };
|
|
184
|
+
|
|
185
|
+
const look = (x: Gaze["x"], y: Gaze["y"]): Gaze => ({ x, y });
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The look-around emote, one step per `PHASE_MS` across both emote frames
|
|
189
|
+
* (~2 s): a slow scan left, up across the ceiling, right, down at the agents
|
|
190
|
+
* and back to you.
|
|
191
|
+
*/
|
|
192
|
+
export const ORACLE_LOOK_PATH: readonly Gaze[] = [
|
|
193
|
+
look(0, 0), look(-1, 0), look(-1, 0), look(-1, 0), look(-1, -1), look(-1, -1),
|
|
194
|
+
look(0, -1), look(1, -1), look(1, -1), look(1, 0), look(1, 0), look(1, 0),
|
|
195
|
+
look(1, 1), look(0, 1), look(0, 1), look(-1, 1), look(0, 0), look(0, 0),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Idle wandering when nothing holds the oracle's attention: it keeps returning
|
|
200
|
+
* to you between glances around the room. Starts looking ahead.
|
|
201
|
+
*/
|
|
202
|
+
export const ORACLE_WANDER: readonly Gaze[] = [
|
|
203
|
+
look(0, 0), look(-1, 0), look(0, 0), look(1, -1), look(1, 0), look(0, 0),
|
|
204
|
+
look(-1, 1), look(0, -1), look(0, 0), look(1, 1), look(-1, -1), look(0, 0),
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
/** Ticks the eyes hold one wandering spot (~2.5 s while work is live). */
|
|
208
|
+
export const ORACLE_WANDER_TICKS = 10;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Lip-sync while the oracle talks: a voice waveform that swells and falls, with
|
|
212
|
+
* a closed-mouth breath so it reads as phrases rather than a buzz.
|
|
213
|
+
*/
|
|
214
|
+
export const ORACLE_TALK: readonly string[] = [
|
|
215
|
+
" ▁▃▅▃▁ ",
|
|
216
|
+
" ▂▆▂ ",
|
|
217
|
+
" ▂▄▇▄▂ ",
|
|
218
|
+
" ▁▂▁ ",
|
|
219
|
+
" ▃▅▂▅▃ ",
|
|
220
|
+
" ▁▂▄▂▁ ",
|
|
221
|
+
" ▁ ",
|
|
222
|
+
ORACLE_MOUTH,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
/** The oracle's resting mood: steady by default, worried when the task is blocked. */
|
|
226
|
+
export type OracleMood = "steady" | "worried";
|
|
227
|
+
|
|
228
|
+
/** The resting mouth per mood: straight, or a tight frown when blocked. */
|
|
229
|
+
export const ORACLE_MOOD_MOUTHS: Record<OracleMood, string> = {
|
|
230
|
+
steady: ORACLE_MOUTH,
|
|
231
|
+
worried: " ╭─╮ ",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Ticks the eyes hold on one working agent before glancing to the next. */
|
|
235
|
+
export const ORACLE_GAZE_TICKS = 8;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The seven-column aura above the crown, stepped by the spinner tick: sparkles
|
|
239
|
+
* twinkle while the oracle orchestrates, and z's drift up while it sleeps.
|
|
240
|
+
*/
|
|
241
|
+
export const ORACLE_AURA: Record<OraclePose, readonly string[]> = {
|
|
242
|
+
orchestrating: ["· ✦ ·", " ✧ · ✧ ", "✦ · ✦", " · ✧ · "],
|
|
243
|
+
dormant: [" z ", " z ", " z Z ", " Z "],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Ticks each aura frame holds: a calm twinkle rather than the spinner's pace. */
|
|
247
|
+
export const ORACLE_AURA_TICKS = 2;
|
|
248
|
+
|
|
137
249
|
/**
|
|
138
250
|
* One frame per status and expression: index 0 is the calm rest face, 1 the
|
|
139
251
|
* blink and 2-3 the emotes. Every status keeps the same frame count and one row
|
|
@@ -209,13 +321,15 @@ export const COMPACT_FRAMES: Record<SlotId, Record<SlotState, readonly string[]>
|
|
|
209
321
|
|
|
210
322
|
/**
|
|
211
323
|
* The oracle tower: 13 columns wide, `rows` while height allows and `smallRows`
|
|
212
|
-
* when it must shrink. `{
|
|
213
|
-
*
|
|
324
|
+
* when it must shrink. `{aura}` twinkles over the radiant `{orb}` crown,
|
|
325
|
+
* `{winL}`/`{winR}` are the three-column eye windows the pupils move in,
|
|
326
|
+
* `{mouth}` speaks and `{door}` is the name over the door. The speech bubble
|
|
327
|
+
* hangs beside the crown.
|
|
214
328
|
*/
|
|
215
329
|
export interface TowerSpec {
|
|
216
330
|
rows: readonly string[];
|
|
217
331
|
smallRows: readonly string[];
|
|
218
|
-
tokens: { orb: string; winL: string; winR: string; door: string; mouth: string };
|
|
332
|
+
tokens: { aura: string; orb: string; winL: string; winR: string; door: string; mouth: string };
|
|
219
333
|
}
|
|
220
334
|
|
|
221
335
|
export const TOWER_WIDTH = 13;
|
|
@@ -224,29 +338,32 @@ export const TOWER_DOOR = "ORC";
|
|
|
224
338
|
|
|
225
339
|
export const TOWER: TowerSpec = {
|
|
226
340
|
rows: [
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
341
|
+
" {aura} ",
|
|
342
|
+
" ╲ │ ╱ ",
|
|
343
|
+
" ──({orb})── ",
|
|
344
|
+
" ╱ │ ╲ ",
|
|
230
345
|
" │ ",
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"│ │{winL}│ │{winR}│ │",
|
|
236
|
-
"│ └─┘ └─┘ │",
|
|
237
|
-
"├───────────┤",
|
|
346
|
+
"╭─────┴─────╮",
|
|
347
|
+
"│╭───╮ ╭───╮│",
|
|
348
|
+
"││{winL}│ │{winR}││",
|
|
349
|
+
"│╰───╯ ╰───╯│",
|
|
238
350
|
"│ {mouth} │",
|
|
239
351
|
"├───┬───┬───┤",
|
|
240
|
-
"
|
|
352
|
+
"│░▒▓│{door}│▓▒░│",
|
|
241
353
|
"└───┴───┴───┘",
|
|
242
354
|
],
|
|
243
355
|
smallRows: [
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"│
|
|
247
|
-
"
|
|
248
|
-
"│▓▓▓│{door}│▓▓▓│",
|
|
356
|
+
" ──({orb})── ",
|
|
357
|
+
"╭─────┴─────╮",
|
|
358
|
+
"││{winL}│ │{winR}││",
|
|
359
|
+
"│░▒▓│{door}│▓▒░│",
|
|
249
360
|
"└───┴───┴───┘",
|
|
250
361
|
],
|
|
251
|
-
tokens: { orb: "{orb}", winL: "{winL}", winR: "{winR}", door: "{door}", mouth: "{mouth}" },
|
|
362
|
+
tokens: { aura: "{aura}", orb: "{orb}", winL: "{winL}", winR: "{winR}", door: "{door}", mouth: "{mouth}" },
|
|
252
363
|
};
|
|
364
|
+
|
|
365
|
+
/** The oracle's speech bubble: outer width including borders, and its text rows. */
|
|
366
|
+
export const BUBBLE = { width: 24, lines: 2 } as const;
|
|
367
|
+
|
|
368
|
+
/** What the oracle says while the master waits on the user between turns. */
|
|
369
|
+
export const ORACLE_IDLE_WORD = "your turn";
|
package/src/pi/tools.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ProcessRunner } from "../execution/pi-runner.ts";
|
|
|
7
7
|
import { inheritThinking } from "../schemas/configuration.ts";
|
|
8
8
|
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
|
-
import { applyStatus, summarizeRun } from "./ui.ts";
|
|
10
|
+
import { applyStatus, reportRuns, summarizeRun } from "./ui.ts";
|
|
11
11
|
import { isQuiet } from "./quiet.ts";
|
|
12
12
|
import {
|
|
13
13
|
ORCHESTRATE_ACTIONS,
|
|
@@ -128,7 +128,7 @@ export function registerOrchestrateTool(pi: ExtensionAPI, configDir: string, run
|
|
|
128
128
|
renderShell: "self",
|
|
129
129
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
130
130
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
131
|
-
const deps = workflowDeps(ctx, configDir, signal, runReporter(onUpdate, (runs) =>
|
|
131
|
+
const deps = workflowDeps(ctx, configDir, signal, runReporter(onUpdate, (runs) => reportRuns(ctx, root, configDir, runs)), runProcess, pi.getThinkingLevel());
|
|
132
132
|
const result = await runWorkflowAction(params as OrchestrateParams, deps);
|
|
133
133
|
if (params.action === "propose" && params.proposal && result.ok) {
|
|
134
134
|
pi.appendEntry("bot-lobby", { kind: "proposal", taskId: params.taskId, text: params.proposal });
|