@a-t-h-i/bot-lobby 0.6.53 → 0.6.55
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 +1 -1
- package/package.json +1 -1
- package/prompts/expert.md +17 -0
- package/prompts/goal-check.md +44 -0
- package/prompts/master.md +70 -32
- package/prompts/worker.md +25 -11
- package/src/desk/board-client.ts +65 -0
- package/src/desk/board.ts +200 -0
- package/src/desk/client-extension.ts +31 -1
- package/src/desk/ipc.ts +16 -4
- package/src/desk/session.ts +129 -1
- package/src/execution/agent-runner.ts +27 -132
- package/src/execution/delegation.ts +112 -38
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +30 -163
- package/src/execution/worker-trees.ts +396 -0
- package/src/index.ts +6 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +2 -0
- package/src/lobby/planner.ts +2 -2
- package/src/lobby/pr-review.ts +3 -3
- package/src/lobby/quickfix.ts +4 -3
- package/src/lobby/recovery.ts +2 -10
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/master/goal-check.ts +114 -0
- package/src/master/master.ts +60 -24
- package/src/master/research.ts +3 -6
- package/src/pi/commands.ts +5 -45
- package/src/pi/delegation.ts +23 -4
- package/src/pi/events.ts +6 -19
- package/src/pi/grants.ts +89 -0
- package/src/pi/model-settings.ts +2 -4
- package/src/pi/model-support.ts +12 -5
- package/src/pi/route.ts +1 -1
- package/src/pi/run-summary.ts +3 -7
- package/src/pi/start-flags.ts +3 -3
- package/src/pi/start-task.ts +2 -8
- package/src/pi/tools.ts +10 -12
- package/src/roles/worker.ts +0 -18
- package/src/schemas/configuration.ts +81 -39
- package/src/schemas/findings.ts +0 -12
- package/src/schemas/task.ts +12 -1
- package/src/state/metrics.ts +1 -1
- package/src/state/work-clock.ts +140 -0
- package/src/state/work-time.ts +4 -7
- package/src/webui/api/index.ts +2 -1
- package/src/webui/api/settings.ts +6 -18
- package/src/webui/dev/fake-service.ts +14 -0
- package/src/webui/protocol.ts +23 -0
- package/src/workflow/workflow.ts +204 -344
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
- package/webui/dist/assets/index-BkXhtB6A.css +2 -0
- package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
- package/webui/dist/build.json +10 -6
- package/webui/dist/index.html +2 -2
- package/src/state/budget.ts +0 -301
- package/src/state/step-limits.ts +0 -41
- package/webui/dist/assets/index-Cca5uEVs.js +0 -115
- package/webui/dist/assets/index-DsVdXmh5.css +0 -2
|
@@ -7,16 +7,31 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
7
7
|
import type { Domain, Role } from "../schemas/agent.ts";
|
|
8
8
|
import { effortFor } from "../classifier/instance.ts";
|
|
9
9
|
import { fellShort, profileLabel, routeLabel } from "../classifier/effort.ts";
|
|
10
|
-
import { agentProfile } from "../schemas/configuration.ts";
|
|
10
|
+
import { agentProfile, expertProfile } from "../schemas/configuration.ts";
|
|
11
|
+
import { loadPrompt } from "../prompts/loader.ts";
|
|
11
12
|
import { dataRoot, detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
12
13
|
import { EditLog } from "../state/changes.ts";
|
|
13
14
|
import { createLiveRun } from "./live-run.ts";
|
|
14
15
|
import { runPiAgent, type PiRunOptions, type ProcessRunner } from "./pi-runner.ts";
|
|
15
16
|
import { startDeskServer } from "../desk/ipc.ts";
|
|
16
|
-
import { DESK_ENV,
|
|
17
|
+
import { DESK_ENV, DeskSession, deskSessionAt, PEER_TOOLS } from "../desk/session.ts";
|
|
18
|
+
import { BOARD_ENV } from "../desk/board.ts";
|
|
17
19
|
|
|
18
20
|
export const MAX_CHILDREN = 5;
|
|
19
21
|
export const DELEGATE_TOOL = "delegate_subtasks";
|
|
22
|
+
export const EXPERT_TOOL = "ask_expert";
|
|
23
|
+
/** Consults one step may make: advice on the hard part, not a second worker. */
|
|
24
|
+
export const MAX_CONSULTS = 2;
|
|
25
|
+
/** Longest an expert may take to answer. */
|
|
26
|
+
/** Marks an expert's run, so its host gives it an adviser's note rather than a child's. */
|
|
27
|
+
export const EXPERT_ENV = "BOT_LOBBY_EXPERT";
|
|
28
|
+
|
|
29
|
+
/** The worker's env without its desk and board: an expert only reads and answers. */
|
|
30
|
+
function withoutTeamEnv(env: Record<string, string> | undefined): Record<string, string> {
|
|
31
|
+
const { [DESK_ENV.address]: _address, [DESK_ENV.worker]: _worker, [BOARD_ENV]: _board, BOT_LOBBY_BOARD_FROM: _from, ...rest } = env ?? {};
|
|
32
|
+
return rest;
|
|
33
|
+
}
|
|
34
|
+
const EXPERT_TOOLS = ["read", "grep", "find", "ls"];
|
|
20
35
|
export const AGENT_ENV = "BOT_LOBBY_AGENT_POLICY";
|
|
21
36
|
export const DELEGATION_ENV = "BOT_LOBBY_DELEGATION";
|
|
22
37
|
|
|
@@ -27,6 +42,12 @@ export function canDelegate(agent: { role: Role; domain: Domain; depth?: number
|
|
|
27
42
|
return agent.role === "reviewer" && agent.domain === "qa";
|
|
28
43
|
}
|
|
29
44
|
|
|
45
|
+
/** A task's DESIGN/DEV workers (not their children, nor quick fixes) may consult a stronger model, when the setting is on. */
|
|
46
|
+
export function canConsult(agent: { runId: string; taskId: string; role: Role; domain: Domain; depth?: number }, enabled: boolean): boolean {
|
|
47
|
+
// A lobby job (a quick fix) runs as its own task: it is one agent on a small change, not a task's worker.
|
|
48
|
+
return enabled && agent.taskId !== agent.runId && (agent.depth ?? 1) === 1 && agent.role === "worker" && (agent.domain === "designer" || agent.domain === "backend");
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
/** One shared child pool per task; parents waiting on children never hold its slots. */
|
|
31
52
|
const pools = new Map<string, { active: number; users: number; waiters: Array<() => void> }>();
|
|
32
53
|
|
|
@@ -63,7 +84,7 @@ export interface AgentPolicy {
|
|
|
63
84
|
depth: number;
|
|
64
85
|
}
|
|
65
86
|
|
|
66
|
-
/** The host owns quotas,
|
|
87
|
+
/** The host owns quotas, file identities and child lifetimes, outside model context. */
|
|
67
88
|
export async function delegationHost(options: PiRunOptions, run: ProcessRunner) {
|
|
68
89
|
const config = loadConfig();
|
|
69
90
|
const id = options.agent?.runId ?? randomUUID();
|
|
@@ -72,26 +93,29 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
72
93
|
const agent = options.agent ?? { runId: id, taskId: id, domain: "backend" as const, role: editable ? "worker" as const : "reviewer" as const };
|
|
73
94
|
const depth = agent.depth ?? 1;
|
|
74
95
|
const quota = options.delegationQuota ?? { used: 0 };
|
|
75
|
-
const
|
|
96
|
+
const consults = canConsult(agent, config.workflow.consultExpert);
|
|
97
|
+
const tools = [...new Set([...baseTools, "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : []), ...(consults ? [EXPERT_TOOL] : [])])];
|
|
76
98
|
const mcpTools = options.mcpTools ?? agentProfile(config, agent.domain, agent.role).mcpTools ?? [];
|
|
77
99
|
const policy: AgentPolicy = { tools, mcpTools, readOnly: !editable, depth };
|
|
78
|
-
if (depth > 1) return { tools, mcpTools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
|
|
100
|
+
if (depth > 1) return { tools, mcpTools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: options.env?.[EXPERT_ENV] ? "You are advising a worker: read what you need, answer its question, and change nothing." : "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
|
|
79
101
|
|
|
80
102
|
const address = options.env?.[DESK_ENV.address];
|
|
81
103
|
let desk = address ? deskSessionAt(address) : undefined;
|
|
82
104
|
if (address && !desk) throw new Error("The inherited file desk is unavailable.");
|
|
83
|
-
const
|
|
84
|
-
|
|
105
|
+
const board = options.env?.[BOARD_ENV];
|
|
106
|
+
// A worker with a checkout of its own (no claims on the batch desk) still needs claims among its children.
|
|
107
|
+
const ownedDesk = editable && (!desk || !desk.usesClaims) ? new DeskSession({ cwd: options.cwd, ...(board ? { boardPath: board } : {}) }) : undefined;
|
|
108
|
+
if (ownedDesk) await ownedDesk.open();
|
|
109
|
+
// Children claim files among themselves; the parent keeps the batch desk (and its peers) when it has one.
|
|
110
|
+
const childDesk = ownedDesk ?? desk;
|
|
111
|
+
desk ??= ownedDesk;
|
|
85
112
|
const controller = new AbortController();
|
|
86
113
|
const lifetime = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
87
|
-
|
|
88
|
-
const childSignal = () => AbortSignal.any([lifetime, timeController.signal]);
|
|
114
|
+
const childSignal = () => lifetime;
|
|
89
115
|
const key = `${options.cwd}:${agent.taskId}`;
|
|
90
116
|
const pool = pools.get(key) ?? { active: 0, users: 0, waiters: [] };
|
|
91
117
|
pool.users += 1;
|
|
92
118
|
pools.set(key, pool);
|
|
93
|
-
let deadlineAt = Date.now() + (options.time?.upAtMs ?? options.timeoutMs);
|
|
94
|
-
let deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
|
|
95
119
|
const children = new Map<string, AgentRun>();
|
|
96
120
|
const pending = new Set<Promise<AgentRun>>();
|
|
97
121
|
const parentDeskId = options.env?.[DESK_ENV.worker] ?? id;
|
|
@@ -109,18 +133,16 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
109
133
|
|
|
110
134
|
const childRun = async (brief: string, signal: AbortSignal): Promise<AgentRun> => {
|
|
111
135
|
const childId = randomUUID();
|
|
112
|
-
const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running",
|
|
136
|
+
const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), ...(options.thinking ? { thinking: options.thinking } : {}) };
|
|
113
137
|
const edits = new EditLog(options.cwd);
|
|
114
|
-
const live = createLiveRun(base, { onUpdate: update
|
|
138
|
+
const live = createLiveRun(base, { onUpdate: update }, edits);
|
|
115
139
|
update(base);
|
|
116
140
|
try {
|
|
117
141
|
const configured = options.effortProfile ?? { model: options.model, thinking: options.thinking ?? "medium" };
|
|
118
142
|
const effort = options.effort ?? effortFor();
|
|
119
143
|
const route = await effort.childRoute?.(brief, configured, { context: options.task, signal }).catch(() => undefined);
|
|
120
144
|
const attempt = async (model: string | undefined, thinking: string) => {
|
|
121
|
-
const left = deadlineAt - Date.now();
|
|
122
145
|
signal.throwIfAborted();
|
|
123
|
-
if (left <= 0) throw new Error("The parent's time is up.");
|
|
124
146
|
const routed = route && model === route.model && thinking === route.thinking;
|
|
125
147
|
live.annotate({ model, thinking, route: routed ? routeLabel(route) : undefined, routedFrom: routed ? profileLabel(route.from) : undefined, note: routed ? routeLabel(route) : undefined, noteKind: "info" });
|
|
126
148
|
return runPiAgent({
|
|
@@ -129,22 +151,19 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
129
151
|
task: brief,
|
|
130
152
|
model,
|
|
131
153
|
thinking,
|
|
132
|
-
tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL), ...(
|
|
154
|
+
tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL && tool !== EXPERT_TOOL), ...(childDesk ? childDesk.tools().filter((tool) => !PEER_TOOLS.includes(tool)) : [])])],
|
|
133
155
|
mcpTools,
|
|
134
|
-
timeoutMs: left,
|
|
135
156
|
signal,
|
|
136
|
-
time: undefined,
|
|
137
|
-
wrapUpAtMs: Math.floor(left * 0.85),
|
|
138
157
|
delegationQuota: undefined,
|
|
139
158
|
onChildRun: undefined,
|
|
140
159
|
onEvent: live.onEvent,
|
|
141
|
-
env: { ...options.env, ...(
|
|
142
|
-
onStart: (handle) =>
|
|
160
|
+
env: { ...options.env, ...(childDesk ? childDesk.env(childId) : {}) },
|
|
161
|
+
onStart: (handle) => childDesk?.attach(childId, { runId: childId, steer: handle.steer, annotate: live.annotate }),
|
|
143
162
|
}, run);
|
|
144
163
|
};
|
|
145
164
|
let result = await attempt(route?.model ?? configured.model, route?.thinking ?? configured.thinking);
|
|
146
165
|
let retried = false;
|
|
147
|
-
if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted
|
|
166
|
+
if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted) {
|
|
148
167
|
retried = true;
|
|
149
168
|
result = await attempt(configured.model, configured.thinking);
|
|
150
169
|
}
|
|
@@ -152,24 +171,88 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
152
171
|
update(final);
|
|
153
172
|
return final;
|
|
154
173
|
} catch (error) {
|
|
155
|
-
const final: AgentRun = { ...live.current(), edited: edits.list(), status:
|
|
174
|
+
const final: AgentRun = { ...live.current(), edited: edits.list(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
156
175
|
update(final);
|
|
157
176
|
return final;
|
|
158
177
|
} finally {
|
|
159
|
-
|
|
178
|
+
childDesk?.release(childId, () => "Child finished; re-read the file and check its report before continuing.");
|
|
160
179
|
}
|
|
161
180
|
};
|
|
162
181
|
|
|
182
|
+
// Kept on the quota, which a step's retries share, so a retry does not grant fresh consults.
|
|
183
|
+
quota.consults ??= 0;
|
|
184
|
+
const consultations = new Map<string, AbortController>();
|
|
185
|
+
const consult = async (question: string, signal: AbortSignal): Promise<{ ok: boolean; text: string }> => {
|
|
186
|
+
// The oracle resolves the expert (its session model when settings name none); settings alone, then the worker's own, without it.
|
|
187
|
+
const settled = options.expert ?? expertProfile(config);
|
|
188
|
+
const expert = { model: settled.model ?? options.model, thinking: settled.thinking };
|
|
189
|
+
const expertId = randomUUID();
|
|
190
|
+
const base: AgentRun = { runId: expertId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: "reviewer", instruction: question, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), thinking: expert.thinking, ...(expert.model ? { model: expert.model } : {}), note: "expert consult", noteKind: "info" };
|
|
191
|
+
const live = createLiveRun(base, { onUpdate: update }, new EditLog(options.cwd));
|
|
192
|
+
update(base);
|
|
193
|
+
let final: AgentRun;
|
|
194
|
+
try {
|
|
195
|
+
const result = await runPiAgent({
|
|
196
|
+
cwd: options.cwd,
|
|
197
|
+
agent: { ...agent, runId: expertId, role: "reviewer", depth: 2 },
|
|
198
|
+
task: [`A worker asks: ${question}`, `The worker's brief, for context:\n${options.task.slice(0, 8000)}`].join("\n\n"),
|
|
199
|
+
systemPrompt: loadPrompt("expert.md"),
|
|
200
|
+
tools: EXPERT_TOOLS,
|
|
201
|
+
mcpTools: [],
|
|
202
|
+
...(expert.model ? { model: expert.model } : {}),
|
|
203
|
+
thinking: expert.thinking,
|
|
204
|
+
...(options.stallTimeoutMs !== undefined ? { stallTimeoutMs: options.stallTimeoutMs } : {}),
|
|
205
|
+
...(options.toolStallTimeoutMs !== undefined ? { toolStallTimeoutMs: options.toolStallTimeoutMs } : {}),
|
|
206
|
+
signal,
|
|
207
|
+
onEvent: live.onEvent,
|
|
208
|
+
// Its own env: the expert is not one of the worker's children, nor on its desk or board.
|
|
209
|
+
env: { ...withoutTeamEnv(options.env), [EXPERT_ENV]: "1" },
|
|
210
|
+
}, run);
|
|
211
|
+
final = { ...live.current(), ...result, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
final = { ...live.current(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
214
|
+
}
|
|
215
|
+
update(final);
|
|
216
|
+
if (final.status === "success" && final.output.trim()) return { ok: true, text: final.output.trim().slice(0, 8000) };
|
|
217
|
+
// An expert that never answered does not use up a consult.
|
|
218
|
+
quota.consults = Math.max(0, (quota.consults ?? 1) - 1);
|
|
219
|
+
return { ok: false, text: `The expert did not answer (${final.status}${final.error ? `: ${final.error}` : ""}); carry on with your best reading.` };
|
|
220
|
+
};
|
|
221
|
+
|
|
163
222
|
const token = randomUUID();
|
|
164
223
|
const server = await startDeskServer(async (request) => {
|
|
165
224
|
if (request.worker !== token) return { ok: false, text: "Unknown parent agent." };
|
|
166
225
|
if (request.op === "cancel_delegate") { controller.abort(); return { ok: true, text: "Children cancelled." }; }
|
|
226
|
+
if (request.op === "cancel_consult") {
|
|
227
|
+
consultations.get(request.consultId ?? "")?.abort();
|
|
228
|
+
return { ok: true, text: "Consult cancelled." };
|
|
229
|
+
}
|
|
230
|
+
if (request.op === "consult") {
|
|
231
|
+
if (!consults) return { ok: false, text: "Consulting is off for this agent." };
|
|
232
|
+
if ((quota.consults ?? 0) >= MAX_CONSULTS) return { ok: false, text: `You have used your ${MAX_CONSULTS} consults for this step; decide with what you have, or report the open question.` };
|
|
233
|
+
const question = (request.text ?? "").trim();
|
|
234
|
+
if (!question) return { ok: false, text: "ask_expert needs a question." };
|
|
235
|
+
quota.consults = (quota.consults ?? 0) + 1;
|
|
236
|
+
// Its own controller, so the worker abandoning its tool call stops this expert and nothing else.
|
|
237
|
+
const own = new AbortController();
|
|
238
|
+
const parent = childSignal();
|
|
239
|
+
const stop = () => own.abort();
|
|
240
|
+
parent.addEventListener("abort", stop, { once: true });
|
|
241
|
+
const consultId = request.consultId ?? randomUUID();
|
|
242
|
+
consultations.set(consultId, own);
|
|
243
|
+
try {
|
|
244
|
+
return await consult(question.slice(0, 6000), own.signal);
|
|
245
|
+
} finally {
|
|
246
|
+
consultations.delete(consultId);
|
|
247
|
+
parent.removeEventListener("abort", stop);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
167
250
|
if (request.op !== "delegate") return { ok: false, text: "Unknown delegation operation." };
|
|
168
251
|
const signal = childSignal();
|
|
169
252
|
signal.throwIfAborted();
|
|
170
253
|
const briefs = reserveChildren(quota, request.briefs);
|
|
171
254
|
// A coordinating parent must release claims before waiting for children that may need them.
|
|
172
|
-
desk?.releaseFiles(parentDeskId, () => "Delegating this work; re-read before editing.");
|
|
255
|
+
for (const held of new Set([desk, childDesk])) held?.releaseFiles(parentDeskId, () => "Delegating this work; re-read before editing.");
|
|
173
256
|
const runs = briefs.map((brief) => {
|
|
174
257
|
const promise = inPool(key, Math.max(1, config.workflow.maxParallelWorkers), signal, () => childRun(brief, signal));
|
|
175
258
|
pending.add(promise);
|
|
@@ -179,7 +262,6 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
179
262
|
const results = await Promise.all(runs);
|
|
180
263
|
return { ok: true, text: JSON.stringify(results.map((child) => ({ runId: child.runId, status: child.status, report: child.output.slice(0, 12_000), reportPath: join(dataRoot(detectProjectRoot(options.cwd, CONFIG_DIR_NAME), CONFIG_DIR_NAME), "delegations", `${child.runId}.json`), error: child.error, usage: child.usage }))) };
|
|
181
264
|
}, () => controller.abort()).catch(async (error) => {
|
|
182
|
-
clearTimeout(deadline);
|
|
183
265
|
if (ownedDesk) await ownedDesk.close();
|
|
184
266
|
pool.users -= 1;
|
|
185
267
|
if (pool.users === 0) pools.delete(key);
|
|
@@ -188,30 +270,22 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
188
270
|
|
|
189
271
|
let closed = false;
|
|
190
272
|
return {
|
|
191
|
-
tools: desk ? [...new Set([...tools, ...
|
|
273
|
+
tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools,
|
|
192
274
|
mcpTools,
|
|
193
|
-
env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...
|
|
194
|
-
note: `${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children
|
|
275
|
+
env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools }), [DELEGATION_ENV]: JSON.stringify({ address: server.address, token }) },
|
|
276
|
+
note: `${consults ? `When you are stuck on a hard step after a real attempt, or about to make a choice you are unsure of (a tricky algorithm, a subtle bug, an unfamiliar API), ${EXPERT_TOOL} asks a stronger model for advice, at most ${MAX_CONSULTS} times; it reads the repository and answers, you still do the work. ` : ""}${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children cannot delegate further. Integrate and verify their work yourself. ` : ""}Use codemode for concise parallel calls, inspect failures, and return only useful evidence.${desk?.usesClaims ? " Before edit/write, claim_file; handover_file when finished. Release files before delegating or waiting." : ""}`,
|
|
195
277
|
onStart: (handle: Parameters<NonNullable<PiRunOptions["onStart"]>>[0]) => {
|
|
196
278
|
desk?.attach(parentDeskId, { runId: id, steer: handle.steer, annotate: () => {} });
|
|
197
279
|
options.onStart?.(handle);
|
|
198
280
|
},
|
|
199
281
|
get children() { return [...children.values()]; },
|
|
200
|
-
pause() { clearTimeout(deadline); },
|
|
201
|
-
extend(ms: number, reset = false) {
|
|
202
|
-
deadlineAt = reset ? Date.now() + ms : deadlineAt + ms;
|
|
203
|
-
clearTimeout(deadline);
|
|
204
|
-
if (timeController.signal.aborted) timeController = new AbortController();
|
|
205
|
-
deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
|
|
206
|
-
},
|
|
207
282
|
async close() {
|
|
208
283
|
if (closed) return;
|
|
209
284
|
closed = true;
|
|
210
285
|
controller.abort();
|
|
211
|
-
clearTimeout(deadline);
|
|
212
286
|
await Promise.allSettled([...pending]);
|
|
213
287
|
await server.close();
|
|
214
|
-
if (ownedDesk) {
|
|
288
|
+
if (ownedDesk) { ownedDesk.release(parentDeskId, () => "Parent finished; re-read before editing."); await ownedDesk.close(); }
|
|
215
289
|
pool.users -= 1;
|
|
216
290
|
if (pool.users === 0) pools.delete(key);
|
|
217
291
|
},
|
|
@@ -3,7 +3,7 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
3
3
|
import type { PiStreamEvent } from "./pi-runner.ts";
|
|
4
4
|
import { EditLog } from "../state/changes.ts";
|
|
5
5
|
import { activityDetail, activityWord, describeToolCall } from "../pi/activity.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { truncate } from "../text.ts";
|
|
7
7
|
|
|
8
8
|
const THROTTLE_MS = 2000;
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ const THROTTLE_MS = 2000;
|
|
|
12
12
|
* `onUpdate`: activity changes, retries and notes immediately, counters and
|
|
13
13
|
* heartbeats at most every couple of seconds.
|
|
14
14
|
*/
|
|
15
|
-
export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate"
|
|
15
|
+
export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate">, edits: EditLog) {
|
|
16
16
|
let state: AgentRun = base;
|
|
17
17
|
let lastEmit = 0;
|
|
18
18
|
const emit = (patch: Partial<AgentRun>, force: boolean) => {
|
|
@@ -55,22 +55,11 @@ export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpd
|
|
|
55
55
|
case "compaction_end":
|
|
56
56
|
emit({ note: event.error, noteKind: event.error ? "warning" : undefined }, true);
|
|
57
57
|
return;
|
|
58
|
-
case "wrap_up":
|
|
59
|
-
emit({ note: `asked to wrap up (${shortDuration(request.timeoutMs)} limit)`, noteKind: "warning", wrappedUp: true }, true);
|
|
60
|
-
return;
|
|
61
|
-
case "time_up":
|
|
62
|
-
emit({ note: request.time?.onTimeUp ? "out of time: reporting where it left off" : "out of time: reporting", noteKind: "warning" }, true);
|
|
63
|
-
return;
|
|
64
|
-
case "extended":
|
|
65
|
-
emit({ endsAt: request.time?.endsAt, extendedMs: (state.extendedMs ?? 0) + event.ms }, true);
|
|
66
|
-
return;
|
|
67
58
|
case "asking":
|
|
68
59
|
emit({ note: `waiting on you: ${event.questions} question${event.questions === 1 ? "" : "s"}`, noteKind: "info" }, true);
|
|
69
60
|
return;
|
|
70
61
|
case "answered":
|
|
71
|
-
|
|
72
|
-
if (request.time) request.time.endsAt += event.waitedMs;
|
|
73
|
-
emit({ note: "you answered; carrying on", noteKind: "info", ...(request.time ? { endsAt: request.time.endsAt } : {}) }, true);
|
|
62
|
+
emit({ note: "you answered; carrying on", noteKind: "info" }, true);
|
|
74
63
|
return;
|
|
75
64
|
case "usage": {
|
|
76
65
|
const usage = state.usage ?? { input: 0, output: 0, cost: 0, turns: 0 };
|