@a-t-h-i/bot-lobby 0.6.55 → 0.6.57

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.
Files changed (41) hide show
  1. package/THIRD_PARTY_NOTICES.md +12 -1
  2. package/package.json +4 -1
  3. package/src/execution/agent-runner.ts +7 -0
  4. package/src/execution/delegation.ts +3 -1
  5. package/src/execution/pi-runner.ts +14 -6
  6. package/src/harness/acp.ts +272 -0
  7. package/src/harness/catalog.ts +257 -0
  8. package/src/harness/claude-sdk.ts +169 -0
  9. package/src/harness/claude.ts +188 -0
  10. package/src/harness/cli-args.ts +62 -0
  11. package/src/harness/codex.ts +155 -0
  12. package/src/harness/instructions.ts +45 -0
  13. package/src/harness/model-manifest.json +829 -0
  14. package/src/harness/process.ts +175 -0
  15. package/src/harness/runner.ts +188 -0
  16. package/src/harness/status.ts +115 -0
  17. package/src/lobby/host.ts +2 -0
  18. package/src/lobby/planner.ts +3 -2
  19. package/src/lobby/pr-review.ts +2 -1
  20. package/src/lobby/quickfix.ts +6 -2
  21. package/src/master/goal-check.ts +1 -0
  22. package/src/master/master.ts +3 -3
  23. package/src/master/research.ts +2 -1
  24. package/src/pi/auto-compaction.ts +23 -5
  25. package/src/pi/delegation.ts +5 -3
  26. package/src/pi/grants.ts +10 -0
  27. package/src/pi/model-support.ts +6 -2
  28. package/src/pi/quiet.ts +2 -0
  29. package/src/schemas/configuration.ts +67 -23
  30. package/src/schemas/harness.ts +122 -0
  31. package/src/webui/api/index.ts +2 -1
  32. package/src/webui/api/settings.ts +7 -1
  33. package/src/webui/dev/fake-service.ts +13 -0
  34. package/src/webui/protocol.ts +9 -0
  35. package/webui/dist/assets/{TerminalView-CIfjkb5H.js → TerminalView-C8VC7KXy.js} +1 -1
  36. package/webui/dist/assets/index-Bs1_77_1.js +114 -0
  37. package/webui/dist/assets/index-DOLQ4FDd.css +2 -0
  38. package/webui/dist/build.json +16 -12
  39. package/webui/dist/index.html +2 -2
  40. package/webui/dist/assets/index-BkXhtB6A.css +0 -2
  41. package/webui/dist/assets/index-DIM2-FUQ.js +0 -114
@@ -5,7 +5,9 @@
5
5
  The web UI's look (colours, radii, shadows, control and dialog styles, the
6
6
  sidebar rail, the chat rows, the work log, the composer (its surface, footer controls and send button), the select
7
7
  fields and drop-down lists, the loading and status animations and the
8
- built-in colour themes) is adapted from T3 Code,
8
+ built-in colour themes), and the third-party harness support (the model
9
+ manifest, CLI flags, prompt injection and ACP handling behind Settings >
10
+ Harnesses), is adapted from T3 Code,
9
11
  https://github.com/pingdotgg/t3code. No T3 Code package is a
10
12
  dependency; the styles and markup below were ported into this repository and
11
13
  rewired to bot-lobby's own data layer. Each adapted file starts with a comment
@@ -37,6 +39,15 @@ naming the file it came from.
37
39
  | `webui/src/app/TerminalDrawer.tsx` | `apps/web/src/components/ThreadTerminalDrawer.tsx` (the drawer, its floating toolbar and terminal list) |
38
40
  | `webui/src/tabs/lobby/InlineActivity.tsx` | `apps/web/src/components/chat/WorkLog.tsx` |
39
41
  | `src/webui/palette-core.ts` (the built-in themes) | `packages/shared/src/themePalettes.ts` (Blossom is their T3 Chat theme, renamed) |
