@a-t-h-i/bot-lobby 0.6.35 → 0.6.37
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 +20 -7
- package/package.json +1 -1
- package/prompts/master.md +7 -0
- package/src/classifier/effort.ts +14 -2
- package/src/execution/agent-runner.ts +73 -14
- package/src/execution/delegation.ts +34 -8
- package/src/execution/pi-runner.ts +9 -3
- package/src/lobby/quickfix.ts +2 -1
- package/src/lobby/service.ts +8 -1
- package/src/lobby/sessions.ts +1 -1
- package/src/master/master.ts +10 -5
- package/src/pi/auto-compaction.ts +4 -4
- package/src/pi/plan-checklist.ts +4 -2
- package/src/pi/start-task.ts +15 -9
- package/src/pi/task-title.ts +46 -0
- package/src/schemas/findings.ts +2 -0
- package/src/state/step-limits.ts +41 -0
- package/src/state/work-time.ts +16 -11
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/plan-facts.ts +3 -2
- package/src/webui/protocol.ts +1 -1
- package/src/workflow/workflow.ts +73 -34
- package/webui/dist/assets/index-BTk14vBb.css +2 -0
- package/webui/dist/assets/index-BwyOrEcV.js +110 -0
- package/webui/dist/build.json +98 -78
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-C8hgfw6m.css +0 -2
- package/webui/dist/assets/index-CoRTgsIG.js +0 -109
package/README.md
CHANGED
|
@@ -36,6 +36,11 @@ you to confirm merging and pushing main, then removes its worktree after verifyi
|
|
|
36
36
|
Declining or failed verification keeps the worktree. Delivery can also be reviewed in **Tasks**,
|
|
37
37
|
where you can create a PR instead. The task branch is kept.
|
|
38
38
|
|
|
39
|
+
New tasks get a short title summarizing the full request, which also names their branch and
|
|
40
|
+
session. Naming uses low reasoning on `classifier.effort.cheapModel` when configured, otherwise
|
|
41
|
+
the session model. If naming fails or takes more than eight seconds, the request's first content
|
|
42
|
+
words are used. Tasks started from an agreed plan keep that plan's title.
|
|
43
|
+
|
|
39
44
|
After every worker step, before QA and at completion, the engine runs the project's own linter
|
|
40
45
|
(ESLint, Biome, Oxlint, Ruff, or a command you set) on the files the task touched. Only problems on
|
|
41
46
|
changed lines count, and QA judges any lint suppression a worker adds. **Settings → Linting** sets it
|
|
@@ -126,25 +131,33 @@ are kept in `themes.json` beside it, so every Pi session and project shows them.
|
|
|
126
131
|
project knowledge stay in `.pi/bot-lobby/` inside your project.
|
|
127
132
|
|
|
128
133
|
|
|
129
|
-
Requires **Pi 1.0.x** (1.0.1 or later).
|
|
130
|
-
`delegate_subtasks` for
|
|
131
|
-
slots total, shared across retries;
|
|
134
|
+
Requires **Pi 1.0.x** (1.0.1 or later). DEV and DESIGN workers and QA reviewers can use
|
|
135
|
+
`delegate_subtasks` for useful, separable work. Delegation is optional. Each parent gets five child
|
|
136
|
+
slots total, shared across retries; Jev selects each child’s model and reasoning effort from its brief, lowering
|
|
137
|
+
inference for simple work and raising reasoning for complex work on the configured
|
|
138
|
+
model. Children inherit the parent’s role,
|
|
132
139
|
permissions, worktree and remaining time. Children report to their parent, which
|
|
133
140
|
integrates and verifies the result. Children cannot delegate further. Active
|
|
134
141
|
children share `workflow.maxParallelWorkers`; waiting parents do not use those
|
|
135
142
|
child slots. Cancelling or finishing a parent stops its children and releases
|
|
136
143
|
their file claims. Full reports are saved under `.pi/bot-lobby/delegations/`.
|
|
137
144
|
|
|
138
|
-
|
|
145
|
+
Every assignment has a 15-minute total allowance (or its shorter configured limit),
|
|
146
|
+
including startup, tools, retries, children, compaction and reporting. Per-step
|
|
147
|
+
allowances persist across reassignments; only the user can extend them, including
|
|
148
|
+
in auto mode. Waiting for user answers pauses clocks.
|
|
149
|
+
|
|
150
|
+
The conversation pane groups running agents beneath their current step, for example
|
|
139
151
|
**Step 6 – Implement large feature (6 devs working)**. Queued and finished agents
|
|
140
152
|
are excluded. A child finishing does not complete a checklist step.
|
|
141
153
|
|
|
142
154
|
The oracle, agents and children compact at settled turn boundaries before context
|
|
143
|
-
reaches
|
|
155
|
+
reaches 64,000 estimated tokens, or earlier at 60% of the model window or Pi's
|
|
144
156
|
native reserve threshold. Recent messages and summaries retain unfinished work;
|
|
145
157
|
raw session history remains available. A failed summary leaves context intact.
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
Checkpoints preserve constraints, decisions, changed files, verification, failed
|
|
159
|
+
approaches, and next actions. A single large input can exceed the threshold before
|
|
160
|
+
the next settled boundary.
|
|
148
161
|
|
|
149
162
|
Pi's native MCP, `tool_search` and `codemode` are available. **MCP tools** in each
|
|
150
163
|
agent's Settings entry grants exact tool names, one per line, such as
|
package/package.json
CHANGED
package/prompts/master.md
CHANGED
|
@@ -282,6 +282,13 @@ Every delegation costs a full agent run, so keep the loop short:
|
|
|
282
282
|
|
|
283
283
|
## Time budget
|
|
284
284
|
|
|
285
|
+
Every assignment has at most 15 minutes total, or its shorter configured limit,
|
|
286
|
+
including retries, children, tools, compaction and reporting. Step allowances
|
|
287
|
+
persist across reassignments: name the plan step explicitly, and do not reword
|
|
288
|
+
or split an exhausted step to reset its clock. Only the user can extend it,
|
|
289
|
+
including in auto mode. Return an unfinished report when time is spent; stop
|
|
290
|
+
repeating failed approaches. Waiting for the user pauses clocks.
|
|
291
|
+
|
|
285
292
|
When the task has a time budget (your context says how much is used and
|
|
286
293
|
left), it covers everyone: you, scouts, workers and the QA gate. The clock
|
|
287
294
|
runs while you work and stops while you wait on the user.
|
package/src/classifier/effort.ts
CHANGED
|
@@ -30,9 +30,9 @@ export interface RunProfileLike {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface EffortRoute extends RunProfileLike {
|
|
33
|
-
level:
|
|
33
|
+
level: EffortLevel;
|
|
34
34
|
confidence: number;
|
|
35
|
-
/** The configured profile
|
|
35
|
+
/** The configured profile before routing. */
|
|
36
36
|
from: RunProfileLike;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -109,6 +109,8 @@ export interface EffortScore {
|
|
|
109
109
|
|
|
110
110
|
/** Lowers the model or thinking of a step the classifier judges simple or trivial. */
|
|
111
111
|
export interface EffortRouter {
|
|
112
|
+
/** Children may raise reasoning for complex work, using the parent's configured model. */
|
|
113
|
+
childRoute?(instruction: string, profile: RunProfileLike, options?: { context?: string; signal?: AbortSignal }): Promise<EffortRoute | undefined>;
|
|
112
114
|
/** Score a step and plan its route in one go. */
|
|
113
115
|
route(instruction: string, profile: RunProfileLike, options?: { thinkingFixed?: boolean; context?: string; signal?: AbortSignal }): Promise<EffortRoute | undefined>;
|
|
114
116
|
/** Score a step once (several agents may share it)… */
|
|
@@ -119,6 +121,16 @@ export interface EffortRouter {
|
|
|
119
121
|
|
|
120
122
|
export function effortRouter(classifier: Classifier, deps: { clamp?: RouteOptions["clamp"]; log?: (text: string) => void } = {}): EffortRouter {
|
|
121
123
|
const router: EffortRouter = {
|
|
124
|
+
async childRoute(instruction, profile, options = {}) {
|
|
125
|
+
const scored = await router.score(instruction, options);
|
|
126
|
+
if (!scored) return undefined;
|
|
127
|
+
if (scored.level !== "complex" || scored.confidence < classifier.config.thresholds.simpleAt) return router.plan(scored, profile);
|
|
128
|
+
const thinking = profile.model && deps.clamp ? deps.clamp(profile.model, "max") : rank(profile.thinking) > rank("high") ? profile.thinking : "high";
|
|
129
|
+
if (thinking === profile.thinking) return undefined;
|
|
130
|
+
const route: EffortRoute = { ...profile, thinking, level: scored.level, confidence: scored.confidence, from: { ...profile } };
|
|
131
|
+
deps.log?.(`routed child ${routeLabel(route)}`);
|
|
132
|
+
return route;
|
|
133
|
+
},
|
|
122
134
|
async score(instruction, options = {}) {
|
|
123
135
|
if (!classifier.enabled("effort")) return undefined;
|
|
124
136
|
return scoreEffort(classifier, instruction, options.context, options.signal);
|
|
@@ -8,6 +8,8 @@ import { EditLog } from "../state/changes.ts";
|
|
|
8
8
|
import { formatMinutes, REPORT_GRACE_MS } from "../state/budget.ts";
|
|
9
9
|
import { runPiAgent, spawnPiProcess, type ProcessRunner, type RelayAsk } from "./pi-runner.ts";
|
|
10
10
|
import { ASK_ENV } from "../ask/relay.ts";
|
|
11
|
+
import { STEP_LIMIT_MS } from "../state/step-limits.ts";
|
|
12
|
+
import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
|
|
11
13
|
import { ASK_TOOL } from "../ask/types.ts";
|
|
12
14
|
import { agentOfRun, grantOption } from "../excalidraw/sessions.ts";
|
|
13
15
|
|
|
@@ -22,7 +24,7 @@ export interface AgentContext {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Live fields a caller (the file desk) may set on a running agent. */
|
|
25
|
-
export type LivePatch = Pick<AgentRun, "note" | "noteKind" | "waitingFor">;
|
|
27
|
+
export type LivePatch = Pick<AgentRun, "note" | "noteKind" | "waitingFor" | "model" | "thinking" | "route" | "routedFrom">;
|
|
26
28
|
|
|
27
29
|
/** Control over one running attempt, handed to `onStart`. */
|
|
28
30
|
export interface AgentHandle {
|
|
@@ -35,6 +37,8 @@ export interface AgentRequest {
|
|
|
35
37
|
taskId: string;
|
|
36
38
|
domain: Domain;
|
|
37
39
|
role: Role;
|
|
40
|
+
effort?: EffortRouter;
|
|
41
|
+
effortProfile?: RunProfileLike;
|
|
38
42
|
/** Concrete instruction sent as the subagent's task message. */
|
|
39
43
|
instruction: string;
|
|
40
44
|
/** Prompt layers selected for this run. */
|
|
@@ -78,6 +82,10 @@ export interface AgentRequest {
|
|
|
78
82
|
* (the engine asking the user) grants more, and the same agent carries on.
|
|
79
83
|
*/
|
|
80
84
|
export interface AgentTime {
|
|
85
|
+
/** Active time shared across attempts and model fallbacks. */
|
|
86
|
+
usedMs?: number;
|
|
87
|
+
/** Include reporting in the hard allowance. */
|
|
88
|
+
strict?: boolean;
|
|
81
89
|
/** When the delegation's time runs out (epoch ms); moved out when more is granted, shared by retries and re-runs. */
|
|
82
90
|
endsAt: number;
|
|
83
91
|
/** The allotment, as the agent and the lobby see it. */
|
|
@@ -108,6 +116,11 @@ export function carryOnMessage(extraMs: number): string {
|
|
|
108
116
|
/** Least time an attempt starts with; a retry with less than this left is not started. */
|
|
109
117
|
const MIN_ATTEMPT_MS = 60_000;
|
|
110
118
|
|
|
119
|
+
export function boundedAgentTime(timeoutMs: number): AgentTime {
|
|
120
|
+
const allotMs = Math.min(STEP_LIMIT_MS, timeoutMs);
|
|
121
|
+
return { endsAt: Date.now() + allotMs, allotMs, strict: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
/** The watchdog fields of an AgentRequest, taken from the workflow config. */
|
|
112
125
|
export function watchdogOptions(workflow: {
|
|
113
126
|
maxAgentRetries: number;
|
|
@@ -137,7 +150,19 @@ function retryable(run: AgentRun): boolean {
|
|
|
137
150
|
* and a slow agent cannot double its time budget.
|
|
138
151
|
*/
|
|
139
152
|
export async function runAgent(request: AgentRequest, run: ProcessRunner = spawnPiProcess): Promise<AgentRun> {
|
|
153
|
+
request.time ??= boundedAgentTime(request.timeoutMs);
|
|
154
|
+
if (!request.time.strict) {
|
|
155
|
+
request.time.endsAt = Math.min(request.time.endsAt, Date.now() + Math.min(STEP_LIMIT_MS, request.timeoutMs));
|
|
156
|
+
request.time.allotMs = Math.min(request.time.allotMs, STEP_LIMIT_MS, request.timeoutMs);
|
|
157
|
+
request.time.strict = true;
|
|
158
|
+
}
|
|
159
|
+
request.time.usedMs ??= Math.max(0, request.time.allotMs - (request.time.endsAt - Date.now()));
|
|
140
160
|
const startedAt = new Date().toISOString();
|
|
161
|
+
if (request.time.endsAt <= Date.now()) {
|
|
162
|
+
const stopped: AgentRun = { ...baseRun(request, `${request.taskId}:${randomUUID()}`, startedAt), status: "timeout", error: "The assignment's shared time limit is spent; request user-approved time before continuing.", finishedAt: startedAt, timeUp: true };
|
|
163
|
+
request.onUpdate?.(stopped);
|
|
164
|
+
return stopped;
|
|
165
|
+
}
|
|
141
166
|
const attempts = Math.max(1, (request.retries ?? 0) + 1);
|
|
142
167
|
// A failed attempt may have edited files before the retry: the run owns every edit.
|
|
143
168
|
const edits = new EditLog(request.cwd);
|
|
@@ -193,21 +218,36 @@ function toolsFor(request: AgentRequest): readonly string[] | undefined {
|
|
|
193
218
|
async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt: number, startedAt: string, edits: EditLog, quota: { used: number }): Promise<AgentRun> {
|
|
194
219
|
const base = baseRun(request, `${request.taskId}:${request.domain}:${request.role}:${randomUUID()}`, startedAt, attempt);
|
|
195
220
|
request.onUpdate?.(base);
|
|
196
|
-
const
|
|
221
|
+
const time = request.time!;
|
|
222
|
+
const attemptStart = Date.now();
|
|
223
|
+
let pausedMs = 0;
|
|
224
|
+
let asking = 0;
|
|
225
|
+
let askStarted = 0;
|
|
226
|
+
let expired = false;
|
|
227
|
+
const elapsed = () => Math.max(0, Date.now() - attemptStart - pausedMs - (asking ? Date.now() - askStarted : 0));
|
|
228
|
+
const live = createLiveRun(base, { ...request, onUpdate: (update) => request.onUpdate?.({ ...update, activeMs: (time.usedMs ?? 0) + elapsed(), endsAt: time.endsAt }) }, edits);
|
|
197
229
|
|
|
198
230
|
const controller = new AbortController();
|
|
199
231
|
activeControllers.add(controller);
|
|
200
232
|
const signal = request.signal ? AbortSignal.any([request.signal, controller.signal]) : controller.signal;
|
|
233
|
+
let deadline: ReturnType<typeof setTimeout>;
|
|
234
|
+
const arm = () => {
|
|
235
|
+
clearTimeout(deadline);
|
|
236
|
+
deadline = setTimeout(() => { expired = true; controller.abort(new Error("Assignment time limit reached")); }, Math.max(0, time.endsAt - Date.now()));
|
|
237
|
+
};
|
|
238
|
+
arm();
|
|
201
239
|
try {
|
|
202
240
|
const systemPrompt = compilePrompt({ domain: request.domain, role: request.role, ...request.context });
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const wrapUp =
|
|
241
|
+
const left = Math.max(1, time.endsAt - Date.now());
|
|
242
|
+
const reportMs = Math.min(REPORT_GRACE_MS, Math.floor(left / 4));
|
|
243
|
+
const wrapUp = Math.floor(left * (request.wrapUpAt || 0.75));
|
|
206
244
|
const onTimeUp = time?.onTimeUp;
|
|
207
245
|
const result = await runPiAgent({
|
|
208
246
|
cwd: request.cwd,
|
|
209
247
|
agent: { runId: base.runId, taskId: request.taskId, domain: request.domain, role: request.role },
|
|
210
248
|
delegationQuota: quota,
|
|
249
|
+
effort: request.effort,
|
|
250
|
+
effortProfile: request.effortProfile,
|
|
211
251
|
onChildRun: (child) => {
|
|
212
252
|
for (const path of child.edited ?? []) edits.note("edit", { path });
|
|
213
253
|
request.onUpdate?.(child);
|
|
@@ -218,21 +258,28 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
|
|
|
218
258
|
tools: toolsFor(request),
|
|
219
259
|
model: request.model,
|
|
220
260
|
thinking: request.thinking,
|
|
221
|
-
timeoutMs:
|
|
261
|
+
timeoutMs: left,
|
|
222
262
|
...(time ? {
|
|
223
263
|
time: {
|
|
224
|
-
|
|
264
|
+
hardLimit: true,
|
|
265
|
+
upAtMs: Math.max(1, left - reportMs),
|
|
225
266
|
headsUpAtMs: left >= 4 * 60_000 ? Math.floor(left * 0.75) : 0,
|
|
226
267
|
headsUpMessage: headsUpMessage(left - Math.floor(left * 0.75), time.allotMs),
|
|
227
|
-
upMessage:
|
|
228
|
-
graceMs:
|
|
268
|
+
upMessage: `bot-lobby: your shared step allowance is nearly spent. Start nothing new; finish or revert the edit in progress and report completed work, verification, failed approaches, blockers, and the next action now.${onTimeUp ? " Include ## Left Off and ## More Time (minutes and reason). Only the user can grant more time." : ""}`,
|
|
269
|
+
graceMs: reportMs,
|
|
229
270
|
...(onTimeUp ? {
|
|
230
271
|
onTimeUp: async (report: string) => {
|
|
231
|
-
|
|
272
|
+
clearTimeout(deadline);
|
|
273
|
+
const waiting = Date.now();
|
|
274
|
+
let extraMs: number;
|
|
275
|
+
try { extraMs = await onTimeUp(live.current(), report); }
|
|
276
|
+
finally { pausedMs += Date.now() - waiting; }
|
|
277
|
+
time.endsAt += Date.now() - waiting;
|
|
232
278
|
if (extraMs > 0) {
|
|
233
279
|
time.endsAt = Date.now() + extraMs;
|
|
234
280
|
live.annotate({ note: `given ${formatMinutes(extraMs)} more`, noteKind: "info" });
|
|
235
281
|
}
|
|
282
|
+
arm();
|
|
236
283
|
return { extraMs, message: carryOnMessage(extraMs) };
|
|
237
284
|
},
|
|
238
285
|
} : {}),
|
|
@@ -245,30 +292,42 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
|
|
|
245
292
|
stallTimeoutMs: request.stallTimeoutMs,
|
|
246
293
|
toolStallTimeoutMs: request.toolStallTimeoutMs,
|
|
247
294
|
wrapUpAtMs: wrapUp,
|
|
248
|
-
onEvent:
|
|
295
|
+
onEvent: (event) => {
|
|
296
|
+
if (event.type === "asking") { if (!asking) askStarted = Date.now(); asking += 1; clearTimeout(deadline); }
|
|
297
|
+
if (event.type === "answered") { asking = 0; pausedMs += event.waitedMs; }
|
|
298
|
+
live.onEvent(event);
|
|
299
|
+
if (event.type === "answered" && asking === 0) arm();
|
|
300
|
+
},
|
|
249
301
|
onStart: (handle) => request.onStart?.({ runId: base.runId, steer: handle.steer, annotate: live.annotate }),
|
|
250
302
|
}, run);
|
|
251
303
|
const current = live.current();
|
|
252
304
|
const final: AgentRun = {
|
|
253
305
|
...base,
|
|
254
|
-
status: result.status,
|
|
306
|
+
status: expired ? "timeout" : result.status,
|
|
255
307
|
output: result.output,
|
|
256
|
-
error: result.error ? describeError(result.error, current, result.stalled === true) : undefined,
|
|
308
|
+
error: expired ? describeError("hit the shared assignment time limit", current, false) : result.error ? describeError(result.error, current, result.stalled === true) : undefined,
|
|
257
309
|
usage: result.usage,
|
|
258
310
|
finishedAt: new Date().toISOString(),
|
|
311
|
+
activeMs: (time.usedMs ?? 0) + elapsed(),
|
|
259
312
|
turns: current.turns ?? (result.usage.turns || undefined),
|
|
260
313
|
tools: current.tools,
|
|
261
314
|
model: result.model ?? current.model,
|
|
262
315
|
...(result.stalled ? { stalled: true } : {}),
|
|
263
316
|
...(result.wrappedUp ? { wrappedUp: true } : {}),
|
|
264
|
-
...(result.timeUp ? { timeUp: true } : {}),
|
|
317
|
+
...(result.timeUp || expired ? { timeUp: true } : {}),
|
|
265
318
|
...(result.extendedMs ? { extendedMs: result.extendedMs } : {}),
|
|
266
319
|
note: undefined,
|
|
267
320
|
noteKind: undefined,
|
|
268
321
|
};
|
|
269
322
|
request.onUpdate?.(final);
|
|
270
323
|
return final;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const final: AgentRun = { ...live.current(), status: expired ? "timeout" : signal.aborted ? "cancelled" : "failed", error: (error as Error).message, activeMs: (time.usedMs ?? 0) + elapsed(), edited: edits.list(), finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined, ...(expired ? { timeUp: true } : {}) };
|
|
326
|
+
request.onUpdate?.(final);
|
|
327
|
+
return final;
|
|
271
328
|
} finally {
|
|
329
|
+
clearTimeout(deadline!);
|
|
330
|
+
time.usedMs = (time.usedMs ?? 0) + elapsed();
|
|
272
331
|
activeControllers.delete(controller);
|
|
273
332
|
}
|
|
274
333
|
}
|
|
@@ -5,6 +5,9 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
8
|
+
import type { Domain, Role } from "../schemas/agent.ts";
|
|
9
|
+
import { effortFor } from "../classifier/instance.ts";
|
|
10
|
+
import { fellShort, profileLabel, routeLabel } from "../classifier/effort.ts";
|
|
8
11
|
import { agentProfile } from "../schemas/configuration.ts";
|
|
9
12
|
import { dataRoot, detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
10
13
|
import { EditLog } from "../state/changes.ts";
|
|
@@ -18,6 +21,13 @@ export const DELEGATE_TOOL = "delegate_subtasks";
|
|
|
18
21
|
export const AGENT_ENV = "BOT_LOBBY_AGENT_POLICY";
|
|
19
22
|
export const DELEGATION_ENV = "BOT_LOBBY_DELEGATION";
|
|
20
23
|
|
|
24
|
+
/** Only DESIGN/DEV workers and the QA gate may spawn children. */
|
|
25
|
+
export function canDelegate(agent: { role: Role; domain: Domain; depth?: number }, quotaUsed: number): boolean {
|
|
26
|
+
if ((agent.depth ?? 1) !== 1 || quotaUsed >= MAX_CHILDREN) return false;
|
|
27
|
+
if (agent.role === "worker") return agent.domain === "designer" || agent.domain === "backend";
|
|
28
|
+
return agent.role === "reviewer" && agent.domain === "qa";
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
/** One shared child pool per task; parents waiting on children never hold its slots. */
|
|
22
32
|
const pools = new Map<string, { active: number; users: number; waiters: Array<() => void> }>();
|
|
23
33
|
|
|
@@ -63,7 +73,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
63
73
|
const agent = options.agent ?? { runId: id, taskId: id, domain: "backend" as const, role: editable ? "worker" as const : "reviewer" as const };
|
|
64
74
|
const depth = agent.depth ?? 1;
|
|
65
75
|
const quota = options.delegationQuota ?? { used: 0 };
|
|
66
|
-
const tools = [...new Set([...baseTools, ...(options.excalidraw ? toolsOf(options.excalidraw) : []), "codemode", "tool_search", ...(
|
|
76
|
+
const tools = [...new Set([...baseTools, ...(options.excalidraw ? toolsOf(options.excalidraw) : []), "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : [])])];
|
|
67
77
|
const mcpTools = options.mcpTools ?? agentProfile(config, agent.domain, agent.role).mcpTools ?? [];
|
|
68
78
|
const policy: AgentPolicy = { tools, mcpTools, readOnly: !editable, depth };
|
|
69
79
|
if (depth > 1) return { tools, 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,17 +110,26 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
100
110
|
|
|
101
111
|
const childRun = async (brief: string, signal: AbortSignal): Promise<AgentRun> => {
|
|
102
112
|
const childId = randomUUID();
|
|
103
|
-
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
|
+
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", endsAt: deadlineAt, output: "", attempts: 1, startedAt: new Date().toISOString(), ...(options.thinking ? { thinking: options.thinking } : {}) };
|
|
104
114
|
const edits = new EditLog(options.cwd);
|
|
105
115
|
const live = createLiveRun(base, { onUpdate: update, timeoutMs: options.timeoutMs }, edits);
|
|
106
116
|
update(base);
|
|
107
117
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const
|
|
118
|
+
const configured = options.effortProfile ?? { model: options.model, thinking: options.thinking ?? "medium" };
|
|
119
|
+
const effort = options.effort ?? effortFor();
|
|
120
|
+
const route = await effort.childRoute?.(brief, configured, { context: options.task, signal }).catch(() => undefined);
|
|
121
|
+
const attempt = async (model: string | undefined, thinking: string) => {
|
|
122
|
+
const left = deadlineAt - Date.now();
|
|
123
|
+
signal.throwIfAborted();
|
|
124
|
+
if (left <= 0) throw new Error("The parent's time is up.");
|
|
125
|
+
const routed = route && model === route.model && thinking === route.thinking;
|
|
126
|
+
live.annotate({ model, thinking, route: routed ? routeLabel(route) : undefined, routedFrom: routed ? profileLabel(route.from) : undefined, note: routed ? routeLabel(route) : undefined, noteKind: "info" });
|
|
127
|
+
return runPiAgent({
|
|
111
128
|
...options,
|
|
112
129
|
agent: { ...agent, runId: childId, depth: 2 },
|
|
113
130
|
task: brief,
|
|
131
|
+
model,
|
|
132
|
+
thinking,
|
|
114
133
|
tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL), ...(desk ? DESK_TOOLS : [])])],
|
|
115
134
|
mcpTools,
|
|
116
135
|
timeoutMs: left,
|
|
@@ -123,11 +142,18 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
123
142
|
env: { ...options.env, ...(desk ? desk.env(childId) : {}) },
|
|
124
143
|
onStart: (handle) => desk?.attach(childId, { runId: childId, steer: handle.steer, annotate: live.annotate }),
|
|
125
144
|
}, run);
|
|
126
|
-
|
|
145
|
+
};
|
|
146
|
+
let result = await attempt(route?.model ?? configured.model, route?.thinking ?? configured.thinking);
|
|
147
|
+
let retried = false;
|
|
148
|
+
if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted && deadlineAt - Date.now() >= 60_000) {
|
|
149
|
+
retried = true;
|
|
150
|
+
result = await attempt(configured.model, configured.thinking);
|
|
151
|
+
}
|
|
152
|
+
const final: AgentRun = { ...live.current(), ...result, thinking: retried ? configured.thinking : route?.thinking ?? configured.thinking, attempts: retried ? 2 : 1, ...(route && !retried ? { route: routeLabel(route), routedFrom: profileLabel(route.from) } : {}), edited: edits.list(), finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
127
153
|
update(final);
|
|
128
154
|
return final;
|
|
129
155
|
} catch (error) {
|
|
130
|
-
const final: AgentRun = { ...live.current(), edited: edits.list(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString() };
|
|
156
|
+
const final: AgentRun = { ...live.current(), edited: edits.list(), status: Date.now() >= deadlineAt ? "timeout" : signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
131
157
|
update(final);
|
|
132
158
|
return final;
|
|
133
159
|
} finally {
|
|
@@ -165,7 +191,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
165
191
|
return {
|
|
166
192
|
tools: desk ? [...new Set([...tools, ...DESK_TOOLS])] : tools,
|
|
167
193
|
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, timeoutMs: options.timeoutMs }) },
|
|
168
|
-
note: `
|
|
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 share your remaining time and cannot delegate further. Integrate and verify their work yourself. ` : ""}Use codemode for concise parallel calls, inspect failures, and return only useful evidence.${desk ? " Before edit/write, claim_file; handover_file when finished. Release files before delegating or waiting." : ""}`,
|
|
169
195
|
onStart: (handle: Parameters<NonNullable<PiRunOptions["onStart"]>>[0]) => {
|
|
170
196
|
desk?.attach(parentDeskId, { runId: id, steer: handle.steer, annotate: () => {} });
|
|
171
197
|
options.onStart?.(handle);
|
|
@@ -11,6 +11,7 @@ import { shortDuration } from "../text.ts";
|
|
|
11
11
|
import { readRelayRequest } from "../ask/relay.ts";
|
|
12
12
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
13
13
|
import { grantEnv, grantNote, type Grant } from "../excalidraw/sessions.ts";
|
|
14
|
+
import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
|
|
14
15
|
|
|
15
16
|
/** Live control over one running subagent: queue a steering message for its next turn. */
|
|
16
17
|
export interface RunHandle {
|
|
@@ -24,6 +25,8 @@ export interface RunHandle {
|
|
|
24
25
|
export type RelayAsk = (questions: AskQuestion[], signal: AbortSignal) => Promise<AskResult>;
|
|
25
26
|
|
|
26
27
|
export interface PiRunOptions {
|
|
28
|
+
effort?: EffortRouter;
|
|
29
|
+
effortProfile?: RunProfileLike;
|
|
27
30
|
agent?: { runId: string; taskId: string; domain: Domain; role: Role; depth?: number };
|
|
28
31
|
mcpTools?: string[];
|
|
29
32
|
delegationQuota?: { used: number };
|
|
@@ -67,6 +70,8 @@ export interface PiRunOptions {
|
|
|
67
70
|
* the decision is pending, and move out with any time granted.
|
|
68
71
|
*/
|
|
69
72
|
export interface TimeLimit {
|
|
73
|
+
/** Reporting time is included in the allowance, including extensions. */
|
|
74
|
+
hardLimit?: boolean;
|
|
70
75
|
upAtMs: number;
|
|
71
76
|
headsUpAtMs?: number;
|
|
72
77
|
headsUpMessage?: string;
|
|
@@ -584,11 +589,12 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
584
589
|
turnDone = false;
|
|
585
590
|
clockStart = Date.now();
|
|
586
591
|
lastOutput = clockStart;
|
|
587
|
-
upAtMs = extraMs;
|
|
592
|
+
upAtMs = time?.hardLimit ? Math.max(1, extraMs - Math.min(time.graceMs, Math.floor(extraMs / 4))) : extraMs;
|
|
588
593
|
// The user just named the time it has: no heads-up quoting the old allotment.
|
|
589
594
|
headsUpAtMs = 0;
|
|
590
|
-
|
|
591
|
-
|
|
595
|
+
const nextLimit = extraMs + (time?.hardLimit ? 0 : time?.graceMs ?? 0);
|
|
596
|
+
deadlineAt = Date.now() + nextLimit;
|
|
597
|
+
deadline = setTimeout(onDeadline, nextLimit);
|
|
592
598
|
input.send({ type: "prompt", message: message ?? "You have more time: carry on from where you left off and finish." });
|
|
593
599
|
options.onEvent?.({ type: "extended", ms: extraMs });
|
|
594
600
|
}, () => {
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
8
8
|
*/
|
|
9
9
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
10
10
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
11
|
+
import { MAX_CHILDREN } from "../execution/delegation.ts";
|
|
11
12
|
import { grantOption } from "../excalidraw/sessions.ts";
|
|
12
13
|
import { describeToolCall } from "../pi/activity.ts";
|
|
13
14
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
@@ -298,7 +299,7 @@ export class QuickFixQueue {
|
|
|
298
299
|
this.edits.set(job.id, new EditLog(this.deps.cwd));
|
|
299
300
|
try {
|
|
300
301
|
const [likely, route] = await Promise.all([this.likely(job.prompt, controller.signal), this.route(job.prompt, profile, controller.signal)]);
|
|
301
|
-
const quota = { used:
|
|
302
|
+
const quota = { used: MAX_CHILDREN };
|
|
302
303
|
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
303
304
|
{
|
|
304
305
|
cwd: this.deps.cwd,
|
package/src/lobby/service.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { SessionManager, type ExtensionContext } from "@earendil-works/pi-coding
|
|
|
7
7
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
8
8
|
import { updateBlockingRequest } from "../state/blocking-requests.ts";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { loadTask, ownedTask, peekTasks, saveTask } from "../state/persistence.ts";
|
|
10
|
+
import { loadTask, ownedTask, peekOwnedTask, peekTasks, saveTask } from "../state/persistence.ts";
|
|
11
11
|
import { taskWork } from "../state/work-time.ts";
|
|
12
12
|
import { refreshReview, deferReview } from "../delivery/review.ts";
|
|
13
13
|
import { deliver, type DeliveryRequest } from "../delivery/operations.ts";
|
|
@@ -221,6 +221,7 @@ function sessionsChanged(): void {
|
|
|
221
221
|
const seen = announced.get(session.key) ?? 0;
|
|
222
222
|
announced.set(session.key, session.dialogs.length);
|
|
223
223
|
if (!state) continue;
|
|
224
|
+
syncTaskName(state, session);
|
|
224
225
|
if (session.dialogs.length > seen) lobbyNotify(state, `bot-lobby: ${session.name} is waiting for you — see the Sessions page`, "info");
|
|
225
226
|
if (!session.alive && !exitsSaid.has(session.key)) {
|
|
226
227
|
exitsSaid.add(session.key);
|
|
@@ -229,6 +230,12 @@ function sessionsChanged(): void {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
function syncTaskName(state: Runtime, session: BackgroundSession): void {
|
|
234
|
+
if (!session.sessionId) return;
|
|
235
|
+
const task = peekOwnedTask(state.root, state.configDir, session.sessionId);
|
|
236
|
+
if (task) session.name = task.id;
|
|
237
|
+
}
|
|
238
|
+
|
|
232
239
|
function dialogTracker(state: Runtime): (id: string, pending: boolean, sessionId?: string) => void {
|
|
233
240
|
const requests = new Map<string, { taskId: string; sessionId: string; requestId: string }>();
|
|
234
241
|
const prefix = `dialog:${randomUUID()}`;
|
package/src/lobby/sessions.ts
CHANGED
|
@@ -64,7 +64,7 @@ export const STOP_GRACE_MS = 5000;
|
|
|
64
64
|
export class BackgroundSession {
|
|
65
65
|
readonly key: string;
|
|
66
66
|
readonly projectRoot?: string;
|
|
67
|
-
|
|
67
|
+
name: string;
|
|
68
68
|
/** The planned task it was started from. */
|
|
69
69
|
readonly planId?: string;
|
|
70
70
|
/** The task it was started to carry on. */
|
package/src/master/master.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schema
|
|
|
4
4
|
import type { Domain } from "../schemas/agent.ts";
|
|
5
5
|
import type { AgentRun, ReviewResult, ScoutResult, WorkerResult } from "../schemas/findings.ts";
|
|
6
6
|
import { domainSpec } from "../agents/registry.ts";
|
|
7
|
-
import { runAgent, runParallel, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
7
|
+
import { boundedAgentTime, runAgent, runParallel, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
8
8
|
import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
|
|
9
9
|
import { knowledgeFilePath, readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
|
|
10
10
|
import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
@@ -52,7 +52,7 @@ export interface ScoutRequest {
|
|
|
52
52
|
/** Model, thinking and time limit for one run, from settings. */
|
|
53
53
|
function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
|
|
54
54
|
const profile = profileFor(config, resolver, domain, role);
|
|
55
|
-
return { model: profile.model, mcpTools: profile.mcpTools, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
55
|
+
return { model: profile.model, mcpTools: profile.mcpTools, thinking: profile.thinking, timeoutMs: profile.timeoutMs, time: boundedAgentTime(profile.timeoutMs) };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
|
@@ -142,6 +142,7 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
142
142
|
const extraTools = request.hints?.tools() ?? [];
|
|
143
143
|
const requests: AgentRequest[] = request.domains.map((domain, index) => ({
|
|
144
144
|
delegationQuota: { used: 0 },
|
|
145
|
+
effort: request.effort,
|
|
145
146
|
taskId: request.taskId,
|
|
146
147
|
domain,
|
|
147
148
|
role: "scout",
|
|
@@ -161,7 +162,7 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
161
162
|
const runs = await runParallel(first, request.config.workflow.maxParallelScouts, run);
|
|
162
163
|
let outcomes = runs.map((agentRun) => toOutcome(agentRun, agentRun.domain));
|
|
163
164
|
// A routed scout that came back unusable runs again on its configured model.
|
|
164
|
-
const again = outcomes.map((outcome, index) => (routes[index] && !outcome.usable && outcome.run.status !== "cancelled" && !request.signal?.aborted ? index : -1)).filter((index) => index >= 0);
|
|
165
|
+
const again = outcomes.map((outcome, index) => (routes[index] && !outcome.usable && outcome.run.status !== "cancelled" && !request.signal?.aborted && requests[index]!.time!.endsAt > Date.now() ? index : -1)).filter((index) => index >= 0);
|
|
165
166
|
if (again.length > 0) {
|
|
166
167
|
const reruns = await runParallel(again.map((index) => requests[index]!), request.config.workflow.maxParallelScouts, run);
|
|
167
168
|
outcomes = outcomes.map((outcome, index) => {
|
|
@@ -288,6 +289,8 @@ export async function runWorker(
|
|
|
288
289
|
const extraTools = [...(request.agent?.extraTools ?? []), ...hintTools];
|
|
289
290
|
const base: AgentRequest = {
|
|
290
291
|
delegationQuota: { used: 0 },
|
|
292
|
+
effort: request.effort,
|
|
293
|
+
effortProfile: configured,
|
|
291
294
|
taskId: request.taskId,
|
|
292
295
|
domain: request.domain,
|
|
293
296
|
role: "worker",
|
|
@@ -311,7 +314,7 @@ export async function runWorker(
|
|
|
311
314
|
};
|
|
312
315
|
let outcome = workerOutcome(request.domain, await runAgent(route ? { ...base, ...routedFields(route) } : base, run));
|
|
313
316
|
// A routed step that fell short runs again at the configured model and thinking.
|
|
314
|
-
if (route && fellShort(outcome.run, outcome.issues) && !request.signal?.aborted) {
|
|
317
|
+
if (route && fellShort(outcome.run, outcome.issues) && !request.signal?.aborted && base.time!.endsAt > Date.now()) {
|
|
315
318
|
const routed = outcome.run.edited ?? [];
|
|
316
319
|
outcome = workerOutcome(request.domain, await runAgent(base, run));
|
|
317
320
|
// The step owns what the routed attempt edited too.
|
|
@@ -337,6 +340,7 @@ export interface ReviewerOutcome {
|
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
export interface ReviewerRequest {
|
|
343
|
+
effort?: EffortRouter;
|
|
340
344
|
taskId: string;
|
|
341
345
|
domain: Domain;
|
|
342
346
|
taskText: string;
|
|
@@ -400,6 +404,7 @@ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelec
|
|
|
400
404
|
taskId: request.taskId,
|
|
401
405
|
domain: request.domain,
|
|
402
406
|
role: "reviewer",
|
|
407
|
+
effort: request.effort,
|
|
403
408
|
instruction:
|
|
404
409
|
request.instruction?.trim() ||
|
|
405
410
|
"Review the current repository changes against the approved requirements and plan.",
|
|
@@ -434,7 +439,7 @@ export async function runReviewer(
|
|
|
434
439
|
const agentRequest = reviewerAgentRequest(request, selected);
|
|
435
440
|
const attempts = Math.max(1, request.config.workflow.maxAgentRetries + 1);
|
|
436
441
|
let agentRun = await runAgent(agentRequest, run);
|
|
437
|
-
for (let attempt = 2; attempt <= attempts && isRetryableReviewRun(agentRun); attempt++) {
|
|
442
|
+
for (let attempt = 2; attempt <= attempts && isRetryableReviewRun(agentRun) && agentRequest.time!.endsAt > Date.now(); attempt++) {
|
|
438
443
|
agentRun = await runAgent(agentRequest, run);
|
|
439
444
|
}
|
|
440
445
|
const result = parseReviewResult(request.domain, agentRun.output);
|
|
@@ -2,10 +2,10 @@ import { compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, typ
|
|
|
2
2
|
import { contextMarks, freshContextOn } from "./fresh-context.ts";
|
|
3
3
|
import { isSubagentProcess } from "./quiet.ts";
|
|
4
4
|
|
|
5
|
-
export const CONTEXT_TARGET =
|
|
5
|
+
export const CONTEXT_TARGET = 64_000;
|
|
6
6
|
|
|
7
7
|
export function compactionThreshold(window: number, reserve = DEFAULT_COMPACTION_SETTINGS.reserveTokens): number {
|
|
8
|
-
return Math.max(1, Math.min(CONTEXT_TARGET, Math.floor(window * 0.
|
|
8
|
+
return Math.max(1, Math.min(CONTEXT_TARGET, Math.floor(window * 0.6), window - Math.min(reserve, Math.floor(window / 2))));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** Compact at a settled tool boundary; ctx.compact() would abort the turn we need to continue. */
|
|
@@ -30,7 +30,7 @@ export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compa
|
|
|
30
30
|
if (boundary !== undefined) entries = entries.filter((entry) => Date.parse(entry.timestamp) >= boundary);
|
|
31
31
|
}
|
|
32
32
|
if (entries.length < 2) return;
|
|
33
|
-
const keepRecentTokens = Math.min(override?.keepRecentTokens ?? configured?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens, Math.floor(threshold / 4));
|
|
33
|
+
const keepRecentTokens = Math.min(16_000, override?.keepRecentTokens ?? configured?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens, Math.floor(threshold / 4));
|
|
34
34
|
const cut = findCutPoint(entries, 0, entries.length, keepRecentTokens);
|
|
35
35
|
if (cut.firstKeptEntryIndex <= 0) return;
|
|
36
36
|
const prefix = entries.slice(0, cut.firstKeptEntryIndex);
|
|
@@ -61,7 +61,7 @@ export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compa
|
|
|
61
61
|
const headers = auth.headers ? Object.fromEntries(Object.entries(auth.headers).filter((entry): entry is [string, string] => entry[1] !== null)) : undefined;
|
|
62
62
|
const retry = pi.getSettings().retry;
|
|
63
63
|
const retryPolicy = { enabled: retry?.enabled ?? true, maxRetries: retry?.maxRetries ?? 3, baseDelayMs: retry?.baseDelayMs ?? 2_000, maxAgentDelayMs: retry?.maxAgentDelayMs };
|
|
64
|
-
const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers, "
|
|
64
|
+
const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers, "Write a concise checkpoint with: Requirements and constraints; Decisions; Files changed; Verification and evidence; Failed approaches (do not repeat); Unfinished work and next action; Outstanding children and their assignments. Preserve concrete file references and distinguish verified facts from guesses. The parent remains responsible for integration and verification.", ctx.signal, undefined, undefined, auth.env, retryPolicy);
|
|
65
65
|
ctx.signal?.throwIfAborted();
|
|
66
66
|
ctx.ui.notify("Agent context compacted", "info");
|
|
67
67
|
return { entries: [...event.entries, { type: "compaction" as const, summary: result.summary, firstKeptEntryId, usage: result.usage, details: result.details }] };
|