@a-t-h-i/bot-lobby 0.6.5 → 0.6.7
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 +127 -6
- package/package.json +1 -1
- package/prompts/global.md +18 -0
- package/prompts/master.md +77 -2
- package/prompts/worker.md +7 -2
- package/src/ask/dialog.ts +15 -3
- package/src/ask/state.ts +10 -1
- package/src/ask/tool.ts +6 -1
- package/src/ask/view.ts +2 -1
- package/src/execution/agent-runner.ts +23 -1
- package/src/execution/fallback.ts +75 -0
- package/src/index.ts +1 -1
- package/src/lobby/feed.ts +8 -1
- package/src/lobby/keys.ts +0 -1
- package/src/lobby/layout.ts +22 -1
- package/src/lobby/mini.ts +159 -0
- package/src/lobby/planner.ts +10 -3
- package/src/lobby/quickfix.ts +17 -3
- package/src/lobby/runtime.ts +29 -26
- package/src/lobby/tabs/home.ts +6 -42
- package/src/lobby/tabs/tasks.ts +1 -1
- package/src/lobby/view.ts +31 -30
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +1 -0
- package/src/pi/activity.ts +1 -33
- package/src/pi/events.ts +3 -16
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +18 -3
- package/src/pi/plan-checklist.ts +305 -0
- package/src/pi/run-summary.ts +12 -3
- package/src/pi/settings-ui.ts +65 -12
- package/src/pi/tools.ts +5 -4
- package/src/pi/ui.ts +18 -284
- package/src/roles/worker.ts +1 -1
- package/src/schemas/configuration.ts +51 -15
- package/src/schemas/findings.ts +2 -0
- package/src/workflow/brief.ts +59 -0
- package/src/workflow/workflow.ts +15 -2
- package/src/pi/expressions.ts +0 -169
- package/src/pi/kaomoji.ts +0 -227
- package/src/pi/mascot-art.ts +0 -359
- package/src/pi/zen-large.ts +0 -699
- package/src/pi/zen-metrics.ts +0 -130
- package/src/pi/zen.ts +0 -659
package/src/workflow/workflow.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { assessReconnaissance, completionBlockers, decideReviewLoop, recordDecis
|
|
|
49
49
|
import { detectSharedFiles, summarizeOutcomes } from "../master/synthesis.ts";
|
|
50
50
|
import { tail, truncate } from "../text.ts";
|
|
51
51
|
import { isAutoMode } from "../state/auto.ts";
|
|
52
|
+
import { briefRejection } from "./brief.ts";
|
|
52
53
|
import { assertNoPendingApprovals, pendingApprovals, requestApproval, resolveApproval } from "./approvals.ts";
|
|
53
54
|
import { pingApproval } from "../pi/notify.ts";
|
|
54
55
|
import { describeRun, runLogEntry } from "../pi/run-summary.ts";
|
|
@@ -295,7 +296,7 @@ async function handleClarify(task: Task, params: OrchestrateParams, deps: Workfl
|
|
|
295
296
|
}
|
|
296
297
|
const answer = params.options?.length ? await deps.choose(question, params.options) : await deps.ask(question);
|
|
297
298
|
if (answer === undefined) {
|
|
298
|
-
return `
|
|
299
|
+
return `The user did not answer. Do not assume an answer or pick the recommended option: ask the user this again in your reply, end your turn, and continue only once they have answered.\n\nQuestion: ${question}`;
|
|
299
300
|
}
|
|
300
301
|
return `User answered: ${answer}`;
|
|
301
302
|
}
|
|
@@ -680,6 +681,10 @@ function workerRequest(deps: WorkflowDeps, task: Task, domain: Domain, instructi
|
|
|
680
681
|
};
|
|
681
682
|
}
|
|
682
683
|
|
|
684
|
+
const ANSWER_NOW = "Answer the questions now";
|
|
685
|
+
/** Times an agent's abandoned questions are put to the user again before it decides. */
|
|
686
|
+
const MAX_NUDGES = 5;
|
|
687
|
+
|
|
683
688
|
/** Agents that may ask the user themselves: the designer, whose choices are the user's to see. */
|
|
684
689
|
const ASKING_DOMAINS: ReadonlySet<Domain> = new Set(["designer"]);
|
|
685
690
|
|
|
@@ -705,7 +710,13 @@ function askRelay(task: Task, deps: WorkflowDeps, domain: Domain): { onAsk: Rela
|
|
|
705
710
|
if (isAutoMode(deps.root, deps.configDir, task.id)) {
|
|
706
711
|
return { answers: [], cancelled: true, globalNote: "Auto mode is on, so nobody can answer: decide with the options you recommend and say in your report what you chose and why." };
|
|
707
712
|
}
|
|
708
|
-
|
|
713
|
+
let result = await askQuestions(questions, label, signal);
|
|
714
|
+
// Leaving the questions must not quietly hand the choice to the agent: ask whether it may decide.
|
|
715
|
+
for (let nudges = 0; result.answers.length === 0 && result.cancelled && !signal.aborted && nudges < MAX_NUDGES; nudges++) {
|
|
716
|
+
const choice = await deps.choose(`${label} is waiting for your answer${questions.length === 1 ? "" : "s"} (${questions.map((question) => question.header).join(", ")}).`, [ANSWER_NOW, `Let ${label} decide with its recommendation`]);
|
|
717
|
+
if (choice !== ANSWER_NOW && choice !== undefined) break;
|
|
718
|
+
result = await askQuestions(questions, label, signal);
|
|
719
|
+
}
|
|
709
720
|
recordDecision(task, askedDecision(label, questions, result), domain);
|
|
710
721
|
return result;
|
|
711
722
|
});
|
|
@@ -877,6 +888,8 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
877
888
|
}
|
|
878
889
|
if (!starting) requireState(task, ["planning", "implementing", "reviewing"]);
|
|
879
890
|
const assignments = parseAssignments(params);
|
|
891
|
+
const short = !deps.config.workflow.briefCheck ? [] : assignments.map((entry) => briefRejection(task.id, entry.domain, entry.instruction)).filter(Boolean);
|
|
892
|
+
if (short.length > 0) throw new Error(short.join("\n"));
|
|
880
893
|
for (const { domain } of assignments) assertNoPendingApprovals(task, domain);
|
|
881
894
|
for (const { domain } of assignments) if (!task.domains.includes(domain)) task.domains.push(domain);
|
|
882
895
|
// Under a time budget every step is given its share before any starts; a spent budget starts none.
|
package/src/pi/expressions.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Caller-driven expression schedule for the zen sprites.
|
|
3
|
-
*
|
|
4
|
-
* Pure and deterministic: `now` and the random source are always injected, so
|
|
5
|
-
* the art, layout and metrics stay free of `Date.now`/`Math.random`. Each slot
|
|
6
|
-
* and the oracle keeps its own state, holds its rest frame between events and,
|
|
7
|
-
* after a random 20-30 s gap, plays a short blink or a longer emote.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Resting gap before the next expression, milliseconds. */
|
|
11
|
-
export const BLINK_MIN_MS = 20_000;
|
|
12
|
-
export const BLINK_MAX_MS = 30_000;
|
|
13
|
-
/** How long one blink and one emote are held. */
|
|
14
|
-
export const BLINK_MS = 500;
|
|
15
|
-
export const EMOTE_MS = 2000;
|
|
16
|
-
/** Fast clock while an expression plays, short enough that a blink is never skipped. */
|
|
17
|
-
export const FAST_TICK_MS = 120;
|
|
18
|
-
|
|
19
|
-
/** Art frame indexes: 0 rests, 1 blinks, `2 ..` are emotes. */
|
|
20
|
-
export const REST_FRAME = 0;
|
|
21
|
-
export const BLINK_FRAME = 1;
|
|
22
|
-
export const EMOTE_FRAME = 2;
|
|
23
|
-
/**
|
|
24
|
-
* Emote frame range: `EMOTE_FRAME .. EMOTE_FRAME + EMOTE_FRAMES - 1`; an emote
|
|
25
|
-
* steps through them: the open face, a blink, then its action (see kaomoji.ts).
|
|
26
|
-
*/
|
|
27
|
-
export const EMOTE_FRAMES = 4;
|
|
28
|
-
/** Each emote frame is held this long, so an emote steps through its frames. */
|
|
29
|
-
export const EMOTE_STEP_MS = Math.floor(EMOTE_MS / EMOTE_FRAMES);
|
|
30
|
-
|
|
31
|
-
const BLINK_CHANCE = 0.65;
|
|
32
|
-
/** Working agents emote as often as they blink. */
|
|
33
|
-
export const WORKING_BLINK_CHANCE = 0.5;
|
|
34
|
-
/** Upper bound (exclusive) of an expression's variant. */
|
|
35
|
-
const VARIANT_SPAN = 2 ** 30;
|
|
36
|
-
|
|
37
|
-
export interface ExpressionState {
|
|
38
|
-
/** Earliest `now` the next expression may start. */
|
|
39
|
-
nextAt: number;
|
|
40
|
-
/** When the playing expression ends; not after `now` while resting. */
|
|
41
|
-
until: number;
|
|
42
|
-
/** When the current expression started; drives emote frame stepping. */
|
|
43
|
-
startedAt: number;
|
|
44
|
-
/** Frame index for the art: 0 rest, 1 blink, 2+ emote. */
|
|
45
|
-
frame: number;
|
|
46
|
-
/** Drawn once per expression; picks which face an emote shows (see kaomoji.ts). */
|
|
47
|
-
variant: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** A random draw clamped to `[0, 1]`, so a non-finite source value can never leak. */
|
|
51
|
-
function unit(rng: () => number): number {
|
|
52
|
-
const value = rng();
|
|
53
|
-
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Resting window between expressions, milliseconds. */
|
|
57
|
-
export interface ExpressionGap {
|
|
58
|
-
min: number;
|
|
59
|
-
max: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Idle agent slots rest 20-30 s between expressions. */
|
|
63
|
-
export const SLOT_GAP: ExpressionGap = { min: BLINK_MIN_MS, max: BLINK_MAX_MS };
|
|
64
|
-
/** Working agents are livelier: 8-15 s between expressions. */
|
|
65
|
-
export const WORKING_GAP: ExpressionGap = { min: 8_000, max: 15_000 };
|
|
66
|
-
/** The oracle is the scene's centrepiece: it blinks and emotes every 6-12 s. */
|
|
67
|
-
export const ORACLE_GAP: ExpressionGap = { min: 6_000, max: 12_000 };
|
|
68
|
-
|
|
69
|
-
/** Random resting gap before the next expression, inside `gap` (the slot window by default). */
|
|
70
|
-
export function nextGap(rng: () => number, gap: ExpressionGap = SLOT_GAP): number {
|
|
71
|
-
return gap.min + Math.round(unit(rng) * (gap.max - gap.min));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Whether the next expression blinks (~65% by default) or emotes. */
|
|
75
|
-
export function pickEvent(rng: () => number, blinkChance = BLINK_CHANCE): "blink" | "emote" {
|
|
76
|
-
return unit(rng) < blinkChance ? "blink" : "emote";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function drawVariant(rng: () => number): number {
|
|
80
|
-
return Math.floor(unit(rng) * (VARIANT_SPAN - 1));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** A freshly rested expression that fires for the first time after one random gap. */
|
|
84
|
-
export function createExpression(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
|
|
85
|
-
const start = Number.isFinite(now) ? now : 0;
|
|
86
|
-
return { nextAt: start + nextGap(rng, gap), until: start, startedAt: start, frame: REST_FRAME, variant: 0 };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** True while `now` is inside a playing blink or emote. */
|
|
90
|
-
export function isPlaying(state: ExpressionState, now: number): boolean {
|
|
91
|
-
return Number.isFinite(now) && now < state.until;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** True when any state is mid-expression; the caller uses it to retime its clock. */
|
|
95
|
-
export function anyPlaying(states: readonly ExpressionState[], now: number): boolean {
|
|
96
|
-
return states.some((state) => isPlaying(state, now));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Advance one sprite to `now`: start a blink or emote once the gap has elapsed,
|
|
101
|
-
* drop back to the rest frame when it ends, and change nothing in between.
|
|
102
|
-
*/
|
|
103
|
-
export function advanceExpression(
|
|
104
|
-
state: ExpressionState,
|
|
105
|
-
now: number,
|
|
106
|
-
rng: () => number,
|
|
107
|
-
gap: ExpressionGap = SLOT_GAP,
|
|
108
|
-
blinkChance = BLINK_CHANCE,
|
|
109
|
-
): ExpressionState {
|
|
110
|
-
if (!Number.isFinite(now)) return state;
|
|
111
|
-
if (now < state.until) return state.frame >= EMOTE_FRAME ? steppedEmote(state, now) : state;
|
|
112
|
-
if (now < state.nextAt) return state.frame === REST_FRAME ? state : { ...state, frame: REST_FRAME };
|
|
113
|
-
return play(now, rng, gap, blinkChance);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function play(now: number, rng: () => number, gap: ExpressionGap, blinkChance: number): ExpressionState {
|
|
117
|
-
const blink = pickEvent(rng, blinkChance) === "blink";
|
|
118
|
-
return blink ? startBlink(now, rng, gap) : triggerEmote(now, rng, gap);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function startBlink(now: number, rng: () => number, gap: ExpressionGap): ExpressionState {
|
|
122
|
-
const until = now + BLINK_MS;
|
|
123
|
-
return { nextAt: until + nextGap(rng, gap), until, startedAt: now, frame: BLINK_FRAME, variant: drawVariant(rng) };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Start an emote right now, whatever the sprite was doing: the reaction when a
|
|
128
|
-
* slot's situation changes (it starts, finishes, fails, stalls, waits or
|
|
129
|
-
* receives a file). The next scheduled expression follows one gap later.
|
|
130
|
-
*/
|
|
131
|
-
export function triggerEmote(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
|
|
132
|
-
const start = Number.isFinite(now) ? now : 0;
|
|
133
|
-
const until = start + EMOTE_MS;
|
|
134
|
-
return { nextAt: until + nextGap(rng, gap), until, startedAt: start, frame: EMOTE_FRAME, variant: drawVariant(rng) };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** One sub-step of a playing expression; the oracle's blink and glance step through these. */
|
|
138
|
-
export const PHASE_MS = FAST_TICK_MS;
|
|
139
|
-
|
|
140
|
-
/** Sub-steps elapsed in the playing expression, 0 while resting or on a bad clock. */
|
|
141
|
-
export function expressionPhase(state: ExpressionState, now: number): number {
|
|
142
|
-
if (!isPlaying(state, now)) return 0;
|
|
143
|
-
const elapsed = now - state.startedAt;
|
|
144
|
-
return Number.isFinite(elapsed) && elapsed > 0 ? Math.floor(elapsed / PHASE_MS) : 0;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** How long the oracle lip-syncs after it says something new, and one mouth shape's hold. */
|
|
148
|
-
export const TALK_MS = 1800;
|
|
149
|
-
export const TALK_STEP_MS = FAST_TICK_MS;
|
|
150
|
-
|
|
151
|
-
/** Mouth-shape index while the oracle is still talking about what it said at `since`, else undefined. */
|
|
152
|
-
export function talkFrame(since: number | undefined, now: number): number | undefined {
|
|
153
|
-
if (since === undefined || !Number.isFinite(since) || !Number.isFinite(now)) return undefined;
|
|
154
|
-
const elapsed = now - since;
|
|
155
|
-
return elapsed >= 0 && elapsed < TALK_MS ? Math.floor(elapsed / TALK_STEP_MS) : undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Emote frame for `now`: one step per EMOTE_STEP_MS, clamped to the last frame. */
|
|
159
|
-
function emoteFrameAt(startedAt: number, now: number): number {
|
|
160
|
-
const elapsed = now - startedAt;
|
|
161
|
-
const step = Number.isFinite(elapsed) && elapsed > 0 ? Math.floor(elapsed / EMOTE_STEP_MS) : 0;
|
|
162
|
-
return EMOTE_FRAME + Math.min(EMOTE_FRAMES - 1, Math.max(0, step));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Advance an in-flight emote's frame, keeping the same object when it has not changed. */
|
|
166
|
-
function steppedEmote(state: ExpressionState, now: number): ExpressionState {
|
|
167
|
-
const frame = emoteFrameAt(state.startedAt, now);
|
|
168
|
-
return frame === state.frame ? state : { ...state, frame };
|
|
169
|
-
}
|
package/src/pi/kaomoji.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The agents' kaomoji: a pure catalogue of emotes and the rules that pick one.
|
|
3
|
-
*
|
|
4
|
-
* Every emote is a tiny animation: the open face, the same face blinking (same
|
|
5
|
-
* width, so nothing shifts), and an optional action frame (a table flip, a
|
|
6
|
-
* sparkle, a bow). Each agent has its own personality set for every emotion,
|
|
7
|
-
* and a shared pool of emotions is open to all of them:
|
|
8
|
-
*
|
|
9
|
-
* - DEV, the hacker: shades, flexing, coffee, and table flips on failure
|
|
10
|
-
* - DESIGN, the artist: sparkles, flowers, hearts and little flourishes
|
|
11
|
-
* - RESEARCH, the bookworm: note-taking, pointing, shrugging, deal-with-it glasses
|
|
12
|
-
* - QA, the inspector: side-eyes and squints, then a flex and a victory dance
|
|
13
|
-
*
|
|
14
|
-
* Nothing here reads a clock or a random source: the caller passes a `variant`
|
|
15
|
-
* drawn once per expression, so the same variant always picks the same face.
|
|
16
|
-
* Every face fits a slot cell and avoids combining marks, right-to-left
|
|
17
|
-
* scripts and emoji-presentation glyphs (enforced by tests).
|
|
18
|
-
*/
|
|
19
|
-
import type { SlotId, SlotState } from "./mascot-art.ts";
|
|
20
|
-
|
|
21
|
-
export const EMOTIONS = [
|
|
22
|
-
"happy",
|
|
23
|
-
"proud",
|
|
24
|
-
"love",
|
|
25
|
-
"excited",
|
|
26
|
-
"focused",
|
|
27
|
-
"curious",
|
|
28
|
-
"thinking",
|
|
29
|
-
"nervous",
|
|
30
|
-
"confused",
|
|
31
|
-
"sleepy",
|
|
32
|
-
"sad",
|
|
33
|
-
"angry",
|
|
34
|
-
"waiting",
|
|
35
|
-
"surprised",
|
|
36
|
-
"grateful",
|
|
37
|
-
] as const;
|
|
38
|
-
export type Emotion = (typeof EMOTIONS)[number];
|
|
39
|
-
|
|
40
|
-
/** One emote: the open face, the same face blinking, and an optional action frame. */
|
|
41
|
-
export interface Emote {
|
|
42
|
-
open: string;
|
|
43
|
-
blink: string;
|
|
44
|
-
action?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const e = (open: string, blink: string, action?: string): Emote => (action ? { open, blink, action } : { open, blink });
|
|
48
|
-
|
|
49
|
-
/** Emotions every agent can draw on. */
|
|
50
|
-
export const SHARED_EMOTES: Record<Emotion, readonly Emote[]> = {
|
|
51
|
-
happy: [e("(^▽^)", "(-▽-)"), e("(^ω^)", "(-ω-)"), e("ヽ(•‿•)ノ", "ヽ(-‿-)ノ"), e("(*^‿^*)", "(*-‿-*)"), e("(◕‿◕)", "(-‿-)")],
|
|
52
|
-
proud: [e("( ̄^ ̄)ゞ", "(-^-)ゞ"), e("(´▽`)b", "(-▽-)b"), e("(  ̄ー ̄)", "( -ー-)"), e("(•ᴗ•)ง", "(-ᴗ-)ง")],
|
|
53
|
-
love: [e("(♡‿♡)", "(-‿-)", "(♡‿♡)♡"), e("(っ˘з(˘⌣˘ )", "(っ-з(-⌣- )"), e("(´ε` )♡", "(-ε- )♡"), e("(◍•ᴗ•◍)♡", "(◍-ᴗ-◍)♡")],
|
|
54
|
-
excited: [e("\\(^o^)/", "\\(-o-)/"), e("ヽ(^▽^)ノ", "ヽ(-▽-)ノ"), e("(ノ◕ヮ◕)ノ", "(ノ-ヮ-)ノ", "(ノ◕ヮ◕)ノ*:・゚✧"), e("o(≧▽≦)o", "o(-▽-)o")],
|
|
55
|
-
focused: [e("(•_•)", "(-_-)"), e("(ง •_•)ง", "(ง -_-)ง"), e("(`・ω・´)", "(`-ω-´)"), e("( ._.)φ", "( -_-)φ")],
|
|
56
|
-
curious: [e("(・・ )?", "(-- )?"), e("(⊙_⊙)?", "(-_-)?"), e("(◔_◔)", "(-_-)"), e("(゚∀゚)?", "(-∀-)?")],
|
|
57
|
-
thinking: [e("( ̄~ ̄;)", "(-~-;)"), e("( ˘_˘ )", "( -_- )"), e("(・・;)ゞ", "(--;)ゞ"), e("(¬‿¬ )", "(-‿- )")],
|
|
58
|
-
nervous: [e("(;・∀・)", "(;-∀-)"), e("(⊙﹏⊙)", "(-﹏-)"), e("(●´⌓`●)", "(●-⌓-●)"), e("(°□°;)", "(-□-;)")],
|
|
59
|
-
confused: [e("(・_・ヾ", "(-_-ヾ"), e("(⊙_☉)", "(-_-)"), e("(゜-゜)", "(---)"), e("(・・?)", "(--?)")],
|
|
60
|
-
sleepy: [e("(-_-) zzZ", "(-.-) zzZ"), e("( ̄o ̄) zzZZ", "(-o-) zzZZ"), e("(∪。∪)。zz", "(-。-)。zz"), e("( ˘ω˘ )スヤァ", "( -ω- )スヤァ")],
|
|
61
|
-
sad: [e("(ಥ_ಥ)", "(-_-)"), e("(╥﹏╥)", "(-﹏-)"), e("(´;ω;`)", "(-;ω;-)"), e("(〒﹏〒)", "(-﹏-)"), e("(T_T)", "(-_-)")],
|
|
62
|
-
angry: [e("(╬ಠ益ಠ)", "(╬-益-)"), e("(ノಠ益ಠ)ノ", "(ノ-益-)ノ", "(ノಠ益ಠ)ノ彡┻━┻"), e("ヽ(`Д´)ノ", "ヽ(-Д-)ノ"), e("(¬_¬\")", "(-_-\")")],
|
|
63
|
-
waiting: [e("(´・_・`)", "(´-_-`)"), e("( ˘・_・˘)", "( ˘-_-˘)"), e("(っ´ω`c)", "(っ-ω-c)"), e("(・_・)…", "(-_-)…")],
|
|
64
|
-
surprised: [e("(°o°)", "(-o-)"), e("Σ(°△°|||)", "Σ(-△-|||)"), e("(゚Д゚)", "(-Д-)"), e("w(°o°)w", "w(-o-)w")],
|
|
65
|
-
grateful: [e("(人´∀`)", "(人-∀-)"), e("(◍•ᴗ•◍)", "(◍-ᴗ-◍)"), e("m(_ _)m", "m(- -)m"), e("(ᵔᴥᵔ)", "(-ᴥ-)"), e("(*´∀`*)", "(*-∀-*)")],
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** DEV, the hacker: shades, flexes, coffee and code brackets; flips tables when things break. */
|
|
69
|
-
const DEV: Record<Emotion, readonly Emote[]> = {
|
|
70
|
-
happy: [e("(⌐■_■)", "(⌐-_-)"), e("(^_^)b", "(-_-)b"), e("</>(^▽^)", "</>(-▽-)"), e("( •_•)c[_]", "( -_-)c[_]")],
|
|
71
|
-
proud: [e("(⌐■_■)b", "(⌐-_-)b"), e("ᕦ(ò_óˇ)ᕤ", "ᕦ(-_-ˇ)ᕤ"), e("( •_•)>⌐■-■", "( -_-)>⌐■-■", "(⌐■_■)"), e("b( ̄▽ ̄)d", "b(-▽-)d")],
|
|
72
|
-
love: [e("(♡_♡)</>", "(-_-)</>"), e("(´∀`)♡{ }", "(-∀-)♡{ }"), e("(˘▽˘)c[_]", "(-▽-)c[_]"), e("(◍•_•◍)♡", "(◍-_-◍)♡")],
|
|
73
|
-
excited: [e("ᕦ(ò_ó)ᕤ", "ᕦ(-_-)ᕤ"), e("(ノ≧∀≦)ノ", "(ノ-∀-)ノ", "(ノ≧∀≦)ノ</>"), e("\\(•_•)/", "\\(-_-)/"), e("(⌐■▽■)", "(⌐-▽-)")],
|
|
74
|
-
focused: [e("(•_•)>_", "(-_-)>_"), e("[•_•]$_", "[-_-]$_"), e("(ง'-')ง", "(ง- -)ง"), e("(⌐■_■)>_", "(⌐-_-)>_")],
|
|
75
|
-
curious: [e("(•_•)?</>", "(-_-)?</>"), e("(・_・)ノ{ }", "(-_-)ノ{ }"), e("(°_°)>_", "(-_-)>_"), e("( ⊙_⊙)</>", "( -_-)</>")],
|
|
76
|
-
thinking: [e("( ̄_ ̄)c[_]", "(-_-)c[_]"), e("( •_•)…{ }", "( -_-)…{ }"), e("(¬_¬)>_", "(-_-)>_"), e("(°~°)</>", "(-~-)</>")],
|
|
77
|
-
nervous: [e("(°_°;)>_", "(-_-;)>_"), e("(・_・;)$_", "(-_-;)$_"), e("(⊙_⊙;)", "(-_-;)"), e("(゚_゚;)c[_]", "(-_-;)c[_]")],
|
|
78
|
-
confused: [e("(・_・)?{ }", "(-_-)?{ }"), e("(°_°)?>_", "(-_-)?>_"), e("(⊙_☉)</>", "(-_-)</>"), e("(゜_゜)$?", "(-_-)$?")],
|
|
79
|
-
sleepy: [e("(-_-)c[_] z", "(-.-)c[_] z"), e("( ̄o ̄)>_ zz", "(-o-)>_ zz"), e("(∪_∪)</> z", "(-_-)</> z"), e("( ˘_˘ )$_ z", "( -_- )$_ z")],
|
|
80
|
-
sad: [e("(╥_╥)>_", "(-_-)>_"), e("(T_T)</>", "(-_-)</>"), e("(ಥ_ಥ)c[_]", "(-_-)c[_]"), e("(´;_;`)$_", "(-;_;-)$_")],
|
|
81
|
-
angry: [e("(╯°□°)╯", "(╯-□-)╯", "(╯°□°)╯︵ ┻━┻"), e("(ノಠ益ಠ)ノ", "(ノ-益-)ノ", "(ノಠ益ಠ)ノ彡┻━┻"), e("(╬ò_ó)>_", "(╬-_-)>_"), e("┻━┻ ︵ (°□°)", "┻━┻ ︵ (-□-)")],
|
|
82
|
-
waiting: [e("(•_•)c[_]", "(-_-)c[_]"), e("(´・_・`)>_", "(´-_-`)>_"), e("( ˘_˘)</>…", "( -_-)</>…"), e("(・_・)$_…", "(-_-)$_…")],
|
|
83
|
-
surprised: [e("(°□°)>_", "(-□-)>_"), e("Σ(°_°)</>", "Σ(-_-)</>"), e("(゚Д゚)$_", "(-Д-)$_"), e("(⊙_⊙)!{ }", "(-_-)!{ }")],
|
|
84
|
-
grateful: [e("(⌐■_■)b", "(⌐-_-)b"), e("m(_ _)m</>", "m(- -)m</>"), e("(^_^)c[_]", "(-_-)c[_]"), e("(人•_•)", "(人-_-)")],
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/** DESIGN, the artist: sparkles, flowers, hearts and a little flourish on most things. */
|
|
88
|
-
const DESIGN: Record<Emotion, readonly Emote[]> = {
|
|
89
|
-
happy: [e("✧(◕‿◕✿)", "✧(-‿-✿)"), e("(✿^‿^)", "(✿-‿-)"), e("(◕ᴗ◕✿)", "(-ᴗ-✿)"), e("❀(^▽^)❀", "❀(-▽-)❀")],
|
|
90
|
-
proud: [e("(✿◠‿◠)", "(✿-‿-)"), e("✧(˘▽˘)✧", "✧(-▽-)✧"), e("( ̄▽ ̄)✿", "(-▽-)✿"), e("(◕‿◕)b✧", "(-‿-)b✧")],
|
|
91
|
-
love: [e("(♡‿♡)✿", "(-‿-)✿"), e("(っ˘з˘)♡✧", "(っ-з-)♡✧"), e("(◍♡‿♡◍)", "(◍-‿-◍)"), e("✿(´ε`)♡", "✿(-ε-)♡")],
|
|
92
|
-
excited: [e("(ノ◕ヮ◕)ノ", "(ノ-ヮ-)ノ", "(ノ◕ヮ◕)ノ*:・゚✧"), e("✧*(^▽^)*✧", "✧*(-▽-)*✧"), e("☆(≧▽≦)☆", "☆(-▽-)☆"), e("♪(^∇^)♪", "♪(-∇-)♪")],
|
|
93
|
-
focused: [e("(•‿•)~✎", "(-‿-)~✎"), e("( ˘_˘)~✿", "( -_-)~✿"), e("(◕_◕)✎", "(-_-)✎"), e("(・ω・)~♪", "(-ω-)~♪")],
|
|
94
|
-
curious: [e("(◕_◕)?✧", "(-_-)?✧"), e("(・・ )✿?", "(-- )✿?"), e("(°‿°)?", "(-‿-)?"), e("✧(◔_◔)", "✧(-_-)")],
|
|
95
|
-
thinking: [e("( ˘‿˘)~✎", "( -‿-)~✎"), e("( ̄~ ̄)✿", "(-~-)✿"), e("(◕~◕)…", "(-~-)…"), e("(・・)~♪", "(--)~♪")],
|
|
96
|
-
nervous: [e("(◕﹏◕✿)", "(-﹏-✿)"), e("(°‿°;)✎", "(-‿-;)✎"), e("(・_・;)✿", "(-_-;)✿"), e("(⊙‿⊙;)", "(-‿-;)")],
|
|
97
|
-
confused: [e("(◕_◕)?~", "(-_-)?~"), e("(・・?)✿", "(--?)✿"), e("(゜‿゜)?", "(-‿-)?"), e("(°_°)✎?", "(-_-)✎?")],
|
|
98
|
-
sleepy: [e("(-‿-)✿ zz", "(-.-)✿ zz"), e("( ˘ω˘ )✿z", "( -ω- )✿z"), e("(∪‿∪)♪ z", "(-‿-)♪ z"), e("( ̄‿ ̄)zzZ", "(-‿-)zzZ")],
|
|
99
|
-
sad: [e("(╥﹏╥)✿", "(-﹏-)✿"), e("(ಥ‿ಥ)", "(-‿-)"), e("(´;ω;`)✎", "(-;ω;-)✎"), e("(◕︵◕)", "(-︵-)")],
|
|
100
|
-
angry: [e("(╬◣_◢)", "(╬-_-)"), e("(ノ`Д´)ノ✎", "(ノ-Д-)ノ✎"), e("(¬_¬)✿", "(-_-)✿"), e("(ಠ‿ಠ)✎", "(-‿-)✎")],
|
|
101
|
-
waiting: [e("(っ´ω`c)✿", "(っ-ω-c)✿"), e("( ˘‿˘)~♪", "( -‿-)~♪"), e("(・‿・)…✿", "(-‿-)…✿"), e("(´・‿・`)", "(´-‿-`)")],
|
|
102
|
-
surprised: [e("(°o°)✿", "(-o-)✿"), e("✧(⊙‿⊙)✧", "✧(-‿-)✧"), e("(゚∀゚)!✎", "(-∀-)!✎"), e("Σ(◕o◕)", "Σ(-o-)")],
|
|
103
|
-
grateful: [e("(人◕‿◕)", "(人-‿-)"), e("(◍•ᴗ•◍)✿", "(◍-ᴗ-◍)✿"), e("✿m(_ _)m", "✿m(- -)m"), e("(*´▽`*)✧", "(*-▽-*)✧")],
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/** RESEARCH, the bookworm: takes notes, points at things, shrugs at the unknown. */
|
|
107
|
-
const RESEARCH: Record<Emotion, readonly Emote[]> = {
|
|
108
|
-
happy: [e("φ(^▽^)", "φ(-▽-)"), e("(^_^)[ ]", "(-_-)[ ]"), e("(•‿•)φ", "(-‿-)φ"), e("( ˘▽˘)ノ", "( -▽-)ノ")],
|
|
109
|
-
proud: [e("( •_•)>⌐■-■", "( -_-)>⌐■-■", "(⌐■_■)"), e("( ̄^ ̄)φ", "(-^-)φ"), e("(•ᴗ•)b[ ]", "(-ᴗ-)b[ ]"), e("(  ̄ー ̄)φ", "( -ー-)φ")],
|
|
110
|
-
love: [e("(♡_♡)[ ]", "(-_-)[ ]"), e("φ(´∀`)♡", "φ(-∀-)♡"), e("(◍•_•◍)φ", "(◍-_-◍)φ"), e("(˘ε˘)♡[ ]", "(-ε-)♡[ ]")],
|
|
111
|
-
excited: [e("(σ・・)σ!", "(σ--)σ!"), e("φ(≧▽≦)", "φ(-▽-)"), e("\\(°o°)/", "\\(-o-)/"), e("(ノ°▽°)ノ[ ]", "(ノ-▽-)ノ[ ]")],
|
|
112
|
-
focused: [e("φ(..)", "φ(--)", "φ(..;)"), e("( ._.)φ__", "( -_-)φ__"), e("(・_・)[ ]", "(-_-)[ ]"), e("( •_•)φ", "( -_-)φ")],
|
|
113
|
-
curious: [e("(σ・・)σ", "(σ--)σ"), e("(・・ )?[ ]", "(-- )?[ ]"), e("(⊙_⊙)φ?", "(-_-)φ?"), e("(°_°)…?", "(-_-)…?")],
|
|
114
|
-
thinking: [e("φ( ̄~ ̄)", "φ(-~-)"), e("( ˘_˘ )φ", "( -_- )φ"), e("(・・;)[ ]", "(--;)[ ]"), e("(¬_¬)φ…", "(-_-)φ…")],
|
|
115
|
-
nervous: [e("φ(°_°;)", "φ(-_-;)"), e("(・_・;)[?]", "(-_-;)[?]"), e("(⊙﹏⊙)φ", "(-﹏-)φ"), e("(°□°;)[ ]", "(-□-;)[ ]")],
|
|
116
|
-
confused: [e("¯\\_(ツ)_/¯", "¯\\_(ー)_/¯"), e("(・_・ヾ[?]", "(-_-ヾ[?]"), e("(⊙_☉)φ", "(-_-)φ"), e("(゜-゜)?[ ]", "(---)?[ ]")],
|
|
117
|
-
sleepy: [e("(-_-)[ ] zz", "(-.-)[ ] zz"), e("φ( ̄o ̄) z", "φ(-o-) z"), e("(∪_∪)φ zz", "(-_-)φ zz"), e("( ˘ω˘ )[ ]z", "( -ω- )[ ]z")],
|
|
118
|
-
sad: [e("(ಥ_ಥ)φ", "(-_-)φ"), e("(╥_╥)[ ]", "(-_-)[ ]"), e("(´;ω;`)φ", "(-;ω;-)φ"), e("(T_T)[?]", "(-_-)[?]")],
|
|
119
|
-
angry: [e("(ಠ_ಠ)φ", "(-_-)φ"), e("(╬ಠ_ಠ)[ ]", "(╬-_-)[ ]"), e("ヽ(`Д´)ノφ", "ヽ(-Д-)ノφ"), e("(¬_¬\")[ ]", "(-_-\")[ ]")],
|
|
120
|
-
waiting: [e("(´・_・`)φ", "(´-_-`)φ"), e("( ˘・_・˘)[ ]", "( ˘-_-˘)[ ]"), e("φ(・_・)…", "φ(-_-)…"), e("(っ´ω`c)φ", "(っ-ω-c)φ")],
|
|
121
|
-
surprised: [e("Σ(°o°)φ", "Σ(-o-)φ"), e("(゚Д゚)[!]", "(-Д-)[!]"), e("(⊙_⊙)!φ", "(-_-)!φ"), e("w(°o°)w[ ]", "w(-o-)w[ ]")],
|
|
122
|
-
grateful: [e("(人´∀`)φ", "(人-∀-)φ"), e("m(_ _)m[ ]", "m(- -)m[ ]"), e("(ᵔᴥᵔ)φ", "(-ᴥ-)φ"), e("(◍•ᴗ•◍)[ ]", "(◍-ᴗ-◍)[ ]")],
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/** QA, the inspector: side-eyes and squints at everything, then flexes and dances on a pass. */
|
|
126
|
-
const QA: Record<Emotion, readonly Emote[]> = {
|
|
127
|
-
happy: [e("(•‿•)✓", "(-‿-)✓"), e("( ^_^)[✓]", "( -_-)[✓]"), e("(ᵔ.ᵔ)✓", "(-.-)✓"), e("(^_^)✓", "(-_-)✓")],
|
|
128
|
-
proud: [e("ᕙ( • ‿ • )ᕗ", "ᕙ( - ‿ - )ᕗ"), e("ᕦ(•_•)ᕤ✓", "ᕦ(-_-)ᕤ✓"), e("( ̄^ ̄)✓", "(-^-)✓"), e("( •_•)b✓", "( -_-)b✓")],
|
|
129
|
-
love: [e("(♡_♡)✓", "(-_-)✓"), e("(´∀`)♡[✓]", "(-∀-)♡[✓]"), e("(◍•_•◍)✓", "(◍-_-◍)✓"), e("(˘ε˘)♡✓", "(-ε-)♡✓")],
|
|
130
|
-
excited: [e("ᕕ( ᐛ )ᕗ", "ᕕ( - )ᕗ"), e("\\(•_•)/✓", "\\(-_-)/✓"), e("(ノ≧▽≦)ノ✓", "(ノ-▽-)ノ✓"), e("ᕙ(⇀‸↼)ᕗ", "ᕙ(-‸-)ᕗ")],
|
|
131
|
-
focused: [e("(ಠ_ಠ)", "(−_−)"), e("(¬_¬)", "(-_-)"), e("(눈_눈)", "(-_-)"), e("(•_•)⊙", "(-_-)⊙")],
|
|
132
|
-
curious: [e("(ಠ_ಠ)?", "(−_−)?"), e("(¬_¬)?[ ]", "(-_-)?[ ]"), e("(⊙_⊙)⊙", "(-_-)⊙"), e("(・_・)?✓", "(-_-)?✓")],
|
|
133
|
-
thinking: [e("(¬‿¬)[ ]", "(-‿-)[ ]"), e("(ಠ~ಠ)", "(−~−)"), e("( ̄_ ̄)[?]", "(-_-)[?]"), e("( ˘_˘ )✓?", "( -_- )✓?")],
|
|
134
|
-
nervous: [e("(ಠ_ಠ;)", "(−_−;)"), e("(°_°;)[ ]", "(-_-;)[ ]"), e("(⊙﹏⊙)✓?", "(-﹏-)✓?"), e("(・_・;)✗?", "(-_-;)✗?")],
|
|
135
|
-
confused: [e("(ಠ_ಠ)?!", "(−_−)?!"), e("(・_・ヾ[?]", "(-_-ヾ[?]"), e("(⊙_☉)✗", "(-_-)✗"), e("(゜-゜)[✗]", "(---)[✗]")],
|
|
136
|
-
sleepy: [e("(-_-)[ ] zz", "(-.-)[ ] zz"), e("( ̄o ̄)✓ zz", "(-o-)✓ zz"), e("(∪_∪)⊙ z", "(-_-)⊙ z"), e("( ˘ω˘ )✓z", "( -ω- )✓z")],
|
|
137
|
-
sad: [e("(ಥ_ಥ)✗", "(-_-)✗"), e("(╥﹏╥)[✗]", "(-﹏-)[✗]"), e("(´;ω;`)✗", "(-;ω;-)✗"), e("(T_T)✗", "(-_-)✗")],
|
|
138
|
-
angry: [e("(ಠ益ಠ)", "(−益−)"), e("(╬ಠ_ಠ)✗", "(╬−_−)✗"), e("(ノಠ益ಠ)ノ", "(ノ−益−)ノ", "(ノಠ益ಠ)ノ[✗]"), e("(¬_¬\")✗", "(-_-\")✗")],
|
|
139
|
-
waiting: [e("(¬_¬)…", "(-_-)…"), e("(´・_・`)[ ]", "(´-_-`)[ ]"), e("(ಠ_ಠ)…", "(−_−)…"), e("( ˘_˘)✓…", "( -_-)✓…")],
|
|
140
|
-
surprised: [e("(ಠ_ಠ)!", "(−_−)!"), e("Σ(°_°)✗", "Σ(-_-)✗"), e("(⊙_⊙)[!]", "(-_-)[!]"), e("(゚Д゚)✓!", "(-Д-)✓!")],
|
|
141
|
-
grateful: [e("(人•_•)✓", "(人-_-)✓"), e("m(_ _)m✓", "m(- -)m✓"), e("(ᵔ.ᵔ)b", "(-.-)b"), e("(◍•ᴗ•◍)✓", "(◍-ᴗ-◍)✓")],
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/** Personality sets per agent column. */
|
|
145
|
-
export const PERSONALITY_EMOTES: Record<SlotId, Record<Emotion, readonly Emote[]>> = {
|
|
146
|
-
dev: DEV,
|
|
147
|
-
design: DESIGN,
|
|
148
|
-
research: RESEARCH,
|
|
149
|
-
qa: QA,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/** Frames per emote: open, blink, then the action (or the open face again), held one step each. */
|
|
153
|
-
export const EMOTE_STEPS = 4;
|
|
154
|
-
|
|
155
|
-
/** The four frames an emote steps through: open, blink, action, action. */
|
|
156
|
-
export function emoteFrames(emote: Emote): readonly string[] {
|
|
157
|
-
const finish = emote.action ?? emote.open;
|
|
158
|
-
return [emote.open, emote.blink, finish, finish];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** What a slot is going through, as far as its face is concerned. */
|
|
162
|
-
export interface Situation {
|
|
163
|
-
status: SlotState;
|
|
164
|
-
/** Live one-word activity while working (reading, editing, running, thinking, ...). */
|
|
165
|
-
activity?: string;
|
|
166
|
-
flag?: "waiting" | "quiet" | "retry";
|
|
167
|
-
/** The run was asked to wrap up early. */
|
|
168
|
-
wrappedUp?: boolean;
|
|
169
|
-
/** A file was just handed to this agent. */
|
|
170
|
-
handover?: boolean;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const ACTIVITY_EMOTIONS: Record<string, readonly Emotion[]> = {
|
|
174
|
-
reading: ["curious", "focused"],
|
|
175
|
-
searching: ["curious", "thinking"],
|
|
176
|
-
editing: ["focused", "excited", "proud"],
|
|
177
|
-
running: ["nervous", "focused"],
|
|
178
|
-
thinking: ["thinking", "curious"],
|
|
179
|
-
writing: ["focused", "proud"],
|
|
180
|
-
researching: ["curious", "thinking"],
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/** The emotions that fit a situation, most fitting first. */
|
|
184
|
-
export function emotionsFor(situation: Situation): readonly Emotion[] {
|
|
185
|
-
switch (situation.status) {
|
|
186
|
-
case "idle":
|
|
187
|
-
return ["sleepy", "waiting", "happy"];
|
|
188
|
-
case "done":
|
|
189
|
-
return situation.wrappedUp ? ["nervous", "proud"] : ["happy", "proud", "love", "excited"];
|
|
190
|
-
case "failed":
|
|
191
|
-
return ["sad", "angry", "confused", "surprised"];
|
|
192
|
-
default:
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
if (situation.flag === "waiting") return ["waiting"];
|
|
196
|
-
if (situation.flag === "quiet") return ["confused", "sleepy"];
|
|
197
|
-
if (situation.flag === "retry") return ["nervous", "sad"];
|
|
198
|
-
if (situation.handover) return ["grateful", "happy"];
|
|
199
|
-
if (situation.wrappedUp) return ["nervous", "surprised"];
|
|
200
|
-
return ACTIVITY_EMOTIONS[situation.activity ?? ""] ?? ["focused", "excited", "happy"];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Share of picks drawn from the agent's own personality set (out of 10). */
|
|
204
|
-
const PERSONALITY_SHARE = 6;
|
|
205
|
-
|
|
206
|
-
function positive(variant: number): number {
|
|
207
|
-
return Number.isFinite(variant) ? Math.abs(Math.floor(variant)) : 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* The emote for one expression: the emotion from the situation, then a face
|
|
212
|
-
* from the agent's personality (about 60%) or the shared pool, all chosen by
|
|
213
|
-
* the expression's `variant`.
|
|
214
|
-
*/
|
|
215
|
-
export function pickEmote(slot: SlotId, situation: Situation, variant: number): Emote {
|
|
216
|
-
const seed = positive(variant);
|
|
217
|
-
const emotions = emotionsFor(situation);
|
|
218
|
-
const emotion = emotions[seed % emotions.length]!;
|
|
219
|
-
const own = (Math.floor(seed / 7) % 10) < PERSONALITY_SHARE;
|
|
220
|
-
const pool = own ? PERSONALITY_EMOTES[slot][emotion] : SHARED_EMOTES[emotion];
|
|
221
|
-
return pool[Math.floor(seed / 71) % pool.length]!;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** The frames of the emote a slot plays for this expression. */
|
|
225
|
-
export function slotEmote(slot: SlotId, situation: Situation, variant: number): readonly string[] {
|
|
226
|
-
return emoteFrames(pickEmote(slot, situation, variant));
|
|
227
|
-
}
|