42
+ | `src/harness/model-manifest.json` (copied as is) | `apps/server/src/provider/model-manifest.json` |
43
+ | `src/harness/catalog.ts` | `apps/server/src/provider/ClaudeModelCatalog.ts` (effort descriptors and maps), `CodexProvider.ts` (effort levels), `antigravityAuthSupport.ts` (`buildAntigravityAcpSpawnInput`) |
44
+ | `src/harness/cli-args.ts` | `packages/shared/src/cliArgs.ts` (`tokenizeCliArgs`) |
45
+ | `src/harness/instructions.ts` | `packages/provider-core/src/server/runtimeInstructions.ts`, `orchestrationInstructions.ts` (`t3AcpPromptWithInstructions`) |
46
+ | `src/harness/codex.ts` | `apps/server/src/textGeneration/CodexTextGeneration.ts`, `apps/server/src/provider/codexLaunchArgs.ts` |
47
+ | `src/harness/claude.ts` | `apps/server/src/textGeneration/ClaudeTextGeneration.ts`, `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` (auto-compact window) |
48
+ | `src/harness/claude-sdk.ts` | `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` (Agent SDK query options) |
49
+ | `src/harness/acp.ts` | `apps/server/src/provider/acp/AntigravityProtocol.ts` (permission answers), `packages/provider-acp/src/server/sessionConfig.ts` (session option categories) |
50
+ | `src/schemas/harness.ts`, `webui/src/tabs/settings/HarnessesGroup.tsx` | `packages/contracts/src/settings.ts` (provider settings fields), `apps/web/src/components/settings/ProviderInstanceCard.tsx` |
40
51
 
