@a-t-h-i/bot-lobby 0.3.0 → 0.5.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.
Files changed (61) hide show
  1. package/README.md +266 -35
  2. package/package.json +1 -1
  3. package/prompts/backend.md +46 -1
  4. package/prompts/designer.md +94 -15
  5. package/prompts/master.md +70 -1
  6. package/prompts/panel.md +39 -0
  7. package/prompts/planner.md +64 -0
  8. package/prompts/qa.md +35 -2
  9. package/prompts/quickfix.md +41 -0
  10. package/prompts/researcher.md +6 -0
  11. package/prompts/reviewer.md +16 -0
  12. package/prompts/scout.md +11 -2
  13. package/prompts/worker.md +35 -2
  14. package/src/desk/client-extension.ts +101 -0
  15. package/src/desk/desk.ts +249 -0
  16. package/src/desk/ipc.ts +178 -0
  17. package/src/desk/session.ts +214 -0
  18. package/src/execution/agent-runner.ts +165 -14
  19. package/src/execution/pi-runner.ts +376 -65
  20. package/src/index.ts +7 -0
  21. package/src/lobby/feed.ts +253 -0
  22. package/src/lobby/issues.ts +227 -0
  23. package/src/lobby/layout.ts +174 -0
  24. package/src/lobby/planner.ts +474 -0
  25. package/src/lobby/quickfix.ts +227 -0
  26. package/src/lobby/runtime.ts +440 -0
  27. package/src/lobby/tabs/home.ts +164 -0
  28. package/src/lobby/tabs/issues.ts +72 -0
  29. package/src/lobby/tabs/metrics.ts +162 -0
  30. package/src/lobby/tabs/plan.ts +160 -0
  31. package/src/lobby/tabs/quickfix.ts +101 -0
  32. package/src/lobby/tabs/tasks.ts +209 -0
  33. package/src/lobby/view.ts +855 -0
  34. package/src/master/master.ts +21 -17
  35. package/src/master/research.ts +8 -7
  36. package/src/pi/activity.ts +165 -0
  37. package/src/pi/commands.ts +46 -59
  38. package/src/pi/events.ts +5 -2
  39. package/src/pi/expressions.ts +43 -12
  40. package/src/pi/kaomoji.ts +227 -0
  41. package/src/pi/mascot-art.ts +5 -15
  42. package/src/pi/model-support.ts +135 -0
  43. package/src/pi/run-summary.ts +172 -0
  44. package/src/pi/settings-ui.ts +162 -60
  45. package/src/pi/start-task.ts +63 -0
  46. package/src/pi/tools.ts +51 -8
  47. package/src/pi/ui.ts +151 -49
  48. package/src/pi/zen-large.ts +41 -4
  49. package/src/pi/zen-metrics.ts +47 -7
  50. package/src/pi/zen.ts +29 -8
  51. package/src/roles/reviewer.ts +24 -4
  52. package/src/roles/worker.ts +7 -1
  53. package/src/schemas/configuration.ts +177 -18
  54. package/src/schemas/findings.ts +26 -0
  55. package/src/schemas/task.ts +27 -1
  56. package/src/state/backlog.ts +106 -0
  57. package/src/state/comments.ts +136 -0
  58. package/src/state/metrics.ts +305 -0
  59. package/src/state/project.ts +9 -0
  60. package/src/text.ts +9 -0
  61. package/src/workflow/workflow.ts +161 -12
@@ -2,7 +2,9 @@ import type { Domain, Role } from "../schemas/agent.ts";
2
2
  import type { AgentRun } from "../schemas/findings.ts";
3
3
  import { roleSpec } from "../roles/registry.ts";
4
4
  import { compilePrompt } from "../prompts/compiler.ts";
5
- import { runPiAgent, spawnPiProcess, type ProcessRunner } from "./pi-runner.ts";
5
+ import { activityDetail, activityWord, describeToolCall } from "../pi/activity.ts";
6
+ import { shortDuration, truncate } from "../text.ts";
7
+ import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "./pi-runner.ts";
6
8
 
