@a-t-h-i/bot-lobby 0.6.59 → 0.6.60
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/package.json +7 -7
- package/prompts/global.md +2 -1
- package/prompts/worker.md +2 -9
- package/src/classifier/files.ts +18 -6
- package/src/classifier/instance.ts +2 -2
- package/src/classifier/knowledge.ts +124 -1
- package/src/desk/board-client.ts +79 -12
- package/src/desk/board.ts +63 -52
- package/src/desk/delivery.ts +243 -0
- package/src/desk/ipc.ts +34 -1
- package/src/desk/lock.ts +81 -0
- package/src/desk/session.ts +85 -18
- package/src/execution/agent-runner.ts +10 -2
- package/src/execution/delegation.ts +160 -7
- package/src/execution/output-artifacts.ts +176 -0
- package/src/index.ts +3 -0
- package/src/knowledge/bookmarks.ts +452 -0
- package/src/knowledge/catalog-tools.ts +145 -0
- package/src/knowledge/catalogue.ts +550 -0
- package/src/knowledge/file-notes.ts +16 -6
- package/src/knowledge/repo-map.ts +67 -2
- package/src/knowledge/retrieval.ts +431 -0
- package/src/master/master.ts +132 -21
- package/src/pi/events.ts +31 -2
- package/src/pi/tools.ts +12 -1
- package/src/prompts/compiler.ts +24 -7
- package/src/schemas/configuration.ts +4 -0
- package/src/schemas/task.ts +32 -0
- package/src/state/checkpoint.ts +199 -0
- package/src/state/persistence.ts +138 -3
- package/src/workflow/workflow.ts +62 -11
package/src/master/master.ts
CHANGED
|
@@ -8,7 +8,12 @@ import { runAgent, runParallel, watchdogOptions, type AgentRequest } from "../ex
|
|
|
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 { taskDir } from "../knowledge/paths.ts";
|
|
11
|
-
import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
11
|
+
import { selectKnowledge, selectRelevant, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
12
|
+
import { appendPost, assignmentId } from "../desk/board.ts";
|
|
13
|
+
import { refreshCatalogue } from "../knowledge/catalogue.ts";
|
|
14
|
+
import { notesFor } from "../knowledge/retrieval.ts";
|
|
15
|
+
import { assignmentKey, applySelection, bookmarkBlock, BOOKMARK_DEADLINE_MS, BOOKMARKS_VERSION, deliverBookmarks, fitHints, lexicalBookmarks, type BookmarkState, type Selection } from "../knowledge/bookmarks.ts";
|
|
16
|
+
import { KNOWLEDGE_BUDGET_CHARS, type BookmarkPick, type BookmarkSelectorInput, type KnowledgePaths, type KnowledgePicker } from "../classifier/knowledge.ts";
|
|
12
17
|
import { scriptsFor } from "../scripts/pick.ts";
|
|
13
18
|
import { summarizeOutcomes } from "./synthesis.ts";
|
|
14
19
|
import { parseWorkerResult, validateWorkerResult } from "../roles/worker.ts";
|
|
@@ -20,8 +25,8 @@ import { toolBudgetEnv, toolBudgetNote } from "../pi/tool-budget.ts";
|
|
|
20
25
|
import { recordFileNotes } from "../knowledge/file-notes.ts";
|
|
21
26
|
import { readCachedScout, scoutKey, writeCachedScout } from "./scout-cache.ts";
|
|
22
27
|
import { BOARD_ENV, BOARD_TOOLS, boardContext } from "../desk/board.ts";
|
|
28
|
+
import { seedDelivery } from "../desk/delivery.ts";
|
|
23
29
|
import type { FileHinter } from "../classifier/files.ts";
|
|
24
|
-
import type { KnowledgePaths, KnowledgePicker } from "../classifier/knowledge.ts";
|
|
25
30
|
import { fellShort, profileLabel, routeLabel, type EffortRoute, type EffortRouter } from "../classifier/effort.ts";
|
|
26
31
|
|
|
27
32
|
export interface ScoutOutcome {
|
|
@@ -82,13 +87,21 @@ function knowledgePaths(dataRoots: readonly string[], agent: Domain): KnowledgeP
|
|
|
82
87
|
* the files that are too long when a picker is given, the keyword selection
|
|
83
88
|
* otherwise and whenever the picker fails; and the shared scripts that fit it.
|
|
84
89
|
*/
|
|
85
|
-
|
|
90
|
+
/** Retrieval mode replaces the historical knowledge and decision dumps with assignment bookmarks; standards stay direct. */
|
|
91
|
+
function retrievalMode(request: { config?: BotLobbyConfig }): boolean {
|
|
92
|
+
return request.config?.workflow.knowledgeRetrieval !== false && request.config !== undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function pickKnowledge(request: { config?: BotLobbyConfig; knowledge?: KnowledgePicker; dataRoots: readonly string[]; signal?: AbortSignal }, agent: Domain, query: string): Promise<KnowledgeSelection> {
|
|
86
96
|
const [selected, scripts] = await Promise.all([pickFiles(request, agent, query), scriptsFor(request.dataRoots[0], query, request.knowledge?.scripts, request.signal)]);
|
|
87
97
|
return scripts ? { ...selected, scripts } : selected;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
async function pickFiles(request: { knowledge?: KnowledgePicker; dataRoots: readonly string[]; signal?: AbortSignal }, agent: Domain, query: string): Promise<KnowledgeSelection> {
|
|
100
|
+
async function pickFiles(request: { config?: BotLobbyConfig; knowledge?: KnowledgePicker; dataRoots: readonly string[]; signal?: AbortSignal }, agent: Domain, query: string): Promise<KnowledgeSelection> {
|
|
91
101
|
const slices = readAgentKnowledge(request.dataRoots, agent);
|
|
102
|
+
if (retrievalMode(request)) {
|
|
103
|
+
return { knowledge: "", decisions: "", standards: selectRelevant(query, slices.standards ?? "", KNOWLEDGE_BUDGET_CHARS) };
|
|
104
|
+
}
|
|
92
105
|
if (!request.knowledge) return selectKnowledge(query, slices);
|
|
93
106
|
try {
|
|
94
107
|
return await request.knowledge.select(query, slices, { paths: knowledgePaths(request.dataRoots, agent), ...(request.signal ? { signal: request.signal } : {}) });
|
|
@@ -97,10 +110,91 @@ async function pickFiles(request: { knowledge?: KnowledgePicker; dataRoots: read
|
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
/** Resolve with the value, or `fallback` once `ms` pass: bookmark preparation never blocks the step. */
|
|
114
|
+
async function within<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {
|
|
115
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
const late = new Promise<T>((resolve) => {
|
|
117
|
+
timer = setTimeout(() => resolve(fallback), ms);
|
|
118
|
+
});
|
|
119
|
+
return Promise.race([promise, late]).finally(() => clearTimeout(timer));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The lexical picks as the bookmark list records them. */
|
|
123
|
+
function lexicalPicks(input: BookmarkSelectorInput): BookmarkPick[] {
|
|
124
|
+
return lexicalBookmarks(input.assignment, input.entries).map((pick) => ({ ...pick, source: "lexical" as const }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Post a bookmark's correction signals to the task board as oracle notes; the delivered mark says verify regardless. */
|
|
128
|
+
function postCorrections(board: string | undefined, corrections: readonly string[]): void {
|
|
129
|
+
for (const text of corrections) {
|
|
130
|
+
try {
|
|
131
|
+
appendPost(board!, { from: "oracle", kind: "note", text });
|
|
132
|
+
} catch {
|
|
133
|
+
// No board (or it is busy): the delivered bookmark still carries its changed/stale mark.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Select and deliver one recipient's assignment bookmarks: bounded catalogue
|
|
140
|
+
* metadata in, at most eight references out (the classifier's picks, or the
|
|
141
|
+
* lexical fallback when it is off, slow or failing), persisted in the task's
|
|
142
|
+
* storage, revalidated at delivery, rendered within the 3,000-character
|
|
143
|
+
* metadata budget. Returns "" in legacy mode; a coverage line when nothing
|
|
144
|
+
* was selected, so the agent knows the retrieval tools are there.
|
|
145
|
+
*/
|
|
146
|
+
async function prepareBookmarks(
|
|
147
|
+
request: { taskId: string; cwd: string; dataRoots: readonly string[]; config: BotLobbyConfig; knowledge?: KnowledgePicker; board?: string; signal?: AbortSignal },
|
|
148
|
+
recipient: string,
|
|
149
|
+
query: string,
|
|
150
|
+
): Promise<string> {
|
|
151
|
+
const dir = request.dataRoots[0];
|
|
152
|
+
if (!request.config.workflow.knowledgeRetrieval || !dir) return "";
|
|
153
|
+
const { catalogue, coverage } = refreshCatalogue(request.dataRoots);
|
|
154
|
+
const input: BookmarkSelectorInput = {
|
|
155
|
+
assignment: query,
|
|
156
|
+
entries: catalogue.entries.filter((entry) => entry.freshness !== "stale"),
|
|
157
|
+
catalogueRevision: catalogue.revision,
|
|
158
|
+
cacheKey: `${request.taskId}:${recipient}`,
|
|
159
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
160
|
+
};
|
|
161
|
+
const picks = request.knowledge?.bookmarks
|
|
162
|
+
? await within(request.knowledge.bookmarks.select(input), BOOKMARK_DEADLINE_MS + 500, lexicalPicks(input))
|
|
163
|
+
: lexicalPicks(input);
|
|
164
|
+
const selections: Selection[] = picks.map(({ ref, reason, revision }) => ({ ref, reason, revision }));
|
|
165
|
+
const authority = picks.some((pick) => pick.source === "classifier") ? "classifier" : "lexical";
|
|
166
|
+
const state = bookmarkState(dir, request, recipient, query, catalogue.revision, selections, authority);
|
|
167
|
+
const access = { dataRoots: request.dataRoots, cwd: request.cwd };
|
|
168
|
+
const notes = notesFor(dir);
|
|
169
|
+
postCorrections(request.board, deliverBookmarks(state, recipient, catalogue, access, notes).corrections);
|
|
170
|
+
return bookmarkBlock(state, recipient, catalogue, access, notes) || `No bookmarks were selected for this assignment (catalogue coverage: ${coverage}); search_knowledge finds the recorded knowledge.`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The persisted bookmark list after this selection; a busy list still delivers this run's picks. */
|
|
174
|
+
function bookmarkState(
|
|
175
|
+
dir: string,
|
|
176
|
+
request: { taskId: string },
|
|
177
|
+
recipient: string,
|
|
178
|
+
query: string,
|
|
179
|
+
sourceRevision: number,
|
|
180
|
+
selections: Selection[],
|
|
181
|
+
authority: "classifier" | "lexical",
|
|
182
|
+
): BookmarkState {
|
|
183
|
+
try {
|
|
184
|
+
return applySelection(dir, request.taskId, recipient, assignmentKey(query), sourceRevision, selections, authority);
|
|
185
|
+
} catch {
|
|
186
|
+
return {
|
|
187
|
+
version: BOOKMARKS_VERSION, taskId: request.taskId, assignment: assignmentKey(query), sourceRevision,
|
|
188
|
+
records: selections.map((pick) => ({ ref: pick.ref, recipient, authority, reason: pick.reason, sourceVersion: `${sourceRevision}`, revision: pick.revision, at: new Date().toISOString() })),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function scoutContext(request: ScoutRequest, domain: Domain, selected: KnowledgeSelection, likely = "", bookmarks = ""): AgentRequest["context"] {
|
|
101
194
|
return {
|
|
102
195
|
task: request.taskText,
|
|
103
196
|
...selected,
|
|
197
|
+
...(bookmarks ? { bookmarks } : {}),
|
|
104
198
|
instructions: request.config.agents[domain].instructions,
|
|
105
199
|
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, toolBudgetNote(request.config.scout.maxToolCalls), likely].filter(Boolean).join("\n\n"),
|
|
106
200
|
};
|
|
@@ -176,20 +270,23 @@ async function remember(dataDir: string, request: ScoutRequest, outcomes: readon
|
|
|
176
270
|
}
|
|
177
271
|
|
|
178
272
|
async function scoutDomains(request: ScoutRequest, run: ProcessRunner): Promise<ScoutOutcome[]> {
|
|
179
|
-
// Each scout gets the files most likely to answer its own instruction and focus.
|
|
180
|
-
const [likely, selected] = await Promise.all([
|
|
273
|
+
// Each scout gets the files most likely to answer its own instruction and focus, and its assignment's bookmarks.
|
|
274
|
+
const [likely, selected, bookmarked] = await Promise.all([
|
|
181
275
|
Promise.all(request.domains.map((domain) => likelyFor(request.hints, `${request.instruction}\n${domainSpec(domain).scoutFocus}`, request.signal, request.taskText))),
|
|
182
276
|
Promise.all(request.domains.map((domain) => pickKnowledge(request, domain, `${request.instruction}\n${domainSpec(domain).scoutFocus}\n\n${request.taskText}`))),
|
|
277
|
+
Promise.all(request.domains.map((domain) => prepareBookmarks(request, domain, `${request.instruction}\n${domainSpec(domain).scoutFocus}\n\n${request.taskText}`))),
|
|
183
278
|
]);
|
|
184
279
|
const extraTools = request.hints?.tools() ?? [];
|
|
185
|
-
const requests: AgentRequest[] = request.domains.map((domain, index) =>
|
|
280
|
+
const requests: AgentRequest[] = request.domains.map((domain, index) => {
|
|
281
|
+
const hints = fitHints(bookmarked[index] ?? "", likely[index] ?? "");
|
|
282
|
+
return {
|
|
186
283
|
delegationQuota: { used: 0 },
|
|
187
284
|
effort: request.effort,
|
|
188
285
|
taskId: request.taskId,
|
|
189
286
|
domain,
|
|
190
287
|
role: "scout",
|
|
191
288
|
instruction: scoutInstruction(request, domain),
|
|
192
|
-
context: scoutContext(request, domain, selected[index]!, likely
|
|
289
|
+
context: scoutContext(request, domain, selected[index]!, hints.likely, hints.bookmarks),
|
|
193
290
|
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
194
291
|
env: toolBudgetEnv(request.config.scout.maxToolCalls),
|
|
195
292
|
...profileFields(request.config, request.profile, domain, "scout"),
|
|
@@ -197,7 +294,8 @@ async function scoutDomains(request: ScoutRequest, run: ProcessRunner): Promise<
|
|
|
197
294
|
signal: request.signal,
|
|
198
295
|
onUpdate: request.onUpdate,
|
|
199
296
|
...watchdogOptions(request.config.workflow),
|
|
200
|
-
|
|
297
|
+
};
|
|
298
|
+
});
|
|
201
299
|
// Scouts think at a fixed level; a trivial one may move to the cheaper model (a pi model, so not on another harness).
|
|
202
300
|
const routes = await Promise.all(requests.map((entry) => entry.harness ? undefined : routeFor(request.effort, entry.instruction, { ...(entry.model ? { model: entry.model } : {}), thinking: entry.thinking ?? "low" }, { thinkingFixed: true, context: request.taskText, ...(request.signal ? { signal: request.signal } : {}) })));
|
|
203
301
|
const first = requests.map((entry, index) => (routes[index] ? { ...entry, ...routedFields(routes[index]!) } : entry));
|
|
@@ -283,6 +381,8 @@ export interface WorkerRequest {
|
|
|
283
381
|
risk?: string;
|
|
284
382
|
/** The task board's file: the worker reads what its role follows and posts contracts, decisions and blockers. */
|
|
285
383
|
board?: string;
|
|
384
|
+
/** The worker's board identity (`backend:9f2c1e`): one per assignment, so two same-role workers never share delivery state. Minted when omitted. */
|
|
385
|
+
boardFrom?: string;
|
|
286
386
|
/** Domains working at the same time as this one, in a parallel batch. */
|
|
287
387
|
alongside?: readonly Domain[];
|
|
288
388
|
/** The worker has a worktree of its own for the batch; its change is applied back when the batch ends. */
|
|
@@ -292,11 +392,13 @@ export interface WorkerRequest {
|
|
|
292
392
|
}
|
|
293
393
|
|
|
294
394
|
/** How a worker uses the task board, and what it already says for its role. */
|
|
295
|
-
function boardGuidance(request: WorkerRequest): string {
|
|
395
|
+
function boardGuidance(request: WorkerRequest, reader: string): string {
|
|
296
396
|
if (!request.board) return "";
|
|
397
|
+
// The board lines below are a delivery: record them before any push can race the read.
|
|
398
|
+
seedDelivery(request.board, reader);
|
|
297
399
|
return [
|
|
298
|
-
"Task board: post with `post_board` whenever you settle something another agent builds against or must follow (`contract`: a route, payload, type, event or file location; `decision`), when you are blocked on someone else's part (`blocker`), and when a piece is finished and testable (`done`). Exact facts in a line or two; no progress chatter. `read_board` shows what the others posted.",
|
|
299
|
-
boardContext(request.board,
|
|
400
|
+
"Task board: post with `post_board` whenever you settle something another agent builds against or must follow (`contract`: a route, payload, type, event or file location; `decision`), when you are blocked on someone else's part (`blocker`), and when a piece is finished and testable (`done`). Exact facts in a line or two; no progress chatter. `read_board` shows what the others posted; `ack_board` records the events you actually incorporated — reading alone does not.",
|
|
401
|
+
boardContext(request.board, reader),
|
|
300
402
|
].filter(Boolean).join("\n\n");
|
|
301
403
|
}
|
|
302
404
|
|
|
@@ -327,7 +429,7 @@ function askGuidance(previews: string): string {
|
|
|
327
429
|
].join(" ");
|
|
328
430
|
}
|
|
329
431
|
|
|
330
|
-
function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
432
|
+
function workerWorkflowContext(request: WorkerRequest, boardFrom: string, likely = ""): string {
|
|
331
433
|
const spec = domainSpec(request.domain);
|
|
332
434
|
const own = request.scoutOutcomes.filter(
|
|
333
435
|
(outcome) => outcome.result.domain === request.domain && outcome.usable,
|
|
@@ -342,7 +444,7 @@ function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
|
342
444
|
likely,
|
|
343
445
|
request.risk ?? "",
|
|
344
446
|
alongsideGuidance(request),
|
|
345
|
-
boardGuidance(request),
|
|
447
|
+
boardGuidance(request, boardFrom),
|
|
346
448
|
request.ask ? askGuidance(request.ask.previews) : "",
|
|
347
449
|
request.dataRoots[0] ? `Scripts you draft for the shared library go in \`${join(taskDir(request.dataRoots[0], request.taskId), "scripts")}\`.` : "",
|
|
348
450
|
request.dataRoots[0] ? `Long findings (an investigation, a list of call sites, a log excerpt) go in a file under \`${join(taskDir(request.dataRoots[0], request.taskId), "notes")}\`: give its path under Notes instead of pasting them into your report, so whoever needs them reads them whole.` : "",
|
|
@@ -355,14 +457,17 @@ export async function runWorker(
|
|
|
355
457
|
run: ProcessRunner = spawnPiProcess,
|
|
356
458
|
): Promise<WorkerOutcome> {
|
|
357
459
|
const configured = profileFields(request.config, request.profile, request.domain, "worker");
|
|
358
|
-
const [selected, likely, route] = await Promise.all([
|
|
460
|
+
const [selected, likely, route, bookmarked] = await Promise.all([
|
|
359
461
|
pickKnowledge(request, request.domain, `${request.instruction}\n\n${request.taskText}`),
|
|
360
462
|
likelyFor(request.hints, request.instruction, request.signal, request.taskText),
|
|
361
463
|
routeFor(request.effort, request.instruction, { ...(configured.model ? { model: configured.model } : {}), thinking: configured.thinking }, { context: request.taskText, ...(request.signal ? { signal: request.signal } : {}) }),
|
|
464
|
+
prepareBookmarks(request, request.domain, `${request.instruction}\n\n${request.taskText}`),
|
|
362
465
|
]);
|
|
466
|
+
const hints = fitHints(bookmarked, likely);
|
|
363
467
|
const hintTools = request.hints?.tools() ?? [];
|
|
468
|
+
const boardFrom = request.boardFrom ?? assignmentId(request.domain);
|
|
364
469
|
const extraTools = [...(request.agent?.extraTools ?? []), ...hintTools, ...(request.board ? BOARD_TOOLS : [])];
|
|
365
|
-
const env = request.board ? { ...request.agent?.env, [BOARD_ENV]: request.board, BOT_LOBBY_BOARD_FROM:
|
|
470
|
+
const env = request.board ? { ...request.agent?.env, [BOARD_ENV]: request.board, BOT_LOBBY_BOARD_FROM: boardFrom } : request.agent?.env;
|
|
366
471
|
const base: AgentRequest = {
|
|
367
472
|
delegationQuota: { used: 0 },
|
|
368
473
|
...(request.expert ? { expert: request.expert } : {}),
|
|
@@ -375,8 +480,9 @@ export async function runWorker(
|
|
|
375
480
|
context: {
|
|
376
481
|
task: request.taskText,
|
|
377
482
|
...selected,
|
|
483
|
+
...(hints.bookmarks ? { bookmarks: hints.bookmarks } : {}),
|
|
378
484
|
instructions: request.config.agents[request.domain].instructions,
|
|
379
|
-
workflowContext: workerWorkflowContext(request, likely),
|
|
485
|
+
workflowContext: workerWorkflowContext(request, boardFrom, hints.likely),
|
|
380
486
|
},
|
|
381
487
|
...configured,
|
|
382
488
|
cwd: request.cwd,
|
|
@@ -480,7 +586,7 @@ function isRetryableReviewRun(run: AgentRun): boolean {
|
|
|
480
586
|
return run.status !== "cancelled" && (run.status !== "success" || !hasReviewVerdict(run.output));
|
|
481
587
|
}
|
|
482
588
|
|
|
483
|
-
function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelection): AgentRequest {
|
|
589
|
+
function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelection, bookmarks = ""): AgentRequest {
|
|
484
590
|
return {
|
|
485
591
|
taskId: request.taskId,
|
|
486
592
|
domain: request.domain,
|
|
@@ -492,6 +598,7 @@ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelec
|
|
|
492
598
|
context: {
|
|
493
599
|
task: request.taskText,
|
|
494
600
|
...selected,
|
|
601
|
+
...(bookmarks ? { bookmarks } : {}),
|
|
495
602
|
instructions: request.config.agents[request.domain].instructions,
|
|
496
603
|
workflowContext: reviewerContext(request),
|
|
497
604
|
},
|
|
@@ -515,8 +622,12 @@ export async function runReviewer(
|
|
|
515
622
|
request: ReviewerRequest,
|
|
516
623
|
run: ProcessRunner = spawnPiProcess,
|
|
517
624
|
): Promise<ReviewerOutcome> {
|
|
518
|
-
const
|
|
519
|
-
const
|
|
625
|
+
const query = `${request.taskText} ${request.workerSummary}`;
|
|
626
|
+
const [selected, bookmarked] = await Promise.all([
|
|
627
|
+
pickKnowledge(request, request.domain, query),
|
|
628
|
+
prepareBookmarks(request, request.domain, query),
|
|
629
|
+
]);
|
|
630
|
+
const agentRequest = reviewerAgentRequest(request, selected, bookmarked);
|
|
520
631
|
const attempts = Math.max(1, request.config.workflow.maxAgentRetries + 1);
|
|
521
632
|
let agentRun = await runAgent(agentRequest, run);
|
|
522
633
|
for (let attempt = 2; attempt <= attempts && isRetryableReviewRun(agentRun); attempt++) {
|
package/src/pi/events.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { activeTask } from "../state/persistence.ts";
|
|
|
3
3
|
import { detectProjectRoot, loadConfig, readDataRoots } from "../state/project.ts";
|
|
4
4
|
import { compilePrompt } from "../prompts/compiler.ts";
|
|
5
5
|
import { readAgentKnowledge } from "../knowledge/store.ts";
|
|
6
|
-
import { selectKnowledge } from "../knowledge/selector.ts";
|
|
6
|
+
import { selectKnowledge, selectRelevant } from "../knowledge/selector.ts";
|
|
7
|
+
import { applySelection, assignmentKey, bookmarkBlock, lexicalBookmarks } from "../knowledge/bookmarks.ts";
|
|
8
|
+
import { refreshCatalogue } from "../knowledge/catalogue.ts";
|
|
9
|
+
import { readNotes } from "../knowledge/notes.ts";
|
|
10
|
+
import { KNOWLEDGE_BUDGET_CHARS } from "../classifier/knowledge.ts";
|
|
7
11
|
import { libraryBlock } from "../scripts/pick.ts";
|
|
8
12
|
import { cancelAllRuns } from "../execution/agent-runner.ts";
|
|
9
13
|
import { describeTask } from "../workflow/workflow.ts";
|
|
@@ -63,6 +67,26 @@ export const LOBBY_CHAT_NOTE = [
|
|
|
63
67
|
"Bot Lobby's workflow (scouts, a plan, the QA gate) runs only when the user types /plan (plan it with the panel) or /task (start it as a task). When a request is large, risky or spans several areas, suggest one of them in a line; never start one yourself.",
|
|
64
68
|
].join("\n");
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The oracle's own bookmarks for the active task, in retrieval mode: lexical
|
|
72
|
+
* picks over the catalogue (the event path is synchronous, so no model call),
|
|
73
|
+
* persisted in the task's storage and revalidated at delivery. Standards and
|
|
74
|
+
* the task context stay direct; the knowledge and decision dumps are replaced.
|
|
75
|
+
*/
|
|
76
|
+
function masterBookmarks(root: string, configDir: string, dataRoots: readonly string[], taskId: string, query: string): string {
|
|
77
|
+
const dir = dataRoots[0];
|
|
78
|
+
if (!dir) return "";
|
|
79
|
+
try {
|
|
80
|
+
const { catalogue, coverage } = refreshCatalogue(dataRoots);
|
|
81
|
+
const picks = lexicalBookmarks(query, catalogue.entries.filter((entry) => entry.freshness !== "stale"));
|
|
82
|
+
const state = applySelection(dir, taskId, "master", assignmentKey(query), catalogue.revision, picks, "lexical");
|
|
83
|
+
const block = bookmarkBlock(state, "master", catalogue, { dataRoots, cwd: root }, readNotes(dir));
|
|
84
|
+
return block || `No bookmarks were selected for this assignment (catalogue coverage: ${coverage}); search_knowledge finds the recorded knowledge.`;
|
|
85
|
+
} catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
67
91
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
68
92
|
let hiddenWeb: string[] = [];
|
|
@@ -137,13 +161,18 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
137
161
|
}
|
|
138
162
|
const dataRoots = readDataRoots(root, configDir);
|
|
139
163
|
const slices = readAgentKnowledge(dataRoots, "master");
|
|
140
|
-
const
|
|
164
|
+
const query = `${taskRequest(task)} ${task.proposal ?? ""}`;
|
|
165
|
+
const retrieval = loadConfig().workflow.knowledgeRetrieval;
|
|
166
|
+
const selected = retrieval
|
|
167
|
+
? { knowledge: "", decisions: "", standards: selectRelevant(query, slices.standards ?? "", KNOWLEDGE_BUDGET_CHARS) }
|
|
168
|
+
: selectKnowledge(query, slices);
|
|
141
169
|
event.systemPromptOptions.sections["bot-lobby"] = compilePrompt({
|
|
142
170
|
domain: "master",
|
|
143
171
|
task: masterTaskContext(task, readPlanComments(root, configDir, task.id), isAutoMode(root, configDir, task.id)),
|
|
144
172
|
standards: selected.standards,
|
|
145
173
|
knowledge: selected.knowledge,
|
|
146
174
|
decisions: selected.decisions,
|
|
175
|
+
bookmarks: retrieval ? masterBookmarks(root, configDir, dataRoots, task.id, query) : "",
|
|
147
176
|
scripts: dataRoots[0] ? libraryBlock(dataRoots[0]) : "",
|
|
148
177
|
workflowContext: masterWorkflowContext(task),
|
|
149
178
|
instructions: loadConfig().master.instructions,
|
package/src/pi/tools.ts
CHANGED
|
@@ -68,6 +68,16 @@ const OrchestrateSchema = Type.Object({
|
|
|
68
68
|
description: "knowledge: which persistent file the text belongs to",
|
|
69
69
|
}),
|
|
70
70
|
),
|
|
71
|
+
op: Type.Optional(
|
|
72
|
+
StringEnum(["add", "remove", "pin", "unpin", "exclude"] as const, {
|
|
73
|
+
description: "bookmark: the operation on the task's assignment bookmark list",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
refs: Type.Optional(
|
|
77
|
+
Type.Array(Type.String(), {
|
|
78
|
+
description: "bookmark: the reference ids (K-…), exactly as search_knowledge returned them",
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
const DESCRIPTION = [
|
|
@@ -76,7 +86,8 @@ const DESCRIPTION = [
|
|
|
76
86
|
"read-only researcher for cited internet evidence on a complex change, tool, plugin, doc set or",
|
|
77
87
|
"dependency), propose (record the proposal and request approval), plan (record the internal",
|
|
78
88
|
"plan, or amend it later with the full revised plan), implement (delegate a step to a domain worker, or several domains in parallel with assignments), qa (final quality gate and the only",
|
|
79
|
-
"review), knowledge (record approved project knowledge or a decision; works without a task),"
|
|
89
|
+
"review), knowledge (record approved project knowledge or a decision; works without a task),"
|
|
90
|
+
+ "bookmark (manage the task's assignment bookmark list: op=add|remove|pin|unpin|exclude with refs — reference ids from search_knowledge — and optionally domain and note),",
|
|
80
91
|
"compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), script (save a script to the shared scripts library, or replace one, for a job that comes back; works without a task),",
|
|
81
92
|
"resolve_approval (approve or reject a request), complete (declare the task done after the gates",
|
|
82
93
|
"pass, then ask the user to confirm merging and pushing main and removing its worktree), block/resume (escalate or continue),",
|
package/src/prompts/compiler.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface CompileInput {
|
|
|
15
15
|
standards?: string;
|
|
16
16
|
knowledge?: string;
|
|
17
17
|
decisions?: string;
|
|
18
|
+
/** Assignment bookmarks: bounded reference metadata pointing into the knowledge catalogue. */
|
|
19
|
+
bookmarks?: string;
|
|
18
20
|
/** The shared scripts that fit the step, or the library for the oracle. */
|
|
19
21
|
scripts?: string;
|
|
20
22
|
workflowContext?: string;
|
|
@@ -30,24 +32,39 @@ function section(title: string, body?: string): string {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* The invariant layers for a domain/role: global + domain + role prompt and the
|
|
36
|
+
* role's output contract. Determined by domain and role alone, so identical
|
|
37
|
+
* configuration yields a byte-identical prefix ahead of task-selected context.
|
|
36
38
|
*/
|
|
37
|
-
export function
|
|
38
|
-
const spec =
|
|
39
|
+
export function invariantPrefix(domain: PromptDomain, role?: Role): string {
|
|
40
|
+
const spec = role ? ROLE_SPECS[role] : undefined;
|
|
39
41
|
const layers = [
|
|
40
42
|
loadPrompt("global.md"),
|
|
41
|
-
loadPrompt(domainPromptFile(
|
|
43
|
+
loadPrompt(domainPromptFile(domain)),
|
|
42
44
|
spec ? loadPrompt(spec.promptFile) : "",
|
|
45
|
+
spec?.contract ?? "",
|
|
46
|
+
];
|
|
47
|
+
return layers.filter((layer) => layer.trim().length > 0).join("\n\n---\n\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compose the layered prompt: invariant prefix (global + domain + role + output
|
|
52
|
+
* contract) + custom instructions + task context + standards + knowledge +
|
|
53
|
+
* decisions + scripts + workflow context. Empty layers are dropped so agents
|
|
54
|
+
* never receive empty headings. Custom instructions keep their precedence over
|
|
55
|
+
* the role layers, including the contract.
|
|
56
|
+
*/
|
|
57
|
+
export function compilePrompt(input: CompileInput): string {
|
|
58
|
+
const layers = [
|
|
59
|
+
invariantPrefix(input.domain, input.role),
|
|
43
60
|
section("Custom Instructions", input.instructions),
|
|
44
61
|
section("Task Context", input.task),
|
|
45
62
|
section("Standards", input.standards),
|
|
46
63
|
section("Knowledge", input.knowledge),
|
|
47
64
|
section("Decisions", input.decisions),
|
|
65
|
+
section("Bookmarks", input.bookmarks),
|
|
48
66
|
section("Scripts", input.scripts),
|
|
49
67
|
section("Workflow Context", input.workflowContext),
|
|
50
|
-
spec?.contract ?? "",
|
|
51
68
|
];
|
|
52
69
|
return layers.filter((layer) => layer.trim().length > 0).join("\n\n---\n\n");
|
|
53
70
|
}
|
|
@@ -98,6 +98,8 @@ export interface WorkflowConfig {
|
|
|
98
98
|
gitIsolation: GitIsolation;
|
|
99
99
|
/** Scouts, workers, lobby chat replies and the planner get the repository map: the layout and the files matching their step, with each file's purpose, names and unchanged scout notes. */
|
|
100
100
|
repoMap: boolean;
|
|
101
|
+
/** Assignment bookmarks and the knowledge catalogue replace the historical knowledge and decision dumps in prompts; false keeps the legacy keyword-selected dumps (the rollback switch; never both). */
|
|
102
|
+
knowledgeRetrieval: boolean;
|
|
101
103
|
/** A scout asked the same thing again for the same task, on a tree that has not changed, reuses its findings instead of running. */
|
|
102
104
|
reuseScouts: boolean;
|
|
103
105
|
/** How a parallel batch shares the repository: one checkout with the file desk (`desk`), or a worktree per worker whose changes are merged back when the batch ends (`worktrees`). */
|
|
@@ -299,6 +301,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
299
301
|
briefCheck: true,
|
|
300
302
|
gitIsolation: "off",
|
|
301
303
|
repoMap: true,
|
|
304
|
+
knowledgeRetrieval: true,
|
|
302
305
|
reuseScouts: true,
|
|
303
306
|
parallelMode: "desk",
|
|
304
307
|
cleanReview: true,
|
|
@@ -528,6 +531,7 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
528
531
|
workflow.fastTrack = workflow.fastTrack !== false;
|
|
529
532
|
workflow.briefCheck = workflow.briefCheck !== false;
|
|
530
533
|
workflow.repoMap = workflow.repoMap !== false;
|
|
534
|
+
workflow.knowledgeRetrieval = workflow.knowledgeRetrieval !== false;
|
|
531
535
|
workflow.reuseScouts = workflow.reuseScouts !== false;
|
|
532
536
|
workflow.parallelMode = isParallelMode(workflow.parallelMode) ? workflow.parallelMode : DEFAULT_CONFIG.workflow.parallelMode;
|
|
533
537
|
workflow.cleanReview = workflow.cleanReview !== false;
|
package/src/schemas/task.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LintReport } from "./lint.ts";
|
|
2
2
|
import type { Domain, Role } from "./agent.ts";
|
|
3
3
|
import type { Delivery } from "../delivery/types.ts";
|
|
4
|
+
import type { TaskCheckpoint } from "../state/checkpoint.ts";
|
|
4
5
|
|
|
5
6
|
export const TASK_STATES = [
|
|
6
7
|
"created",
|
|
@@ -74,6 +75,33 @@ export interface WorkerRunRecord {
|
|
|
74
75
|
/** Upper bound on persisted worker records; plans cap at 50 steps. */
|
|
75
76
|
export const MAX_WORKER_RECORDS = 64;
|
|
76
77
|
|
|
78
|
+
/** What one assignment-scoped identity may do: reads only, granted by the parent that issued it. */
|
|
79
|
+
export type AssignmentGrant = "artifact.read" | "checkpoint.read";
|
|
80
|
+
export type AssignmentState = "active" | "cancelled" | "revoked";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A parent-issued assignment identity for one agent run: the durable basis
|
|
84
|
+
* for reading output artifacts and checkpoints, revalidated live on every use.
|
|
85
|
+
*/
|
|
86
|
+
export interface AssignmentRecord {
|
|
87
|
+
/** A-<uuid>; the identity artifacts, checkpoints and child assignments are bound to. */
|
|
88
|
+
id: string;
|
|
89
|
+
ownerSessionId: string;
|
|
90
|
+
runId: string;
|
|
91
|
+
worktree: string;
|
|
92
|
+
domain: Domain;
|
|
93
|
+
role: Role;
|
|
94
|
+
state: AssignmentState;
|
|
95
|
+
grants: AssignmentGrant[];
|
|
96
|
+
/** The assignment that issued this one (delegated children). */
|
|
97
|
+
parentAssignmentId?: string;
|
|
98
|
+
issuedAt: string;
|
|
99
|
+
endedAt?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Upper bound on the persisted assignment registry; terminal records are never trimmed. */
|
|
103
|
+
export const MAX_ASSIGNMENTS = 64;
|
|
104
|
+
|
|
77
105
|
/** One finished subagent run of any role, kept for `/bot-lobby runs`. */
|
|
78
106
|
export interface RunLogEntry {
|
|
79
107
|
runId: string;
|
|
@@ -226,6 +254,8 @@ export interface PhaseTiming {
|
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
export interface Task {
|
|
257
|
+
/** Advisory task-state checkpoint; external references require live revalidation. */
|
|
258
|
+
checkpoint?: TaskCheckpoint;
|
|
229
259
|
/** Canonical waits, including legacy tasks without a reliable clock. */
|
|
230
260
|
blockingRequestIds?: string[];
|
|
231
261
|
phaseTiming?: PhaseTiming;
|
|
@@ -248,6 +278,8 @@ export interface Task {
|
|
|
248
278
|
approvals: Approval[];
|
|
249
279
|
/** Worker delegations in start order; absent on tasks created before tracking. */
|
|
250
280
|
workerRuns?: WorkerRunRecord[];
|
|
281
|
+
/** Parent-issued assignment identities for artifact/checkpoint reads; terminal records are final. */
|
|
282
|
+
assignments?: AssignmentRecord[];
|
|
251
283
|
/** Recent finished runs of every role, newest last. */
|
|
252
284
|
runLog?: RunLogEntry[];
|
|
253
285
|
createdAt: string;
|