@a-t-h-i/bot-lobby 0.6.32 → 0.6.34
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 +30 -0
- package/package.json +7 -7
- package/src/desk/client-extension.ts +2 -2
- package/src/desk/ipc.ts +4 -3
- package/src/desk/session.ts +14 -1
- package/src/execution/agent-runner.ts +16 -92
- package/src/execution/delegation.ts +193 -0
- package/src/execution/live-run.ts +95 -0
- package/src/execution/pi-runner.ts +35 -9
- package/src/index.ts +5 -0
- package/src/lobby/planner.ts +9 -0
- package/src/lobby/pr-review.ts +9 -0
- package/src/lobby/quickfix.ts +16 -0
- package/src/master/master.ts +3 -1
- package/src/master/research.ts +1 -0
- package/src/pi/auto-compaction.ts +72 -0
- package/src/pi/delegation.ts +68 -0
- package/src/pi/model-support.ts +1 -1
- package/src/pi/plan-checklist.ts +18 -7
- package/src/pi/run-summary.ts +9 -0
- package/src/schemas/configuration.ts +19 -7
- package/src/schemas/findings.ts +4 -0
- package/src/schemas/task.ts +4 -0
- package/src/state/work-time.ts +4 -2
- package/src/webui/api/index.ts +2 -2
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/metrics.ts +41 -8
- package/src/webui/api/plan-facts.ts +4 -3
- package/src/webui/api/tasks.ts +1 -1
- package/src/webui/dev/fake-service.ts +1 -1
- package/src/webui/dev/fixtures.ts +1 -1
- package/src/webui/protocol.ts +4 -3
- package/src/workflow/workflow.ts +4 -1
- package/webui/dist/assets/index-B4zpEv7K.js +109 -0
- package/webui/dist/assets/index-C8hgfw6m.css +2 -0
- package/webui/dist/build.json +39 -27
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BtKUZnAE.css +0 -2
- package/webui/dist/assets/index-CmNcHve-.js +0 -108
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { AgentRun } from "../schemas/findings.ts";
|
|
2
|
+
import type { Domain, Role } from "../schemas/agent.ts";
|
|
3
|
+
import { delegationHost } from "./delegation.ts";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
1
5
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
6
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -6,7 +10,7 @@ import { activityWord } from "../pi/activity.ts";
|
|
|
6
10
|
import { shortDuration } from "../text.ts";
|
|
7
11
|
import { readRelayRequest } from "../ask/relay.ts";
|
|
8
12
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
9
|
-
import { grantEnv, grantNote,
|
|
13
|
+
import { grantEnv, grantNote, type Grant } from "../excalidraw/sessions.ts";
|
|
10
14
|
|
|
11
15
|
/** Live control over one running subagent: queue a steering message for its next turn. */
|
|
12
16
|
export interface RunHandle {
|
|
@@ -20,6 +24,10 @@ export interface RunHandle {
|
|
|
20
24
|
export type RelayAsk = (questions: AskQuestion[], signal: AbortSignal) => Promise<AskResult>;
|
|
21
25
|
|
|
22
26
|
export interface PiRunOptions {
|
|
27
|
+
agent?: { runId: string; taskId: string; domain: Domain; role: Role; depth?: number };
|
|
28
|
+
mcpTools?: string[];
|
|
29
|
+
delegationQuota?: { used: number };
|
|
30
|
+
onChildRun?: (run: AgentRun) => void;
|
|
23
31
|
cwd: string;
|
|
24
32
|
task: string;
|
|
25
33
|
systemPrompt?: string;
|
|
@@ -96,6 +104,7 @@ export type PiStreamEvent =
|
|
|
96
104
|
| { type: "retry"; attempt: number; maxAttempts: number; delayMs: number; error: string }
|
|
97
105
|
| { type: "retry_end"; success: boolean }
|
|
98
106
|
| { type: "compaction" }
|
|
107
|
+
| { type: "compaction_end"; error?: string }
|
|
99
108
|
| { type: "usage"; input: number; output: number; cost: number; model?: string }
|
|
100
109
|
| { type: "wrap_up" }
|
|
101
110
|
/** Its allotted time is up: it was asked to stop and report where it left off. */
|
|
@@ -139,6 +148,7 @@ export type ProcessRunner = (
|
|
|
139
148
|
) => Promise<ProcessOutcome>;
|
|
140
149
|
|
|
141
150
|
export interface PiRunResult {
|
|
151
|
+
children?: AgentRun[];
|
|
142
152
|
status: "success" | "failed" | "cancelled" | "timeout";
|
|
143
153
|
output: string;
|
|
144
154
|
error?: string;
|
|
@@ -186,6 +196,7 @@ export interface ControlEvent {
|
|
|
186
196
|
/** extension_ui_request: the dialog's title and (editor) prefilled text. */
|
|
187
197
|
title?: string;
|
|
188
198
|
prefill?: string;
|
|
199
|
+
message?: string;
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
/** Buffers one JSON-mode stream, keeping only lines parsePiStream consumes. */
|
|
@@ -203,6 +214,7 @@ const INTERESTING = [
|
|
|
203
214
|
'"turn_start"',
|
|
204
215
|
'"auto_retry_',
|
|
205
216
|
'"compaction_start"',
|
|
217
|
+
'"compaction_end"',
|
|
206
218
|
'"agent_settled"',
|
|
207
219
|
'"agent_end"',
|
|
208
220
|
'"extension_ui_request"',
|
|
@@ -241,6 +253,8 @@ function streamEvent(event: RawEvent): PiStreamEvent | undefined {
|
|
|
241
253
|
return { type: "retry_end", success: event.success === true };
|
|
242
254
|
case "compaction_start":
|
|
243
255
|
return { type: "compaction" };
|
|
256
|
+
case "compaction_end":
|
|
257
|
+
return { type: "compaction_end", ...(typeof event.errorMessage === "string" ? { error: event.errorMessage } : {}) };
|
|
244
258
|
case "message_end": {
|
|
245
259
|
const message = event.message;
|
|
246
260
|
if (message?.role !== "assistant") return undefined;
|
|
@@ -345,7 +359,7 @@ export function finishedThought(line: string): string | undefined {
|
|
|
345
359
|
|
|
346
360
|
/** Build the `pi` argv for one isolated, headless RPC agent run; the task goes over stdin. */
|
|
347
361
|
export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string }): string[] {
|
|
348
|
-
const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes"];
|
|
362
|
+
const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes", "--extension", fileURLToPath(new URL("../index.ts", import.meta.url))];
|
|
349
363
|
if (options.model && options.model !== "inherit") args.push("--model", options.model);
|
|
350
364
|
if (options.thinking) args.push("--thinking", options.thinking);
|
|
351
365
|
if (options.tools && options.tools.length > 0) args.push("--tools", options.tools.join(","));
|
|
@@ -608,6 +622,11 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
|
|
|
608
622
|
});
|
|
609
623
|
};
|
|
610
624
|
const onControl = (event: ControlEvent) => {
|
|
625
|
+
if (event.type === "extension_ui_request" && event.method === "notify") {
|
|
626
|
+
if (event.message === "Compacting agent context") options.onEvent?.({ type: "compaction" });
|
|
627
|
+
else if (event.message === "Agent context compacted") options.onEvent?.({ type: "compaction_end" });
|
|
628
|
+
else if (event.message?.startsWith("Context compaction failed:")) options.onEvent?.({ type: "compaction_end", error: event.message });
|
|
629
|
+
}
|
|
611
630
|
const relayed = event.type === "extension_ui_request" && event.method === "editor" && options.onAsk ? readRelayRequest(event.title, event.prefill) : undefined;
|
|
612
631
|
if (event.type === "response" && event.command === "prompt" && event.success === false) {
|
|
613
632
|
state.protocolError = event.error ?? "the prompt was rejected";
|
|
@@ -744,31 +763,38 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
|
|
|
744
763
|
|
|
745
764
|
/** Run one isolated subagent and normalise its outcome. */
|
|
746
765
|
export async function runPiAgent(options: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
|
|
747
|
-
const
|
|
766
|
+
const host = await delegationHost(options, run);
|
|
767
|
+
let prompt: ReturnType<typeof writePromptFile> | undefined;
|
|
748
768
|
const grant = options.excalidraw;
|
|
749
|
-
// An agent with an allowlist gets the Excalidraw tools added to it; one without is not restricted, so already has them.
|
|
750
|
-
const tools = grant && options.tools ? [...new Set([...options.tools, ...toolsOf(grant)])] : options.tools;
|
|
751
769
|
try {
|
|
752
|
-
|
|
770
|
+
prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
|
|
771
|
+
if (options.signal?.aborted) return { status: "cancelled", output: "", usage: { input: 0, output: 0, cost: 0, turns: 0 } };
|
|
772
|
+
const args = buildPiArgs({ ...options, tools: host.tools, systemPromptFile: prompt.file });
|
|
753
773
|
const outcome = await run(args, {
|
|
754
774
|
cwd: options.cwd,
|
|
755
775
|
signal: options.signal,
|
|
756
776
|
timeoutMs: options.timeoutMs,
|
|
757
777
|
prompt: `Task: ${options.task}${grant ? `\n\n${grantNote(grant)}` : ""}`,
|
|
758
|
-
env: grant ? { ...
|
|
778
|
+
env: grant ? { ...host.env, ...grantEnv(grant) } : host.env,
|
|
759
779
|
stallTimeoutMs: options.stallTimeoutMs,
|
|
760
780
|
toolStallTimeoutMs: options.toolStallTimeoutMs,
|
|
761
781
|
wrapUpAtMs: options.wrapUpAtMs,
|
|
762
782
|
...(options.time ? { time: options.time } : {}),
|
|
763
783
|
...(options.onAsk ? { onAsk: options.onAsk } : {}),
|
|
764
|
-
onStart:
|
|
784
|
+
onStart: host.onStart,
|
|
765
785
|
onEvent: (event) => {
|
|
786
|
+
if (event.type === "asking") host.pause?.();
|
|
787
|
+
if (event.type === "answered") host.extend?.(event.waitedMs);
|
|
788
|
+
if (event.type === "extended") host.extend?.(event.ms, true);
|
|
766
789
|
if (event.type === "tool_execution_start") options.onActivity?.(activityWord(event.toolName));
|
|
767
790
|
options.onEvent?.(event);
|
|
768
791
|
},
|
|
769
792
|
});
|
|
770
|
-
|
|
793
|
+
const result = toResult(parsePiStream(outcome.stdout), outcome, options.signal?.aborted === true, options);
|
|
794
|
+
await host.close();
|
|
795
|
+
return host.children.length ? { ...result, children: host.children } : result;
|
|
771
796
|
} finally {
|
|
772
797
|
prompt?.cleanup();
|
|
798
|
+
await host.close();
|
|
773
799
|
}
|
|
774
800
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerAgentPermissions, registerDelegation } from "./pi/delegation.ts";
|
|
2
|
+
import { registerAutoCompaction } from "./pi/auto-compaction.ts";
|
|
1
3
|
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { registerCommands } from "./pi/commands.ts";
|
|
3
5
|
import { registerLifecycle } from "./pi/events.ts";
|
|
@@ -20,6 +22,9 @@ import { registerWebTools } from "./web/tools.ts";
|
|
|
20
22
|
import { registerExcalidrawTools } from "./excalidraw/tools.ts";
|
|
21
23
|
|
|
22
24
|
export default function (pi: ExtensionAPI): void {
|
|
25
|
+
registerAgentPermissions(pi);
|
|
26
|
+
registerDelegation(pi);
|
|
27
|
+
registerAutoCompaction(pi);
|
|
23
28
|
let root = detectProjectRoot(process.cwd(), CONFIG_DIR_NAME);
|
|
24
29
|
pi.on("session_start", (_event, ctx) => { root = detectProjectRoot(ctx.cwd, CONFIG_DIR_NAME); });
|
|
25
30
|
// First, so every session_start handler below finds the classifier bound to this session's keys.
|
package/src/lobby/planner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
/**
|
|
2
3
|
* Task planning mode: a planning panel. The user describes an idea (or picks a
|
|
3
4
|
* GitHub issue) and every seat questions them from its own domain — DEV,
|
|
@@ -998,14 +999,22 @@ export class PlanningSession {
|
|
|
998
999
|
|
|
999
1000
|
private async run(label: string, kind: "planner" | "panel", profile: QuickFixProfile, request: { task: string; systemPrompt: string; tools: readonly string[]; excalidraw?: Grant }, signal: AbortSignal, setStep: (step: string) => void, routedFrom?: string): Promise<RunOutcome> {
|
|
1000
1001
|
const startedAt = Date.now();
|
|
1002
|
+
const runId = randomUUID();
|
|
1001
1003
|
let outcome: RunOutcome;
|
|
1002
1004
|
try {
|
|
1003
1005
|
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
1004
1006
|
{
|
|
1005
1007
|
cwd: this.deps.cwd,
|
|
1008
|
+
agent: { runId, taskId: runId, domain: label === "DESIGN" ? "designer" : label === "QA" ? "qa" : "backend", role: label === "RESEARCH" ? "researcher" : "reviewer" },
|
|
1006
1009
|
...request,
|
|
1007
1010
|
model,
|
|
1008
1011
|
thinking,
|
|
1012
|
+
mcpTools: profile.mcpTools,
|
|
1013
|
+
onChildRun: (child) => {
|
|
1014
|
+
this.deps.feed?.step(label, `child ${child.runId.slice(0, 8)}: ${child.step ?? child.status}`, child.runId);
|
|
1015
|
+
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
1016
|
+
this.deps.onChange?.();
|
|
1017
|
+
},
|
|
1009
1018
|
timeoutMs: profile.timeoutMs,
|
|
1010
1019
|
signal,
|
|
1011
1020
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
package/src/lobby/pr-review.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
/**
|
|
2
3
|
* Reviewing pull requests from the Git tab. A read-only agent (on QA's model,
|
|
3
4
|
* thinking and time limit) gets the pull request's description, changed files
|
|
@@ -264,17 +265,25 @@ export class PullReviews {
|
|
|
264
265
|
|
|
265
266
|
private async run(review: PullReview, task: string, signal: AbortSignal): Promise<RunUsage> {
|
|
266
267
|
const profile = this.deps.profile();
|
|
268
|
+
const runId = randomUUID();
|
|
267
269
|
if (profile.model) review.model = profile.model;
|
|
268
270
|
review.thinking = profile.thinking;
|
|
269
271
|
this.step(review, "reading the diff");
|
|
270
272
|
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
271
273
|
{
|
|
272
274
|
cwd: this.deps.cwd,
|
|
275
|
+
agent: { runId, taskId: runId, domain: "qa", role: "reviewer" },
|
|
273
276
|
task,
|
|
274
277
|
systemPrompt: reviewPrompt(profile.instructions),
|
|
275
278
|
tools: REVIEW_TOOLS,
|
|
276
279
|
model,
|
|
277
280
|
thinking,
|
|
281
|
+
mcpTools: profile.mcpTools,
|
|
282
|
+
onChildRun: (child) => {
|
|
283
|
+
this.deps.feed?.step(REVIEW_SOURCE, `child ${child.runId.slice(0, 8)}: ${child.step ?? child.status}`, child.runId);
|
|
284
|
+
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
285
|
+
this.deps.onChange?.();
|
|
286
|
+
},
|
|
278
287
|
timeoutMs: profile.timeoutMs,
|
|
279
288
|
signal,
|
|
280
289
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentRun } from "../schemas/findings.ts";
|
|
1
2
|
/**
|
|
2
3
|
* Quick fixes: a direct prompt from the lobby goes straight to one coding
|
|
3
4
|
* subagent — no scouting, proposal, plan or QA gate — while any bot-lobby task
|
|
@@ -38,6 +39,7 @@ export interface QuickFixStep {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export interface QuickFixJob {
|
|
42
|
+
children?: AgentRun[];
|
|
41
43
|
id: string;
|
|
42
44
|
prompt: string;
|
|
43
45
|
status: QuickFixStatus;
|
|
@@ -83,6 +85,7 @@ export interface QuickFixProfile {
|
|
|
83
85
|
thinking: string;
|
|
84
86
|
timeoutMs: number;
|
|
85
87
|
instructions?: string;
|
|
88
|
+
mcpTools?: string[];
|
|
86
89
|
/** Where the job goes when its model runs out of usage or is unavailable. */
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -295,6 +298,7 @@ export class QuickFixQueue {
|
|
|
295
298
|
this.edits.set(job.id, new EditLog(this.deps.cwd));
|
|
296
299
|
try {
|
|
297
300
|
const [likely, route] = await Promise.all([this.likely(job.prompt, controller.signal), this.route(job.prompt, profile, controller.signal)]);
|
|
301
|
+
const quota = { used: 0 };
|
|
298
302
|
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
299
303
|
{
|
|
300
304
|
cwd: this.deps.cwd,
|
|
@@ -304,6 +308,18 @@ export class QuickFixQueue {
|
|
|
304
308
|
...grantOption("quickfix"),
|
|
305
309
|
model,
|
|
306
310
|
thinking,
|
|
311
|
+
mcpTools: profile.mcpTools,
|
|
312
|
+
delegationQuota: quota,
|
|
313
|
+
agent: { runId: job.id, taskId: job.id, domain: "backend", role: "worker" },
|
|
314
|
+
onChildRun: (child) => {
|
|
315
|
+
const edits = this.edits.get(job.id);
|
|
316
|
+
for (const path of child.edited ?? []) edits?.note("edit", { path });
|
|
317
|
+
if (edits) job.files = edits.shown();
|
|
318
|
+
job.children = [...(job.children ?? []).filter((entry) => entry.runId !== child.runId), child];
|
|
319
|
+
this.deps.feed?.step(QUICK_FIX_SOURCE, `child ${child.runId.slice(0, 8)}: ${child.step ?? child.status}`, child.runId);
|
|
320
|
+
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
|
321
|
+
this.changed();
|
|
322
|
+
},
|
|
307
323
|
timeoutMs: profile.timeoutMs,
|
|
308
324
|
signal: controller.signal,
|
|
309
325
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
package/src/master/master.ts
CHANGED
|
@@ -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, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
55
|
+
return { model: profile.model, mcpTools: profile.mcpTools, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
|
@@ -141,6 +141,7 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
141
141
|
]);
|
|
142
142
|
const extraTools = request.hints?.tools() ?? [];
|
|
143
143
|
const requests: AgentRequest[] = request.domains.map((domain, index) => ({
|
|
144
|
+
delegationQuota: { used: 0 },
|
|
144
145
|
taskId: request.taskId,
|
|
145
146
|
domain,
|
|
146
147
|
role: "scout",
|
|
@@ -286,6 +287,7 @@ export async function runWorker(
|
|
|
286
287
|
const hintTools = request.hints?.tools() ?? [];
|
|
287
288
|
const extraTools = [...(request.agent?.extraTools ?? []), ...hintTools];
|
|
288
289
|
const base: AgentRequest = {
|
|
290
|
+
delegationQuota: { used: 0 },
|
|
289
291
|
taskId: request.taskId,
|
|
290
292
|
domain: request.domain,
|
|
291
293
|
role: "worker",
|
package/src/master/research.ts
CHANGED
|
@@ -70,6 +70,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
70
70
|
instruction: request.instruction,
|
|
71
71
|
context: researchContext(request),
|
|
72
72
|
model: profile.model,
|
|
73
|
+
mcpTools: profile.mcpTools,
|
|
73
74
|
thinking: profile.thinking,
|
|
74
75
|
timeoutMs: profile.timeoutMs,
|
|
75
76
|
...(request.time ? { time: request.time } : {}),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, type ExtensionAPI, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { contextMarks, freshContextOn } from "./fresh-context.ts";
|
|
3
|
+
import { isSubagentProcess } from "./quiet.ts";
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_TARGET = 250_000;
|
|
6
|
+
|
|
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), window - Math.min(reserve, Math.floor(window / 2))));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Compact at a settled tool boundary; ctx.compact() would abort the turn we need to continue. */
|
|
12
|
+
export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compact = compact): void {
|
|
13
|
+
let busy = false;
|
|
14
|
+
let lastLeaf: string | null | undefined;
|
|
15
|
+
pi.on("session_start", () => { busy = false; lastLeaf = undefined; });
|
|
16
|
+
pi.on("turn_end", async (event: TurnEndEvent, ctx) => {
|
|
17
|
+
if (busy || !ctx.model) return;
|
|
18
|
+
const leaf = ctx.sessionManager.getLeafId();
|
|
19
|
+
if (leaf === lastLeaf) return;
|
|
20
|
+
const configured = pi.getSettings().compaction;
|
|
21
|
+
const override = configured?.modelOverrides?.[`${ctx.model.provider}/${ctx.model.id}`];
|
|
22
|
+
const reserve = Math.min(override?.reserveTokens ?? configured?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens, Math.floor(ctx.model.contextWindow / 2));
|
|
23
|
+
const threshold = compactionThreshold(ctx.model.contextWindow, reserve);
|
|
24
|
+
const estimate = event.context.contextMessages.reduce((total, message) => total + estimateTokens(message), 0) + Math.ceil(ctx.getSystemPrompt().length / 4);
|
|
25
|
+
const usage = ctx.getContextUsage();
|
|
26
|
+
if (Math.max(estimate, usage?.tokens ?? 0) < threshold) return;
|
|
27
|
+
let entries = ctx.sessionManager.buildContextEntries();
|
|
28
|
+
if (!isSubagentProcess() && freshContextOn()) {
|
|
29
|
+
const boundary = contextMarks(ctx.sessionManager.getBranch()).at(-1)?.at;
|
|
30
|
+
if (boundary !== undefined) entries = entries.filter((entry) => Date.parse(entry.timestamp) >= boundary);
|
|
31
|
+
}
|
|
32
|
+
if (entries.length < 2) return;
|
|
33
|
+
const keepRecentTokens = Math.min(override?.keepRecentTokens ?? configured?.keepRecentTokens ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens, Math.floor(threshold / 4));
|
|
34
|
+
const cut = findCutPoint(entries, 0, entries.length, keepRecentTokens);
|
|
35
|
+
if (cut.firstKeptEntryIndex <= 0) return;
|
|
36
|
+
const prefix = entries.slice(0, cut.firstKeptEntryIndex);
|
|
37
|
+
const messages = prefix.flatMap((entry) => entry.type === "message" ? [entry.message] : []);
|
|
38
|
+
if (!messages.length) return;
|
|
39
|
+
const previous = [...entries].reverse().find((entry) => entry.type === "compaction");
|
|
40
|
+
const firstKeptEntryId = entries[cut.firstKeptEntryIndex]!.id;
|
|
41
|
+
const fileOps = { read: new Set<string>(), written: new Set<string>(), edited: new Set<string>() };
|
|
42
|
+
const details = previous?.type === "compaction" ? previous.details as { readFiles?: string[]; modifiedFiles?: string[] } | undefined : undefined;
|
|
43
|
+
for (const path of details?.readFiles ?? []) fileOps.read.add(path);
|
|
44
|
+
for (const path of details?.modifiedFiles ?? []) fileOps.edited.add(path);
|
|
45
|
+
for (const message of messages) {
|
|
46
|
+
const calls = message.role === "assistant" ? message.content.filter((part) => part.type === "toolCall") : message.role === "toolResult" ? message.nestedCalls?.calls ?? [] : [];
|
|
47
|
+
for (const call of calls) {
|
|
48
|
+
const path = call.arguments?.path;
|
|
49
|
+
if (typeof path !== "string") continue;
|
|
50
|
+
if (call.name === "read") fileOps.read.add(path);
|
|
51
|
+
if (call.name === "write") fileOps.written.add(path);
|
|
52
|
+
if (call.name === "edit") fileOps.edited.add(path);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
lastLeaf = leaf;
|
|
56
|
+
busy = true;
|
|
57
|
+
ctx.ui.notify("Compacting agent context", "info");
|
|
58
|
+
try {
|
|
59
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
60
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
61
|
+
const headers = auth.headers ? Object.fromEntries(Object.entries(auth.headers).filter((entry): entry is [string, string] => entry[1] !== null)) : undefined;
|
|
62
|
+
const retry = pi.getSettings().retry;
|
|
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, "Preserve goals, constraints, decisions, file references, verification evidence, unfinished work and outstanding delegated subtasks. The parent remains responsible for integration and verification.", ctx.signal, undefined, undefined, auth.env, retryPolicy);
|
|
65
|
+
ctx.signal?.throwIfAborted();
|
|
66
|
+
ctx.ui.notify("Agent context compacted", "info");
|
|
67
|
+
return { entries: [...event.entries, { type: "compaction" as const, summary: result.summary, firstKeptEntryId, usage: result.usage, details: result.details }] };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
ctx.ui.notify(`Context compaction failed: ${(error as Error).message}`, "warning");
|
|
70
|
+
} finally { busy = false; }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { createDeskClient } from "../desk/ipc.ts";
|
|
4
|
+
import { AGENT_ENV, DELEGATION_ENV, DELEGATE_TOOL, MAX_CHILDREN, type AgentPolicy } from "../execution/delegation.ts";
|
|
5
|
+
import { loadConfig } from "../state/project.ts";
|
|
6
|
+
import { isSubagentProcess } from "./quiet.ts";
|
|
7
|
+
|
|
8
|
+
export function allowedAgentTool(policy: AgentPolicy, name: string, readOnlyHint?: boolean, hidden = false): boolean {
|
|
9
|
+
if (hidden) return false;
|
|
10
|
+
if (name.startsWith("mcp__")) return policy.mcpTools.includes(name) && (!policy.readOnly || readOnlyHint === true);
|
|
11
|
+
if (name === DELEGATE_TOOL && policy.depth > 1) return false;
|
|
12
|
+
return policy.tools.includes(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Enforce the same permissions on direct, discovered and codemode-nested calls. */
|
|
16
|
+
export function registerAgentPermissions(pi: ExtensionAPI): void {
|
|
17
|
+
const raw = process.env[AGENT_ENV];
|
|
18
|
+
let policy: AgentPolicy | undefined;
|
|
19
|
+
if (raw) {
|
|
20
|
+
try { policy = JSON.parse(raw) as AgentPolicy; }
|
|
21
|
+
catch { throw new Error("Invalid bot-lobby agent policy."); }
|
|
22
|
+
if (!Array.isArray(policy?.tools) || !Array.isArray(policy?.mcpTools) || typeof policy.readOnly !== "boolean" || ![1, 2].includes(policy.depth)) throw new Error("Invalid bot-lobby agent policy.");
|
|
23
|
+
}
|
|
24
|
+
pi.on("tool_call", (event) => {
|
|
25
|
+
const info = pi.getAllTools().find((tool) => tool.name === event.toolName);
|
|
26
|
+
if (!info) return { block: true, reason: `Unknown tool ${event.toolName}.` };
|
|
27
|
+
if (["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"].includes(event.toolName)) {
|
|
28
|
+
const server = (event.input as { server?: unknown }).server;
|
|
29
|
+
const grants = policy?.mcpTools ?? loadConfig().master.mcpTools ?? [];
|
|
30
|
+
if (typeof server === "string" && grants.some((tool) => tool.startsWith(`mcp__${server.replace(/[^A-Za-z0-9_]/g, "_")}__`)) && info?.exposure !== "hidden") return;
|
|
31
|
+
return { block: true, reason: "Specify an MCP server with an explicit tool grant to read its resources." };
|
|
32
|
+
}
|
|
33
|
+
if (policy) {
|
|
34
|
+
if (allowedAgentTool(policy, event.toolName, info?.annotations?.readOnlyHint, info?.exposure === "hidden")) return;
|
|
35
|
+
return { block: true, reason: `This agent is not granted ${event.toolName}.` };
|
|
36
|
+
}
|
|
37
|
+
if (event.toolName.startsWith("mcp__")) {
|
|
38
|
+
if ((loadConfig().master.mcpTools ?? []).includes(event.toolName) && info?.exposure !== "hidden") return;
|
|
39
|
+
return { block: true, reason: `Grant ${event.toolName} to the master in bot-lobby settings first.` };
|
|
40
|
+
}
|
|
41
|
+
if (event.parentToolCallId && !pi.getActiveTools().includes(event.toolName)) return { block: true, reason: `${event.toolName} is not active for this agent.` };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerDelegation(pi: ExtensionAPI): void {
|
|
46
|
+
if (!isSubagentProcess() || !process.env[DELEGATION_ENV]) return;
|
|
47
|
+
const target = JSON.parse(process.env[DELEGATION_ENV]!) as { address: string; token: string; timeoutMs: number };
|
|
48
|
+
const client = createDeskClient(target.address, target.token);
|
|
49
|
+
pi.on("session_shutdown", () => client.close());
|
|
50
|
+
pi.registerTool({
|
|
51
|
+
name: DELEGATE_TOOL,
|
|
52
|
+
label: "Delegate subtasks",
|
|
53
|
+
description: "Split substantial, separable work among at most five children total. Supply complete briefs with goals, files, contracts, constraints and done criteria. Wait for their reports, integrate changes and verify the whole task yourself. Children cannot delegate further.",
|
|
54
|
+
parameters: Type.Object({ briefs: Type.Array(Type.String({ minLength: 1, maxLength: 32_000 }), { minItems: 1, maxItems: MAX_CHILDREN }) }),
|
|
55
|
+
executionMode: "sequential",
|
|
56
|
+
async execute(_id, params, signal) {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const cancel = () => { void client.request({ op: "cancel_delegate" }); };
|
|
59
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
60
|
+
try {
|
|
61
|
+
const answer = await client.request({ op: "delegate", briefs: params.briefs }, target.timeoutMs + 10_000);
|
|
62
|
+
if (!answer.ok) throw new Error(answer.text);
|
|
63
|
+
const children = JSON.parse(answer.text) as Array<{ runId: string; status: string; report: string; reportPath?: string; error?: string }>;
|
|
64
|
+
return { content: [{ type: "text", text: children.map((child) => `Child ${child.runId}: ${child.status}\n${child.report}${child.reportPath ? `\nFull report: ${child.reportPath}` : ""}${child.error ? `\nError: ${child.error}` : ""}`).join("\n\n") }], details: { children } };
|
|
65
|
+
} finally { signal?.removeEventListener("abort", cancel); }
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
package/src/pi/model-support.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string };
|
|
93
|
+
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] };
|
|
94
94
|
|
|
95
95
|
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
96
96
|
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
package/src/pi/plan-checklist.ts
CHANGED
|
@@ -315,7 +315,7 @@ export function targetStep(steps: readonly string[], instruction: string | undef
|
|
|
315
315
|
export function latestWorkerRun(runs: readonly AgentRun[]): AgentRun | undefined {
|
|
316
316
|
let latest: AgentRun | undefined;
|
|
317
317
|
for (const run of runs) {
|
|
318
|
-
if (run.role !== "worker" || !run.instruction) continue;
|
|
318
|
+
if (run.role !== "worker" || run.parentRunId || !run.instruction) continue;
|
|
319
319
|
if (!latest || Date.parse(run.startedAt) >= Date.parse(latest.startedAt)) latest = run;
|
|
320
320
|
}
|
|
321
321
|
return latest;
|
|
@@ -360,7 +360,7 @@ function replaySteps(steps: readonly string[], runs: readonly AgentRun[]): { com
|
|
|
360
360
|
const latestRun = latestWorkerRun(runs);
|
|
361
361
|
let latest = -1;
|
|
362
362
|
for (const run of byStart(runs)) {
|
|
363
|
-
if (run.role !== "worker") continue;
|
|
363
|
+
if (run.role !== "worker" || run.parentRunId) continue;
|
|
364
364
|
const matched = targetStep(steps, run.instruction, completed);
|
|
365
365
|
if (run === latestRun) latest = matched >= 0 && run.status === "success" ? matched + 1 : matched;
|
|
366
366
|
if (run.status !== "success") continue;
|
|
@@ -401,6 +401,7 @@ export interface StepWork {
|
|
|
401
401
|
ms: number;
|
|
402
402
|
/** A worker is on it right now. */
|
|
403
403
|
active: boolean;
|
|
404
|
+
activeAgentCount: number;
|
|
404
405
|
}
|
|
405
406
|
|
|
406
407
|
/** The steps a run works on: every step of a range it names, else the step it targets, else the next one still open. */
|
|
@@ -419,19 +420,29 @@ function runSteps(steps: readonly string[], instruction: string | undefined, com
|
|
|
419
420
|
* completes them, so a later run that names no step goes to the next one.
|
|
420
421
|
*/
|
|
421
422
|
export function stepWork(steps: readonly string[], runs: readonly AgentRun[], now = Date.now()): StepWork[] {
|
|
422
|
-
const work = steps.map(() => ({ ms: 0, active: false }));
|
|
423
|
+
const work = steps.map(() => ({ ms: 0, active: false, activeAgentCount: 0 }));
|
|
424
|
+
const assigned = new Map<string, number[]>();
|
|
425
|
+
const counted = new Set<string>();
|
|
423
426
|
const completed = new Set<number>();
|
|
424
427
|
for (const run of byStart(runs)) {
|
|
425
428
|
if (run.role !== "worker") continue;
|
|
426
|
-
|
|
429
|
+
if (counted.has(run.runId)) continue;
|
|
430
|
+
counted.add(run.runId);
|
|
431
|
+
const covered = run.parentRunId
|
|
432
|
+
? assigned.get(run.parentRunId) ?? runSteps(steps, run.stepInstruction, completed)
|
|
433
|
+
: runSteps(steps, run.instruction, completed);
|
|
434
|
+
assigned.set(run.runId, covered);
|
|
427
435
|
const start = Date.parse(run.startedAt);
|
|
428
436
|
const end = run.finishedAt ? Date.parse(run.finishedAt) : now;
|
|
429
437
|
const ms = Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : 0;
|
|
430
438
|
for (const index of covered) {
|
|
431
|
-
work[index]!.ms += ms;
|
|
432
|
-
if (run.status === "running")
|
|
439
|
+
if (!run.parentRunId) work[index]!.ms += ms;
|
|
440
|
+
if (run.status === "running") {
|
|
441
|
+
work[index]!.active = true;
|
|
442
|
+
work[index]!.activeAgentCount += 1;
|
|
443
|
+
}
|
|
433
444
|
}
|
|
434
|
-
if (run.status === "success" && covered.length > 0) markThrough(completed, covered[covered.length - 1]!);
|
|
445
|
+
if (!run.parentRunId && run.status === "success" && covered.length > 0) markThrough(completed, covered[covered.length - 1]!);
|
|
435
446
|
}
|
|
436
447
|
return work;
|
|
437
448
|
}
|
package/src/pi/run-summary.ts
CHANGED
|
@@ -69,6 +69,7 @@ export function runFlags(run: Pick<AgentRun, "status" | "stalled" | "wrappedUp"
|
|
|
69
69
|
*/
|
|
70
70
|
export function describeRun(run: AgentRun, now = Date.now()): string {
|
|
71
71
|
const parts = [`${STATUS_ICONS[run.status]} ${agentName(run)} ${run.role}`, shortDuration(durationOf(run, now))];
|
|
72
|
+
if (run.parentRunId) parts.push(`child of ${run.parentRunId}`);
|
|
72
73
|
const turns = run.turns ?? run.usage?.turns;
|
|
73
74
|
if (turns) parts.push(`${turns} turn${turns === 1 ? "" : "s"}`);
|
|
74
75
|
if (run.tools) parts.push(`${run.tools} tool${run.tools === 1 ? "" : "s"}`);
|
|
@@ -84,6 +85,10 @@ export function describeRun(run: AgentRun, now = Date.now()): string {
|
|
|
84
85
|
export function runLogEntry(run: AgentRun): RunLogEntry {
|
|
85
86
|
return {
|
|
86
87
|
runId: run.runId,
|
|
88
|
+
...(run.parentRunId ? { parentRunId: run.parentRunId } : {}),
|
|
89
|
+
...(run.depth ? { depth: run.depth } : {}),
|
|
90
|
+
...(run.instruction ? { instruction: run.instruction } : {}),
|
|
91
|
+
...(run.stepInstruction ? { stepInstruction: run.stepInstruction } : {}),
|
|
87
92
|
domain: run.domain,
|
|
88
93
|
role: run.role,
|
|
89
94
|
status: run.status,
|
|
@@ -106,6 +111,10 @@ export function runLogEntry(run: AgentRun): RunLogEntry {
|
|
|
106
111
|
export function runFromLog(entry: RunLogEntry, taskId: string): AgentRun {
|
|
107
112
|
return {
|
|
108
113
|
runId: entry.runId,
|
|
114
|
+
...(entry.parentRunId ? { parentRunId: entry.parentRunId } : {}),
|
|
115
|
+
...(entry.depth ? { depth: entry.depth } : {}),
|
|
116
|
+
...(entry.instruction ? { instruction: entry.instruction } : {}),
|
|
117
|
+
...(entry.stepInstruction ? { stepInstruction: entry.stepInstruction } : {}),
|
|
109
118
|
taskId,
|
|
110
119
|
domain: entry.domain,
|
|
111
120
|
role: entry.role,
|
|
@@ -27,12 +27,15 @@ export interface AgentModelConfig {
|
|
|
27
27
|
thinking: string;
|
|
28
28
|
/** Free-form instructions layered on top of this agent's built-in prompt. */
|
|
29
29
|
instructions?: string;
|
|
30
|
+
/** Exact native MCP tool names this profile may call; empty grants none. */
|
|
31
|
+
mcpTools?: string[];
|
|
30
32
|
/** Time limit per run; falls back to `workflow.agentTimeoutMs`. */
|
|
31
33
|
timeoutMs?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
|
|
35
37
|
export interface ScoutConfig {
|
|
38
|
+
mcpTools?: string[];
|
|
36
39
|
model: ModelRef;
|
|
37
40
|
timeoutMs: number;
|
|
38
41
|
}
|
|
@@ -320,20 +323,27 @@ function positive(value: unknown): number | undefined {
|
|
|
320
323
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
321
324
|
}
|
|
322
325
|
|
|
326
|
+
export function normalizeMcpTools(value: unknown): string[] {
|
|
327
|
+
return Array.isArray(value) ? [...new Set(value.filter((tool): tool is string => typeof tool === "string" && /^mcp__[A-Za-z0-9_]+__[A-Za-z0-9_]+$/.test(tool)))] : [];
|
|
328
|
+
}
|
|
329
|
+
|
|
323
330
|
/** Merge one agent's override over its default; legacy `inherit` or unknown thinking falls back to the default level. */
|
|
324
331
|
function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConfig> | undefined): AgentModelConfig {
|
|
325
332
|
const merged = { ...base, ...(override ?? {}) };
|
|
326
333
|
const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
|
|
327
334
|
const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
|
|
328
|
-
|
|
335
|
+
const mcpTools = normalizeMcpTools(merged.mcpTools);
|
|
336
|
+
return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}) };
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
/** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
|
|
332
340
|
function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig {
|
|
333
341
|
const base = DEFAULT_CONFIG.scout;
|
|
342
|
+
const mcpTools = normalizeMcpTools(override?.mcpTools);
|
|
334
343
|
return {
|
|
335
344
|
model: typeof override?.model === "string" && override.model.trim() ? override.model : base.model,
|
|
336
345
|
timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
|
|
346
|
+
...(mcpTools.length ? { mcpTools } : {}),
|
|
337
347
|
};
|
|
338
348
|
}
|
|
339
349
|
|
|
@@ -479,6 +489,8 @@ export interface AgentProfile {
|
|
|
479
489
|
thinking: string;
|
|
480
490
|
timeoutMs: number;
|
|
481
491
|
instructions?: string;
|
|
492
|
+
/** Exact native MCP tool names this profile may call; empty grants none. */
|
|
493
|
+
mcpTools?: string[];
|
|
482
494
|
}
|
|
483
495
|
|
|
484
496
|
/** The settings entry a domain/role run draws from. */
|
|
@@ -503,10 +515,10 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
|
|
|
503
515
|
const instructions = config.agents[domain].instructions;
|
|
504
516
|
const fallback = config.workflow.agentTimeoutMs;
|
|
505
517
|
if (kind === "scout") {
|
|
506
|
-
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions };
|
|
518
|
+
return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(config.scout.mcpTools?.length ? { mcpTools: config.scout.mcpTools } : {}) };
|
|
507
519
|
}
|
|
508
520
|
const entry = kind === "researcher" ? config.researcher : config.agents[kind];
|
|
509
|
-
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions };
|
|
521
|
+
return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
|
|
510
522
|
}
|
|
511
523
|
|
|
512
524
|
/** The settings entry of a lobby agent (quick fix or planner). */
|
|
@@ -515,19 +527,19 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
|
|
|
515
527
|
}
|
|
516
528
|
|
|
517
529
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
|
518
|
-
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
530
|
+
export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
|
|
519
531
|
const entry = lobbyAgentConfig(config, kind);
|
|
520
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions };
|
|
532
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
|
|
521
533
|
}
|
|
522
534
|
|
|
523
535
|
/**
|
|
524
536
|
* A planning panel seat's profile: the domain's (or the researcher's) model,
|
|
525
537
|
* thinking and custom instructions, bounded by the planner's per-turn limit.
|
|
526
538
|
*/
|
|
527
|
-
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string } {
|
|
539
|
+
export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
|
|
528
540
|
const entry = member === "researcher" ? config.researcher : config.agents[member];
|
|
529
541
|
const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
|
|
530
|
-
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions };
|
|
542
|
+
return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
|
|
531
543
|
}
|
|
532
544
|
|
|
533
545
|
/** Resolves the model, thinking and time limit one subagent run uses. */
|
package/src/schemas/findings.ts
CHANGED
|
@@ -100,6 +100,10 @@ export interface ResearchResult {
|
|
|
100
100
|
|
|
101
101
|
export interface AgentRun {
|
|
102
102
|
runId: string;
|
|
103
|
+
parentRunId?: string;
|
|
104
|
+
depth?: number;
|
|
105
|
+
/** Parent instruction used to assign children to plan steps. */
|
|
106
|
+
stepInstruction?: string;
|
|
103
107
|
taskId: string;
|
|
104
108
|
domain: Domain;
|
|
105
109
|
role: Role;
|