7
9
  export interface AgentContext {
8
10
  task: string;
@@ -14,6 +16,16 @@ export interface AgentContext {
14
16
  workflowContext?: string;
15
17
  }
16
18
 
19
+ /** Live fields a caller (the file desk) may set on a running agent. */
20
+ export type LivePatch = Pick<AgentRun, "note" | "noteKind" | "waitingFor">;
21
+
22
+ /** Control over one running attempt, handed to `onStart`. */
23
+ export interface AgentHandle {
24
+ runId: string;
25
+ steer(text: string): void;
26
+ annotate(patch: LivePatch): void;
27
+ }
28
+
17
29
  export interface AgentRequest {
18
30
  taskId: string;
19
31
  domain: Domain;
@@ -28,15 +40,50 @@ export interface AgentRequest {
28
40
  cwd: string;
29
41
  signal?: AbortSignal;
30
42
  onUpdate?: (run: AgentRun) => void;
31
- /** Bounded retries for transient failures (crash/timeout). */
43
+ /** Bounded retries for transient failures (crash/stall). */
32
44
  retries?: number;
45
+ /** Kill after this long without output; 0 or absent disables. */
46
+ stallTimeoutMs?: number;
47
+ /** Silence allowed while one tool call runs. */
48
+ toolStallTimeoutMs?: number;
49
+ /** Fraction of `timeoutMs` at which the agent is asked to wrap up; 0 disables. */
50
+ wrapUpAt?: number;
51
+ /** Extra tools on top of the role allowlist (the file desk's tools). */
52
+ extraTools?: readonly string[];
53
+ env?: Record<string, string>;
54
+ /** Called once per attempt when the process is running. */
55
+ onStart?: (handle: AgentHandle) => void;
56
+ /** Called once per attempt after it ends, before any retry. */
57
+ onAttemptEnd?: (run: AgentRun) => void;
58
+ }
59
+
60
+ /** The watchdog fields of an AgentRequest, taken from the workflow config. */
61
+ export function watchdogOptions(workflow: {
62
+ maxAgentRetries: number;
63
+ stallTimeoutMs: number;
64
+ toolStallTimeoutMs: number;
65
+ wrapUpAt: number;
66
+ }): Pick<AgentRequest, "retries" | "stallTimeoutMs" | "toolStallTimeoutMs" | "wrapUpAt"> {
67
+ return {
68
+ retries: workflow.maxAgentRetries,
69
+ stallTimeoutMs: workflow.stallTimeoutMs,
70
+ toolStallTimeoutMs: workflow.toolStallTimeoutMs,
71
+ wrapUpAt: workflow.wrapUpAt,
72
+ };
33
73
  }
34
74
 
35
75
  const activeControllers = new Set<AbortController>();
36
76
 
77
+ /** A stall or crash is worth one more try; a spent deadline or a cancel is not. */
78
+ function retryable(run: AgentRun): boolean {
79
+ if (run.status === "failed") return true;
80
+ return run.status === "timeout" && run.stalled === true;
81
+ }
82
+
37
83
  /**
38
84
  * Run one domain/role agent, retrying transient failures a bounded number of
39
- * times. Cancellation never retries, so Esc/quit stays responsive.
85
+ * times. Cancellation and a spent deadline never retry, so Esc stays responsive
86
+ * and a slow agent cannot double its time budget.
40
87
  */
41
88
  export async function runAgent(request: AgentRequest, run: ProcessRunner = spawnPiProcess): Promise<AgentRun> {
42
89
  const startedAt = new Date().toISOString();
@@ -44,7 +91,8 @@ export async function runAgent(request: AgentRequest, run: ProcessRunner = spawn
44
91
  let last: AgentRun | undefined;
45
92
  for (let attempt = 1; attempt <= attempts; attempt++) {
46
93
  last = await runAgentOnce(request, run, attempt, startedAt);
47
- if (last.status === "success" || last.status === "cancelled") break;
94
+ request.onAttemptEnd?.(last);
95
+ if (!retryable(last)) break;
48
96
  }
49
97
  return last!;
50
98
  }
@@ -66,9 +114,17 @@ function baseRun(request: AgentRequest, runId: string, startedAt: string, attemp
66
114
  output: "",
67
115
  attempts,
68
116
  startedAt,
117
+ ...(request.thinking ? { thinking: request.thinking } : {}),
118
+ ...(attempts > 1 ? { note: `retry ${attempts - 1} of ${(request.retries ?? 0)}`, noteKind: "warning" as const } : {}),
69
119
  };
70
120
  }
71
121
 
122
+ function toolsFor(request: AgentRequest): readonly string[] | undefined {
123
+ const tools = roleSpec(request.role).tools;
124
+ if (!request.extraTools?.length) return tools;
125
+ return [...(tools ?? []), ...request.extraTools];
126
+ }
127
+
72
128
  /**
73
129
  * Run one domain/role agent in an isolated pi process. The role's tool
74
130
  * allowlist comes from its spec, so read-only roles cannot modify anything.
@@ -76,24 +132,46 @@ function baseRun(request: AgentRequest, runId: string, startedAt: string, attemp
76
132
  async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt: number, startedAt: string): Promise<AgentRun> {
77
133
  const base = baseRun(request, `${request.taskId}:${request.domain}:${request.role}:${Date.now().toString(36)}`, startedAt, attempt);
78
134
  request.onUpdate?.(base);
135
+ const live = createLiveRun(base, request);
79
136
 
80
137
  const controller = new AbortController();
81
138
  activeControllers.add(controller);
82
139
  const signal = request.signal ? AbortSignal.any([request.signal, controller.signal]) : controller.signal;
83
140
  try {
84
141
  const systemPrompt = compilePrompt({ domain: request.domain, role: request.role, ...request.context });
142
+ const wrapUp = request.wrapUpAt && request.wrapUpAt > 0 && request.wrapUpAt < 1 ? Math.floor(request.timeoutMs * request.wrapUpAt) : 0;
85
143
  const result = await runPiAgent({
86
144
  cwd: request.cwd,
87
145
  task: request.instruction,
88
146
  systemPrompt,
89
- tools: roleSpec(request.role).tools,
147
+ tools: toolsFor(request),
90
148
  model: request.model,
91
149
  thinking: request.thinking,
92
150
  timeoutMs: request.timeoutMs,
93
151
  signal,
94
- onActivity: activityReporter(base, request),
152
+ env: request.env,
153
+ stallTimeoutMs: request.stallTimeoutMs,
154
+ toolStallTimeoutMs: request.toolStallTimeoutMs,
155
+ wrapUpAtMs: wrapUp,
156
+ onEvent: live.onEvent,
157
+ onStart: (handle) => request.onStart?.({ runId: base.runId, steer: handle.steer, annotate: live.annotate }),
95
158
  }, run);
96
- const final: AgentRun = { ...base, status: result.status, output: result.output, error: result.error, usage: result.usage, finishedAt: new Date().toISOString() };
159
+ const current = live.current();
160
+ const final: AgentRun = {
161
+ ...base,
162
+ status: result.status,
163
+ output: result.output,
164
+ error: result.error ? describeError(result.error, current, result.stalled === true) : undefined,
165
+ usage: result.usage,
166
+ finishedAt: new Date().toISOString(),
167
+ turns: current.turns ?? (result.usage.turns || undefined),
168
+ tools: current.tools,
169
+ model: result.model ?? current.model,
170
+ ...(result.stalled ? { stalled: true } : {}),
171
+ ...(result.wrappedUp ? { wrappedUp: true } : {}),
172
+ note: undefined,
173
+ noteKind: undefined,
174
+ };
97
175
  request.onUpdate?.(final);
98
176
  return final;
99
177
  } finally {
@@ -101,14 +179,82 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
101
179
  }
102
180
  }
103
181
 
104
- /** Streams activity-word changes for one run, deduping consecutive repeats. */
105
- function activityReporter(base: AgentRun, request: AgentRequest): (activity: string) => void {
106
- let last: string | undefined;
107
- return (activity) => {
108
- if (activity === last) return;
109
- last = activity;
110
- request.onUpdate?.({ ...base, activity });
182
+ /** Name what the agent was doing when a stall or deadline hit. */
183
+ function describeError(error: string, current: AgentRun, stalled: boolean): string {
184
+ if (!stalled && !/time limit/.test(error)) return error;
185
+ const doing = current.activity ? ` while ${current.activity}${current.detail ? ` ${current.detail}` : ""}` : "";
186
+ return `${error}${doing}`;
187
+ }
188
+
189
+ const THROTTLE_MS = 2000;
190
+
191
+ /**
192
+ * Tracks one attempt's live state from its stream and reports it through
193
+ * `onUpdate`: activity changes, retries and notes immediately, counters and
194
+ * heartbeats at most every couple of seconds.
195
+ */
196
+ function createLiveRun(base: AgentRun, request: AgentRequest) {
197
+ let state: AgentRun = base;
198
+ let lastEmit = 0;
199
+ const emit = (patch: Partial<AgentRun>, force: boolean) => {
200
+ state = { ...state, ...patch, lastEventAt: Date.now() };
201
+ const now = Date.now();
202
+ if (!force && now - lastEmit < THROTTLE_MS) return;
203
+ lastEmit = now;
204
+ request.onUpdate?.(state);
205
+ };
206
+ const changed = (activity: string, detail: string | undefined) => activity !== state.activity || detail !== state.detail;
207
+ const onEvent = (event: PiStreamEvent) => {
208
+ switch (event.type) {
209
+ case "tool_execution_start": {
210
+ const activity = activityWord(event.toolName);
211
+ const detail = activityDetail(event.toolName, event.args);
212
+ const step = describeToolCall(event.toolName, event.args);
213
+ emit({ activity, detail, step, tools: (state.tools ?? 0) + 1 }, changed(activity, detail) || step !== state.step);
214
+ return;
215
+ }
216
+ case "thought":
217
+ emit({ thought: event.text }, true);
218
+ return;
219
+ case "thinking":
220
+ case "writing":
221
+ emit({ activity: event.type, detail: undefined }, changed(event.type, undefined));
222
+ return;
223
+ case "turn_start":
224
+ emit({ turns: (state.turns ?? 0) + 1 }, false);
225
+ return;
226
+ case "retry":
227
+ emit({ note: `provider retry ${event.attempt}/${event.maxAttempts}: ${truncateLine(event.error)}`, noteKind: "warning" }, true);
228
+ return;
229
+ case "retry_end":
230
+ emit({ note: undefined, noteKind: undefined }, true);
231
+ return;
232
+ case "compaction":
233
+ emit({ note: "compacting context", noteKind: "info" }, true);
234
+ return;
235
+ case "wrap_up":
236
+ emit({ note: `asked to wrap up (${shortDuration(request.timeoutMs)} limit)`, noteKind: "warning", wrappedUp: true }, true);
237
+ return;
238
+ case "usage": {
239
+ const usage = state.usage ?? { input: 0, output: 0, cost: 0, turns: 0 };
240
+ const next = { input: usage.input + event.input, output: usage.output + event.output, cost: usage.cost + event.cost, turns: usage.turns + 1 };
241
+ emit({ usage: next, model: event.model ?? state.model }, false);
242
+ return;
243
+ }
244
+ default:
245
+ emit({}, false);
246
+ }
111
247
  };
248
+ return {
249
+ onEvent,
250
+ annotate: (patch: LivePatch) => emit(patch, true),
251
+ current: () => state,
252
+ };
253
+ }
254
+
255
+ function truncateLine(text: string): string {
256
+ const first = text.split("\n")[0] ?? "";
257
+ return truncate(first, 48).split("\n")[0]!;
112
258
  }
113
259
 
114
260
  async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn) => Promise<TOut>): Promise<TOut[]> {
@@ -126,6 +272,11 @@ async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: num
126
272
  return results;
127
273
  }
128
274
 
275
+ /** Run independent tasks concurrently, bounded by `limit`. */
276
+ export async function mapConcurrent<TIn, TOut>(items: TIn[], limit: number, fn: (item: TIn) => Promise<TOut>): Promise<TOut[]> {
277
+ return mapWithConcurrencyLimit(items, limit, fn);
278
+ }
279
+
129
280
  /** Run independent agents concurrently, bounded by `limit`. */
130
281
  export async function runParallel(requests: AgentRequest[], limit: number, run: ProcessRunner = spawnPiProcess): Promise<AgentRun[]> {
131
282
  return mapWithConcurrencyLimit(requests, limit, (request) => runAgent(request, run));