@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.
- package/README.md +266 -35
- package/package.json +1 -1
- package/prompts/backend.md +46 -1
- package/prompts/designer.md +94 -15
- package/prompts/master.md +70 -1
- package/prompts/panel.md +39 -0
- package/prompts/planner.md +64 -0
- package/prompts/qa.md +35 -2
- package/prompts/quickfix.md +41 -0
- package/prompts/researcher.md +6 -0
- package/prompts/reviewer.md +16 -0
- package/prompts/scout.md +11 -2
- package/prompts/worker.md +35 -2
- package/src/desk/client-extension.ts +101 -0
- package/src/desk/desk.ts +249 -0
- package/src/desk/ipc.ts +178 -0
- package/src/desk/session.ts +214 -0
- package/src/execution/agent-runner.ts +165 -14
- package/src/execution/pi-runner.ts +376 -65
- package/src/index.ts +7 -0
- package/src/lobby/feed.ts +253 -0
- package/src/lobby/issues.ts +227 -0
- package/src/lobby/layout.ts +174 -0
- package/src/lobby/planner.ts +474 -0
- package/src/lobby/quickfix.ts +227 -0
- package/src/lobby/runtime.ts +440 -0
- package/src/lobby/tabs/home.ts +164 -0
- package/src/lobby/tabs/issues.ts +72 -0
- package/src/lobby/tabs/metrics.ts +162 -0
- package/src/lobby/tabs/plan.ts +160 -0
- package/src/lobby/tabs/quickfix.ts +101 -0
- package/src/lobby/tabs/tasks.ts +209 -0
- package/src/lobby/view.ts +855 -0
- package/src/master/master.ts +21 -17
- package/src/master/research.ts +8 -7
- package/src/pi/activity.ts +165 -0
- package/src/pi/commands.ts +46 -59
- package/src/pi/events.ts +5 -2
- package/src/pi/expressions.ts +43 -12
- package/src/pi/kaomoji.ts +227 -0
- package/src/pi/mascot-art.ts +5 -15
- package/src/pi/model-support.ts +135 -0
- package/src/pi/run-summary.ts +172 -0
- package/src/pi/settings-ui.ts +162 -60
- package/src/pi/start-task.ts +63 -0
- package/src/pi/tools.ts +51 -8
- package/src/pi/ui.ts +151 -49
- package/src/pi/zen-large.ts +41 -4
- package/src/pi/zen-metrics.ts +47 -7
- package/src/pi/zen.ts +29 -8
- package/src/roles/reviewer.ts +24 -4
- package/src/roles/worker.ts +7 -1
- package/src/schemas/configuration.ts +177 -18
- package/src/schemas/findings.ts +26 -0
- package/src/schemas/task.ts +27 -1
- package/src/state/backlog.ts +106 -0
- package/src/state/comments.ts +136 -0
- package/src/state/metrics.ts +305 -0
- package/src/state/project.ts +9 -0
- package/src/text.ts +9 -0
- 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 {
|
|
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/
|
|
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
|
|
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
|
-
|
|
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:
|
|
147
|
+
tools: toolsFor(request),
|
|
90
148
|
model: request.model,
|
|
91
149
|
thinking: request.thinking,
|
|
92
150
|
timeoutMs: request.timeoutMs,
|
|
93
151
|
signal,
|
|
94
|
-
|
|
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
|
|
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
|
-
/**
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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));
|