41
52
  ```
42
53
  MIT License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-t-h-i/bot-lobby",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Structured multi-agent software engineering orchestrator for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -96,5 +96,8 @@
96
96
  "typebox": "1.3.27",
97
97
  "typescript": "^5.7.0",
98
98
  "vite": "8.3.2"
99
+ },
100
+ "optionalDependencies": {
101
+ "@anthropic-ai/claude-agent-sdk": "^0.3.306"
99
102
  }
100
103
  }
@@ -9,6 +9,7 @@ import { runPiAgent, spawnPiProcess, type ProcessRunner, type RelayAsk } from ".
9
9
  import { ASK_ENV } from "../ask/relay.ts";
10
10
  import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
11
11
  import { ASK_TOOL } from "../ask/types.ts";
12
+ import type { HarnessRun } from "../schemas/harness.ts";
12
13
 
13
14
  export interface AgentContext {
14
15
  task: string;
@@ -48,6 +49,8 @@ export interface AgentRequest {
48
49
  mcpTools?: string[];
49
50
  /** False when the agent's settings turn its auto-compaction off. */
50
51
  autoCompact?: boolean;
52
+ /** Tokens at which this agent compacts, when its settings set it. */
53
+ compactAt?: number;
51
54
  delegationQuota?: { used: number; consults?: number };
52
55
  /** See PiRunOptions.expert. */
53
56
  expert?: { model?: string; thinking: string };
@@ -75,6 +78,8 @@ export interface AgentRequest {
75
78
  route?: string;
76
79
  /** Lets the agent ask the user (ask_user_question, relayed through the master). */
77
80
  onAsk?: RelayAsk;
81
+ /** Run on this third-party harness instead of pi. */
82
+ harness?: HarnessRun;
78
83
  }
79
84
 
80
85
  /** The watchdog fields of an AgentRequest, taken from the workflow config. */
@@ -183,6 +188,7 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
183
188
  mcpTools: request.mcpTools,
184
189
  ...(request.skills ? { skills: request.skills } : {}),
185
190
  ...(request.autoCompact === false ? { autoCompact: false } : {}),
191
+ ...(request.compactAt ? { compactAt: request.compactAt } : {}),
186
192
  task: request.instruction,
187
193
  systemPrompt,
188
194
  tools: toolsFor(request),
@@ -190,6 +196,7 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
190
196
  thinking: request.thinking,
191
197
  signal,
192
198
  ...(request.onAsk ? { onAsk: request.onAsk } : {}),
199
+ ...(request.harness ? { harness: request.harness } : {}),
193
200
  env: request.onAsk ? { ...request.env, [ASK_ENV]: "1" } : request.env,
194
201
  stallTimeoutMs: request.stallTimeoutMs,
195
202
  toolStallTimeoutMs: request.toolStallTimeoutMs,
@@ -16,6 +16,7 @@ import { runPiAgent, type PiRunOptions, type ProcessRunner } from "./pi-runner.t
16
16
  import { startDeskServer } from "../desk/ipc.ts";
17
17
  import { DESK_ENV, DeskSession, deskSessionAt, PEER_TOOLS } from "../desk/session.ts";
18
18
  import { BOARD_ENV } from "../desk/board.ts";
19
+ import { mcpGrant } from "../pi/grants.ts";
19
20
 
20
21
  export const MAX_CHILDREN = 5;
21
22
  export const DELEGATE_TOOL = "delegate_subtasks";
@@ -95,7 +96,8 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
95
96
  const quota = options.delegationQuota ?? { used: 0 };
96
97
  const consults = canConsult(agent, config.workflow.consultExpert);
97
98
  const tools = [...new Set([...baseTools, "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : []), ...(consults ? [EXPERT_TOOL] : [])])];
98
- const mcpTools = options.mcpTools ?? agentProfile(config, agent.domain, agent.role).mcpTools ?? [];
99
+ // A caller that passes its profile's grant (even an absent one, meaning every connected tool) wins over the role's.
100
+ const mcpTools = mcpGrant("mcpTools" in options ? options.mcpTools : agentProfile(config, agent.domain, agent.role).mcpTools, !editable);
99
101
  const policy: AgentPolicy = { tools, mcpTools, readOnly: !editable, depth };
100
102
  if (depth > 1) return { tools, mcpTools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: options.env?.[EXPERT_ENV] ? "You are advising a worker: read what you need, answer its question, and change nothing." : "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
101
103
 
@@ -1,7 +1,7 @@
1
1
  import type { AgentRun } from "../schemas/findings.ts";
2
2
  import type { Domain, Role } from "../schemas/agent.ts";
3
3
  import { delegationHost } from "./delegation.ts";
4
- import { AUTO_COMPACT_ENV } from "../pi/quiet.ts";
4
+ import { AUTO_COMPACT_ENV, COMPACT_AT_ENV } from "../pi/quiet.ts";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { spawn, type ChildProcess } from "node:child_process";
7
7
  import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
@@ -13,6 +13,8 @@ import { shortDuration } from "../text.ts";
13
13
  import { readRelayRequest } from "../ask/relay.ts";
14
14
  import type { AskQuestion, AskResult } from "../ask/types.ts";
15
15
  import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
16
+ import type { HarnessRun } from "../schemas/harness.ts";
17
+ import { runHarnessAgent } from "../harness/runner.ts";
16
18
 
17
19
  /** Live control over one running subagent: queue a steering message for its next turn. */
18
20
  export interface RunHandle {
@@ -51,6 +53,8 @@ export interface PiRunOptions {
51
53
  env?: Record<string, string>;
52
54
  /** False when the agent's settings turn its auto-compaction off. */
53
55
  autoCompact?: boolean;
56
+ /** Tokens at which the agent compacts its context; absent uses the built-in threshold. */
57
+ compactAt?: number;
54
58
  /** Kill the agent after this long without any output; 0 disables. */
55
59
  stallTimeoutMs?: number;
56
60
  /** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
@@ -59,6 +63,8 @@ export interface PiRunOptions {
59
63
  onStart?: (handle: RunHandle) => void;
60
64
  /** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
61
65
  onAsk?: RelayAsk;
66
+ /** Run on this third-party harness instead of pi (`model` is then ignored for the harness's own). */
67
+ harness?: HarnessRun;
62
68
  }
63
69
 
64
70
  export interface ProcessOutcome {
@@ -402,10 +408,10 @@ export function isPiLauncher(script: string): boolean {
402
408
  return base === "pi" || base === "pi.js" || script.includes("pi-coding-agent");
403
409
  }
404
410
 
405
- const POSIX = process.platform !== "win32";
411
+ export const POSIX = process.platform !== "win32";
406
412
 
407
413
  /** Signal the child's whole process group (POSIX), so grandchildren go too. */
408
- function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
414
+ export function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
409
415
  if (proc.pid === undefined) return;
410
416
  if (POSIX) {
411
417
  try {
@@ -422,7 +428,7 @@ function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
422
428
  }
423
429
  }
424
430
 
425
- function terminate(proc: ChildProcess, graceMs = 5000): void {
431
+ export function terminate(proc: ChildProcess, graceMs = 5000): void {
426
432
  signalGroup(proc, "SIGTERM");
427
433
  const escalation = setTimeout(() => signalGroup(proc, "SIGKILL"), graceMs);
428
434
  escalation.unref();
@@ -465,7 +471,7 @@ interface RunState {
465
471
  }
466
472
 
467
473
  /** Smallest positive limit, used to pick a watchdog cadence that can see it. */
468
- function watchInterval(...limits: Array<number | undefined>): number {
474
+ export function watchInterval(...limits: Array<number | undefined>): number {
469
475
  const positive = limits.filter((limit): limit is number => typeof limit === "number" && limit > 0);
470
476
  const smallest = positive.length > 0 ? Math.min(...positive) : 4000;
471
477
  return Math.max(25, Math.min(1000, Math.floor(smallest / 4)));
@@ -651,7 +657,9 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
651
657
 
652
658
  /** Run one isolated subagent and normalise its outcome. */
653
659
  export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
654
- const options = given.autoCompact === false ? { ...given, env: { ...given.env, [AUTO_COMPACT_ENV]: "0" } } : given;
660
+ if (given.harness) return runHarnessAgent({ ...given, harness: given.harness });
661
+ const compactEnv = { ...(given.autoCompact === false ? { [AUTO_COMPACT_ENV]: "0" } : {}), ...(given.compactAt ? { [COMPACT_AT_ENV]: String(given.compactAt) } : {}) };
662
+ const options = Object.keys(compactEnv).length ? { ...given, env: { ...given.env, ...compactEnv } } : given;
655
663
  const host = await delegationHost(options, run);
656
664
  let prompt: ReturnType<typeof writePromptFile> | undefined;
657
665
  try {
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Agent Client Protocol (JSON-RPC 2.0 over stdio) for Antigravity, Gemini
3
+ * CLI, OpenCode and any other ACP agent: initialize, open a session in the
4
+ * working folder, pick the model and effort through the session's options,
5
+ * send the task with Bot Lobby's instructions in front, and map the
6
+ * `session/update` notifications onto pi's stream events. The client offers
7
+ * no file system or terminal of its own, so the agent uses its own tools,
8
+ * and its permission requests are answered from the role's allowlist.
9
+ *
10
+ * Adapted from T3 Code: the prompt wrapping from
11
+ * `packages/provider-core/src/server/orchestrationInstructions.ts`, the
12
+ * permission answers from `apps/server/src/provider/acp/AntigravityProtocol.ts`
13
+ * (`selectAntigravityPermissionOptionId`, `isAntigravityUserInputRequest`) and
14
+ * the session option categories from `packages/provider-acp/src/server/sessionConfig.ts`.
15
+ */
16
+ import { THINKING_LEVELS } from "../schemas/configuration.ts";
17
+ import type { HarnessDriver, HarnessIO } from "./process.ts";
18
+ import type { HarnessAccess, HarnessCollector, HarnessPlan } from "./runner.ts";
19
+
20
+ export const ACP_PROTOCOL_VERSION = 1;
21
+
22
+ interface RpcMessage {
23
+ jsonrpc?: string;
24
+ id?: number | string;
25
+ method?: string;
26
+ params?: Record<string, unknown>;
27
+ result?: Record<string, unknown> | null;
28
+ error?: { code?: number; message?: string };
29
+ }
30
+
31
+ interface PermissionOption {
32
+ optionId: string;
33
+ kind?: string;
34
+ name?: string;
35
+ }
36
+
37
+ interface ConfigOption {
38
+ id: string;
39
+ category?: string;
40
+ type?: string;
41
+ options?: Array<{ value?: string; name?: string; options?: Array<{ value?: string; name?: string }> }>;
42
+ }
43
+
44
+ /** pi's tool names for ACP tool kinds, for the activity line and the edit log. */
45
+ const KIND_TOOLS: Record<string, string> = {
46
+ read: "read",
47
+ edit: "edit",
48
+ delete: "write",
49
+ move: "write",
50
+ search: "grep",
51
+ execute: "bash",
52
+ fetch: "fetch_content",
53
+ think: "think",
54
+ };
55
+
56
+ /** Whether a role may run a tool of this ACP kind. */
57
+ export function acpKindAllowed(kind: string | undefined, access: HarnessAccess): boolean {
58
+ if (access.writes) return true;
59
+ switch (kind) {
60
+ case "read":
61
+ case "search":
62
+ case "think":
63
+ return true;
64
+ case "fetch":
65
+ return access.web;
66
+ case "execute":
67
+ return access.shell;
68
+ default:
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /** The option to pick for a permission request, or undefined to cancel it (a question for the user, which nobody can answer here). */
74
+ export function acpPermissionOption(request: { toolCall?: { toolCallId?: string; kind?: string }; options?: PermissionOption[] }, access: HarnessAccess): string | undefined {
75
+ if (request.toolCall?.toolCallId?.startsWith("interaction_")) return undefined;
76
+ const allow = acpKindAllowed(request.toolCall?.kind, access);
77
+ const kinds = allow ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
78
+ for (const kind of kinds) {
79
+ const option = request.options?.find((entry) => entry.kind === kind && entry.optionId.trim());
80
+ if (option) return option.optionId;
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ function choices(option: ConfigOption): Array<{ value: string; name: string }> {
86
+ return (option.options ?? []).flatMap((entry) => ("options" in entry && Array.isArray(entry.options) ? entry.options : [entry]))
87
+ .filter((entry): entry is { value: string; name: string } => typeof entry.value === "string")
88
+ .map((entry) => ({ value: entry.value, name: entry.name ?? entry.value }));
89
+ }
90
+
91
+ /** The choice whose id or name is the effort level, or the nearest one below it. */
92
+ export function acpEffortChoice(option: ConfigOption, level: string): string | undefined {
93
+ const list = choices(option);
94
+ const at = THINKING_LEVELS.indexOf(level as (typeof THINKING_LEVELS)[number]);
95
+ for (let index = at; index >= 0; index--) {
96
+ const name = THINKING_LEVELS[index]!;
97
+ const hit = list.find((choice) => choice.value.toLowerCase() === name || choice.name.toLowerCase() === name);
98
+ if (hit) return hit.value;
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ function blockText(content: unknown): string {
104
+ if (!content || typeof content !== "object") return "";
105
+ const block = content as { type?: string; text?: string };
106
+ return block.type === "text" && typeof block.text === "string" ? block.text : "";
107
+ }
108
+
109
+ function toolArgs(update: Record<string, unknown>): Record<string, unknown> {
110
+ const raw = (update.rawInput && typeof update.rawInput === "object" ? update.rawInput : {}) as Record<string, unknown>;
111
+ const locations = Array.isArray(update.locations) ? update.locations as Array<{ path?: string }> : [];
112
+ const path = locations.find((entry) => typeof entry.path === "string")?.path ?? raw.path ?? raw.file_path ?? raw.filePath;
113
+ return { ...raw, ...(typeof path === "string" ? { path } : {}), ...(typeof update.title === "string" ? { title: update.title } : {}) };
114
+ }
115
+
116
+ export function acpDriver(io: HarnessIO, plan: HarnessPlan, prompt: string, collector: HarnessCollector, access: HarnessAccess, cwd: string): HarnessDriver {
117
+ let nextId = 1;
118
+ const pending = new Map<number, (message: RpcMessage) => void>();
119
+ const tools = new Map<string, string>();
120
+ const steers: string[] = [];
121
+ let sessionId: string | undefined;
122
+ let message = "";
123
+ let thought = "";
124
+ let prompting = false;
125
+ let finished = false;
126
+
127
+ const request = (method: string, params: Record<string, unknown>, then: (message: RpcMessage) => void) => {
128
+ const id = nextId++;
129
+ pending.set(id, then);
130
+ io.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
131
+ };
132
+ const notify = (method: string, params: Record<string, unknown>) => io.write(JSON.stringify({ jsonrpc: "2.0", method, params }));
133
+ const respond = (id: number | string, result: unknown) => io.write(JSON.stringify({ jsonrpc: "2.0", id, result }));
134
+ const fail = (text: string) => {
135
+ if (finished) return;
136
+ finished = true;
137
+ collector.error = text;
138
+ io.done();
139
+ };
140
+ const flushThought = () => {
141
+ if (thought.trim()) io.emit({ type: "thought", text: thought.trim().slice(0, 1500) });
142
+ thought = "";
143
+ };
144
+ const send = (text: string) => {
145
+ prompting = true;
146
+ message = "";
147
+ io.emit({ type: "turn_start" });
148
+ request("session/prompt", { sessionId, prompt: [{ type: "text", text }] }, (reply) => {
149
+ prompting = false;
150
+ flushThought();
151
+ if (message.trim()) collector.text = message;
152
+ if (reply.error) return fail(reply.error.message ?? "the agent refused the prompt");
153
+ const usage = reply.result?.usage as { inputTokens?: number; outputTokens?: number } | undefined;
154
+ if (usage) {
155
+ collector.addUsage(usage.inputTokens ?? 0, usage.outputTokens ?? 0, 0, plan.model || undefined);
156
+ io.emit({ type: "usage", input: usage.inputTokens ?? 0, output: usage.outputTokens ?? 0, cost: 0, ...(plan.model ? { model: plan.model } : {}) });
157
+ }
158
+ const stop = String(reply.result?.stopReason ?? "end_turn");
159
+ if (stop === "cancelled") collector.stopReason = "aborted";
160
+ else if (stop === "refusal") collector.error = "the agent refused the task";
161
+ // A steering message sent mid-turn goes in as the next prompt.
162
+ if (stop !== "cancelled" && steers.length > 0) return send(steers.splice(0).join("\n\n"));
163
+ finished = true;
164
+ io.done();
165
+ });
166
+ };
167
+ const configure = (result: Record<string, unknown>, then: () => void) => {
168
+ const steps: Array<(next: () => void) => void> = [];
169
+ const options = Array.isArray(result.configOptions) ? result.configOptions as ConfigOption[] : [];
170
+ const modelOption = options.find((option) => option.category === "model");
171
+ const effortOption = options.find((option) => option.category === "thought_level");
172
+ if (plan.model && modelOption) steps.push((next) => request("session/set_config_option", { sessionId, configId: modelOption.id, value: plan.model }, next));
173
+ else if (plan.model && result.models) steps.push((next) => request("session/set_model", { sessionId, modelId: plan.model }, next));
174
+ const effort = effortOption ? acpEffortChoice(effortOption, plan.level) : undefined;
175
+ if (effortOption && effort) steps.push((next) => request("session/set_config_option", { sessionId, configId: effortOption.id, value: effort }, next));
176
+ const run = () => {
177
+ const step = steps.shift();
178
+ if (step) step(() => run());
179
+ else then();
180
+ };
181
+ run();
182
+ };
183
+ const onUpdate = (update: Record<string, unknown>) => {
184
+ switch (update.sessionUpdate) {
185
+ case "agent_message_chunk": {
186
+ const text = blockText(update.content);
187
+ if (!text) return;
188
+ flushThought();
189
+ if (!message) io.emit({ type: "writing" });
190
+ message += text;
191
+ return;
192
+ }
193
+ case "agent_thought_chunk": {
194
+ const text = blockText(update.content);
195
+ if (!text) return;
196
+ if (!thought) io.emit({ type: "thinking" });
197
+ thought += text;
198
+ return;
199
+ }
200
+ case "tool_call": {
201
+ flushThought();
202
+ if (message.trim()) collector.text = message;
203
+ message = "";
204
+ const id = String(update.toolCallId ?? "");
205
+ const name = KIND_TOOLS[String(update.kind ?? "")] ?? "tool";
206
+ if (id && !tools.has(id)) {
207
+ tools.set(id, name);
208
+ io.emit({ type: "tool_execution_start", toolName: name, args: toolArgs(update) });
209
+ }
210
+ if (update.status === "completed" || update.status === "failed") onUpdate({ ...update, sessionUpdate: "tool_call_update" });
211
+ return;
212
+ }
213
+ case "tool_call_update": {
214
+ const id = String(update.toolCallId ?? "");
215
+ const name = tools.get(id);
216
+ if (!name || (update.status !== "completed" && update.status !== "failed")) return;
217
+ tools.delete(id);
218
+ io.emit({ type: "tool_execution_end", toolName: name, isError: update.status === "failed" });
219
+ return;
220
+ }
221
+ case "usage_update": {
222
+ const cost = (update.cost as { amount?: number } | undefined)?.amount;
223
+ if (typeof cost === "number" && cost > collector.usage.cost) collector.usage.cost = cost;
224
+ return;
225
+ }
226
+ }
227
+ };
228
+ const onRequest = (message: RpcMessage) => {
229
+ if (message.id === undefined) return;
230
+ if (message.method === "session/request_permission") {
231
+ const optionId = acpPermissionOption((message.params ?? {}) as Parameters<typeof acpPermissionOption>[0], access);
232
+ respond(message.id, { outcome: optionId ? { outcome: "selected", optionId } : { outcome: "cancelled" } });
233
+ return;
234
+ }
235
+ // No file system or terminal was offered; anything else is unknown.
236
+ io.write(JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } }));
237
+ };
238
+ return {
239
+ start() {
240
+ request("initialize", { protocolVersion: ACP_PROTOCOL_VERSION, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }, clientInfo: { name: "bot-lobby", version: "1" } }, (reply) => {
241
+ if (reply.error) return fail(`the agent did not start: ${reply.error.message ?? "initialize failed"}`);
242
+ request("session/new", { cwd, mcpServers: [] }, (created) => {
243
+ if (created.error || typeof created.result?.sessionId !== "string") return fail(`the agent did not open a session: ${created.error?.message ?? "no session id"}${/auth|key|sign|log ?in/i.test(created.error?.message ?? "") ? " (sign in to it once in a terminal)" : ""}`);
244
+ sessionId = created.result.sessionId;
245
+ collector.session = sessionId;
246
+ configure(created.result, () => send(prompt));
247
+ });
248
+ });
249
+ },
250
+ steer(text) {
251
+ if (prompting) steers.push(text);
252
+ },
253
+ abort() {
254
+ if (sessionId) notify("session/cancel", { sessionId });
255
+ },
256
+ line(text) {
257
+ let message: RpcMessage;
258
+ try {
259
+ message = JSON.parse(text) as RpcMessage;
260
+ } catch {
261
+ return;
262
+ }
263
+ if (message.method === "session/update") return onUpdate((message.params?.update ?? {}) as Record<string, unknown>);
264
+ if (message.method) return onRequest(message);
265
+ if (typeof message.id === "number") {
266
+ const then = pending.get(message.id);
267
+ pending.delete(message.id);
268
+ then?.(message);
269
+ }
270
+ },
271
+ };
272
+ }