@a-t-h-i/bot-lobby 0.6.57 → 0.6.59
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 +17 -11
- package/THIRD_PARTY_NOTICES.md +17 -3
- package/package.json +6 -1
- package/prompts/lobby-chat.md +39 -0
- package/prompts/master.md +7 -7
- package/prompts/reviewer.md +6 -6
- package/src/classifier/instance.ts +1 -1
- package/src/classifier/lobby.ts +37 -0
- package/src/classifier/triage.ts +2 -24
- package/src/execution/delegation.ts +2 -2
- package/src/index.ts +0 -3
- package/src/lobby/chat-agents.ts +293 -0
- package/src/lobby/feed.ts +73 -17
- package/src/lobby/host.ts +8 -4
- package/src/lobby/keys.ts +1 -0
- package/src/lobby/planner.ts +5 -5
- package/src/lobby/pr-review.ts +2 -2
- package/src/lobby/profile.ts +16 -0
- package/src/lobby/prompts.ts +56 -4
- package/src/lobby/recovery.ts +10 -42
- package/src/lobby/runtime.ts +27 -45
- package/src/lobby/service.ts +142 -13
- package/src/lobby/session-files.ts +4 -1
- package/src/lobby/topics.ts +0 -1
- package/src/master/master.ts +1 -1
- package/src/pi/commands.ts +6 -8
- package/src/pi/events.ts +11 -3
- package/src/pi/fresh-context.ts +1 -1
- package/src/pi/model-settings.ts +0 -2
- package/src/pi/model-support.ts +11 -2
- package/src/pi/owner.ts +35 -4
- package/src/pi/start-task.ts +3 -9
- package/src/pi/switch.ts +1 -1
- package/src/schemas/configuration.ts +14 -24
- package/src/state/changes.ts +25 -23
- package/src/state/metrics.ts +5 -8
- package/src/state/recovery.ts +3 -3
- package/src/webui/api/index.ts +12 -18
- package/src/webui/api/lobby.ts +9 -6
- package/src/webui/code.ts +202 -0
- package/src/webui/dev/fake-service.ts +28 -62
- package/src/webui/dev/fixtures.ts +1 -4
- package/src/webui/protocol.ts +15 -9
- package/src/webui/static.ts +5 -2
- package/src/workflow/track.ts +1 -52
- package/src/workflow/workflow.ts +2 -2
- package/webui/dist/assets/CodeSurface-BtzJOVyH.js +448 -0
- package/webui/dist/assets/{TerminalView-C8VC7KXy.js → TerminalView-Cz-KNWsq.js} +2 -2
- package/webui/dist/assets/astro-p42oEArs.js +1 -0
- package/webui/dist/assets/c-DaO8xguW.js +1 -0
- package/webui/dist/assets/ccount-DQxSH8e6.js +1 -0
- package/webui/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/webui/dist/assets/css-C8fQr_0Q.js +1 -0
- package/webui/dist/assets/diff-woXpYk--.js +1 -0
- package/webui/dist/assets/dockerfile-IyjqRm3v.js +1 -0
- package/webui/dist/assets/dotenv-CuH5my32.js +1 -0
- package/webui/dist/assets/fish-BJitypiv.js +1 -0
- package/webui/dist/assets/go-rLFTqkRN.js +1 -0
- package/webui/dist/assets/graphql-AVu4aKQX.js +1 -0
- package/webui/dist/assets/html-wSbaAAK3.js +1 -0
- package/webui/dist/assets/index-D5fvY8f8.js +112 -0
- package/webui/dist/assets/index-DFvk8ADr.css +2 -0
- package/webui/dist/assets/ini-B5eOa1yu.js +1 -0
- package/webui/dist/assets/java-DdHNQ9hk.js +1 -0
- package/webui/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/webui/dist/assets/json-qhed-kSA.js +1 -0
- package/webui/dist/assets/json5-BR5RXkoi.js +1 -0
- package/webui/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/webui/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/webui/dist/assets/jsx-gTgmArGx.js +1 -0
- package/webui/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/webui/dist/assets/kts-DFAlwc6n.js +1 -0
- package/webui/dist/assets/less-DVTAwKKz.js +1 -0
- package/webui/dist/assets/lua-xZcbXUE9.js +1 -0
- package/webui/dist/assets/makefile-Dixweg8N.js +1 -0
- package/webui/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/webui/dist/assets/php-C6PDnbTe.js +1 -0
- package/webui/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/webui/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/webui/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/webui/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/webui/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/webui/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/webui/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/webui/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/webui/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/webui/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/webui/dist/assets/postcss-D_Wz94rJ.js +1 -0
- package/webui/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/webui/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/webui/dist/assets/properties-CxAuzHS9.js +1 -0
- package/webui/dist/assets/python-gzcpVVnB.js +1 -0
- package/webui/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/webui/dist/assets/ruby-CPL7DL0w.js +1 -0
- package/webui/dist/assets/rust-DpiIf5JO.js +1 -0
- package/webui/dist/assets/sass-DXrisJhu.js +1 -0
- package/webui/dist/assets/scss-CHVQqdPx.js +1 -0
- package/webui/dist/assets/shellscript-CLZ0U2zV.js +1 -0
- package/webui/dist/assets/sql-CEbI0hMM.js +1 -0
- package/webui/dist/assets/svelte-DKhxH1PN.js +1 -0
- package/webui/dist/assets/swift-CyEgAFGc.js +1 -0
- package/webui/dist/assets/toml-CcmNWLt0.js +1 -0
- package/webui/dist/assets/tsx-CUOciP0b.js +1 -0
- package/webui/dist/assets/typescript-BydbNFcO.js +1 -0
- package/webui/dist/assets/vue-kBwcnTZb.js +1 -0
- package/webui/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/webui/dist/assets/xml-CeXJrFlG.js +1 -0
- package/webui/dist/assets/yaml-rwi0_p6S.js +1 -0
- package/webui/dist/assets/yml-s9iVJXCw.js +1 -0
- package/webui/dist/assets/zsh-B7CDPTg5.js +1 -0
- package/webui/dist/build.json +61 -45
- package/webui/dist/index.html +4 -2
- package/prompts/quickfix.md +0 -67
- package/src/lobby/quickfix.ts +0 -546
- package/src/pi/route.ts +0 -180
- package/src/webui/api/quickfix.ts +0 -49
- package/webui/dist/assets/index-Bs1_77_1.js +0 -114
- package/webui/dist/assets/index-DOLQ4FDd.css +0 -2
package/src/lobby/runtime.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The lobby inside pi: one runtime per master session. It owns the lobby's
|
|
3
|
-
* state (the
|
|
3
|
+
* state (the chat agents, planner, issues, pulls, knowledge), narrates the
|
|
4
4
|
* Master's own turn into the feed, passes plan comments to the Master that
|
|
5
5
|
* owns a task and records each Master turn in the metrics log. The web server
|
|
6
6
|
* (src/webui) is its only surface.
|
|
@@ -15,19 +15,17 @@ import { basename } from "node:path";
|
|
|
15
15
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
16
16
|
import { modelRef } from "../pi/model-support.ts";
|
|
17
17
|
import { modelLookup } from "../pi/tools.ts";
|
|
18
|
-
import { setQuickFixHandoff } from "../pi/route.ts";
|
|
19
|
-
import type { Domain } from "../schemas/agent.ts";
|
|
20
18
|
import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike } from "./feed.ts";
|
|
21
19
|
import { classifier, effortFor, hintsFor, scriptsPickerFor } from "../classifier/instance.ts";
|
|
22
20
|
import { checkThinking } from "../pi/model-support.ts";
|
|
23
|
-
import { QuickFixQueue } from "./quickfix.ts";
|
|
24
21
|
import type { Asker } from "../ask/types.ts";
|
|
22
|
+
import { ChatAgents } from "./chat-agents.ts";
|
|
25
23
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
26
24
|
import { PullsState } from "./pulls.ts";
|
|
27
25
|
import { PullReviews } from "./pr-review.ts";
|
|
28
26
|
import { KnowledgeBook } from "./knowledge.ts";
|
|
29
27
|
import type { LobbyService } from "./host.ts";
|
|
30
|
-
import { backgroundSessions, carryHandOff, createLobbyService, lobbyNotify, lobbyProfile, plannerDeps, resumeSession, reviewProfile, seatProfile, setServiceState, savePlan as serviceSavePlan, answerPanel as serviceAnswerPanel } from "./service.ts";
|
|
28
|
+
import { backgroundSessions, carryHandOff, chatProfile, chatReplied, chatHistoryLines, createLobbyService, lobbyNotify, lobbyProfile, plannerDeps, resumeSession, reviewProfile, seatProfile, setServiceState, savePlan as serviceSavePlan, answerPanel as serviceAnswerPanel } from "./service.ts";
|
|
31
29
|
import { lobbyTopics } from "./topics.ts";
|
|
32
30
|
export { backgroundSessions, setSessionLauncher } from "./service.ts";
|
|
33
31
|
import { deliverComments, onOwnerEvent } from "../pi/owner.ts";
|
|
@@ -42,7 +40,8 @@ export interface Runtime {
|
|
|
42
40
|
configDir: string;
|
|
43
41
|
ctx: ExtensionContext;
|
|
44
42
|
root: string;
|
|
45
|
-
|
|
43
|
+
/** The lobby chat's agents other than the oracle, answering one at a time. */
|
|
44
|
+
chat: ChatAgents;
|
|
46
45
|
planner?: PlanningSession;
|
|
47
46
|
issues: IssuesState;
|
|
48
47
|
pulls: PullsState;
|
|
@@ -80,11 +79,11 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
80
79
|
return serviceAnswerPanel(state);
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
/** What the lobby is running now (a
|
|
82
|
+
/** What the lobby is running now (a chat agent's reply, a review, a planning round), in words, or undefined. */
|
|
84
83
|
export function lobbyWorkRunning(): string | undefined {
|
|
85
84
|
const state = runtime;
|
|
86
85
|
if (!state) return undefined;
|
|
87
|
-
if (state.
|
|
86
|
+
if (state.chat.running) return "A chat reply";
|
|
88
87
|
if (state.planner?.busy) return "A planning round";
|
|
89
88
|
if (state.reviews.busy) return "A pull request review";
|
|
90
89
|
return undefined;
|
|
@@ -107,32 +106,13 @@ function shutdown(carryOn: boolean): void {
|
|
|
107
106
|
runtime = undefined;
|
|
108
107
|
lobbyService = undefined;
|
|
109
108
|
setServiceState(undefined);
|
|
110
|
-
setQuickFixHandoff(undefined);
|
|
111
109
|
state.unsubscribeFeed?.();
|
|
112
|
-
state.
|
|
110
|
+
state.chat.cancelAll();
|
|
113
111
|
state.reviews.cancelAll();
|
|
114
112
|
state.planner?.cancel();
|
|
115
113
|
onRunUpdates(undefined);
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
/**
|
|
119
|
-
* The oracle routed a request to the quick-fix agent: queue it (no "looks like
|
|
120
|
-
* a task" hold, the oracle already decided), on its builder's settings when it
|
|
121
|
-
* is a quick feature, and tell the page where it went.
|
|
122
|
-
*/
|
|
123
|
-
function handToQuickFix(state: Runtime, request: string, builder: Domain | undefined, reason: string): string {
|
|
124
|
-
const quick = lobbyProfile(state, "quickfix");
|
|
125
|
-
const profile = builder ? { ...seatProfile(state, builder), instructions: quick.instructions } : undefined;
|
|
126
|
-
const job = state.quickfix.submit(request, Date.now(), {
|
|
127
|
-
force: true,
|
|
128
|
-
...(profile ? { profile } : {}),
|
|
129
|
-
note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and grants.` : ""}`,
|
|
130
|
-
});
|
|
131
|
-
lobbyTopics.bump("quickfix");
|
|
132
|
-
pushNotice("The oracle sent your request to the quick-fix agent.", "info");
|
|
133
|
-
return job.id;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
116
|
/**
|
|
137
117
|
* Start the lobby's backend: state, service and background wiring. Runs for
|
|
138
118
|
* every interactive master session, not a subagent.
|
|
@@ -150,10 +130,10 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
|
|
|
150
130
|
asking: false,
|
|
151
131
|
workspace: { name: basename(ctx.cwd) || ctx.cwd },
|
|
152
132
|
readingWorkspace: false,
|
|
153
|
-
quickfix: undefined as unknown as QuickFixQueue,
|
|
154
133
|
issues: new IssuesState(execCommand, ctx.cwd, () => lobbyTopics.bump("issues")),
|
|
155
134
|
pulls: new PullsState(execCommand, ctx.cwd, () => lobbyTopics.bump("git")),
|
|
156
135
|
reviews: undefined as unknown as PullReviews,
|
|
136
|
+
chat: undefined as unknown as ChatAgents,
|
|
157
137
|
knowledge: new KnowledgeBook({
|
|
158
138
|
root,
|
|
159
139
|
configDir,
|
|
@@ -175,34 +155,30 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
|
|
|
175
155
|
notify: (message, level) => lobbyNotify(state, message, level),
|
|
176
156
|
classifier: classifier(),
|
|
177
157
|
});
|
|
178
|
-
state.
|
|
158
|
+
state.chat = new ChatAgents({
|
|
179
159
|
cwd: ctx.cwd,
|
|
180
160
|
root,
|
|
181
161
|
configDir,
|
|
182
|
-
profile: () =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
feed: lobbyFeed,
|
|
162
|
+
profile: (agent) => chatProfile(state, agent),
|
|
163
|
+
history: chatHistoryLines,
|
|
164
|
+
onReply: (job) => chatReplied(state, job),
|
|
186
165
|
onChange: () => {
|
|
187
|
-
lobbyTopics.bump("
|
|
188
|
-
|
|
166
|
+
lobbyTopics.bump("lobby");
|
|
167
|
+
lobbyTopics.bump("status");
|
|
189
168
|
},
|
|
190
|
-
|
|
169
|
+
feed: lobbyFeed,
|
|
191
170
|
hints: hintsFor({ cwd: ctx.cwd, root, configDir }),
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
195
|
-
lint: () => loadConfig().lint,
|
|
171
|
+
stallTimeoutMs: workflow.stallTimeoutMs,
|
|
172
|
+
toolStallTimeoutMs: workflow.toolStallTimeoutMs,
|
|
196
173
|
});
|
|
197
174
|
runtime = state;
|
|
198
175
|
lobbyService = createLobbyService(state);
|
|
199
176
|
setServiceState(state);
|
|
200
177
|
// Topic routing (src/lobby/topics.ts): sources bump a topic when their data
|
|
201
178
|
// changes and the web page rereads it. lobbyFeed (lobby), run updates (tasks),
|
|
202
|
-
//
|
|
179
|
+
// chat agents (lobby), planner (planner), background sessions (sessions),
|
|
203
180
|
// metrics (metrics), pulls/reviews (git), issues (issues), the prompt hub
|
|
204
181
|
// (prompts), owner/notify events (notices), busy/workspace (status).
|
|
205
|
-
setQuickFixHandoff((request, builder, reason) => (runtime === state ? handToQuickFix(state, request, builder, reason) : undefined));
|
|
206
182
|
lobbyFeed.clear();
|
|
207
183
|
// Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
|
|
208
184
|
lobbyFeed.seedChat(chatFromEntries(ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY));
|
|
@@ -219,6 +195,7 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
|
|
|
219
195
|
if (event.kind === "comments") lobbyFeed.log("LOBBY", `passed ${event.count} plan comment${event.count === 1 ? "" : "s"} on ${event.taskId} to the oracle`, "info");
|
|
220
196
|
else if (event.kind === "inbox" || event.kind === "messages") lobbyFeed.log("LOBBY", `passed ${event.count} message${event.count === 1 ? "" : "s"} from another session to the oracle`, "info");
|
|
221
197
|
else if (event.kind === "auto") lobbyFeed.log("LOBBY", `auto mode ${event.on ? "on" : "off"} for ${event.taskId}`, event.on ? "success" : "info");
|
|
198
|
+
else if (event.kind === "held") lobbyFeed.log("LOBBY", `auto mode: you stopped the oracle on ${event.taskId}; it waits for your next message`, "info");
|
|
222
199
|
else if (event.kind === "nudge") lobbyFeed.log("LOBBY", `auto mode: keeping the oracle going on ${event.taskId}`, "info");
|
|
223
200
|
else lobbyFeed.log("LOBBY", `auto mode: no progress on ${event.taskId} — it needs you`, "warning");
|
|
224
201
|
});
|
|
@@ -239,7 +216,6 @@ function carryOn(state: Runtime): void {
|
|
|
239
216
|
sessionFile: () => state.ctx.sessionManager.getSessionFile?.(),
|
|
240
217
|
working: () => !state.ctx.isIdle(),
|
|
241
218
|
planner: () => state.planner,
|
|
242
|
-
quickfix: () => state.quickfix.snapshot(),
|
|
243
219
|
background: () => backgroundSessions(),
|
|
244
220
|
});
|
|
245
221
|
let summary: string | undefined;
|
|
@@ -250,7 +226,6 @@ function carryOn(state: Runtime): void {
|
|
|
250
226
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
251
227
|
relaunch: (start) => resumeSession(state, start),
|
|
252
228
|
restorePlanner: (snapshot) => restorePlanner(state, snapshot),
|
|
253
|
-
restoreQuickFixes: (jobs) => state.quickfix.restore(jobs).length,
|
|
254
229
|
nudge: (message) => nudgeWhenIdle(state, message),
|
|
255
230
|
});
|
|
256
231
|
} catch (error) {
|
|
@@ -359,6 +334,9 @@ export function registerLobbyEvents(pi: ExtensionAPI, configDir: string): void {
|
|
|
359
334
|
const finished = turn;
|
|
360
335
|
turn = undefined;
|
|
361
336
|
lobbyFeed.replyEnd();
|
|
337
|
+
// A tool call cut off by a stop or a steer never ends on its own; the turn's end closes it.
|
|
338
|
+
lobbyFeed.endAll("MASTER");
|
|
339
|
+
lobbyTopics.bump("status");
|
|
362
340
|
const taskId = finished?.taskId ?? currentZenTask()?.id;
|
|
363
341
|
if (!finished || !taskId || !runtime) return;
|
|
364
342
|
appendMetrics(runtime.root, configDir, [{
|
|
@@ -378,6 +356,10 @@ export function registerLobbyEvents(pi: ExtensionAPI, configDir: string): void {
|
|
|
378
356
|
taskId,
|
|
379
357
|
}]);
|
|
380
358
|
lobbyTopics.bump("metrics");
|
|
359
|
+
});
|
|
360
|
+
// pi is idle only once the run has settled (after agent_end, retries and continuations): the page's busy flag rereads then.
|
|
361
|
+
pi.on("agent_settled", () => {
|
|
362
|
+
lobbyFeed.endAll("MASTER");
|
|
381
363
|
lobbyTopics.bump("status");
|
|
382
364
|
});
|
|
383
365
|
}
|
package/src/lobby/service.ts
CHANGED
|
@@ -29,12 +29,14 @@ import { applyStatus, currentZenTask, taskSnapshot } from "../pi/ui.ts";
|
|
|
29
29
|
import { taskName } from "../text.ts";
|
|
30
30
|
import { describeWorkspace } from "../execution/workspace.ts";
|
|
31
31
|
import { stripStartFlags } from "../pi/start-flags.ts";
|
|
32
|
-
import { checkThinking, modelRef, resolveLobbyProfile, resolvePanelProfile, resolveReviewProfile, supportedThinking } from "../pi/model-support.ts";
|
|
32
|
+
import { checkThinking, modelRef, resolveChatProfile, resolveLobbyProfile, resolvePanelProfile, resolveReviewProfile, supportedThinking } from "../pi/model-support.ts";
|
|
33
33
|
import { modelLookup } from "../pi/tools.ts";
|
|
34
|
-
import { startPlannedTask } from "../pi/start-task.ts";
|
|
35
|
-
import { pendingRequest, startRequest } from "../pi/route.ts";
|
|
34
|
+
import { startPlannedTask, startTask } from "../pi/start-task.ts";
|
|
36
35
|
import { deliverComments, setAuto } from "../pi/owner.ts";
|
|
37
|
-
import { chatFromEntries, lobbyFeed, type ChatEntry } from "./feed.ts";
|
|
36
|
+
import { CHAT_MARK, chatFromEntries, chatMarkContent, CLEARED_NOTE, lobbyFeed, type ChatEntry, type ChatMarkDetails } from "./feed.ts";
|
|
37
|
+
import { CHAT_AGENT_SOURCES, parseLobbyCommand, parseMention, type LobbyCommand } from "./prompts.ts";
|
|
38
|
+
import type { ChatJob, ChatWorker } from "./chat-agents.ts";
|
|
39
|
+
import { routeChatMessage } from "../classifier/lobby.ts";
|
|
38
40
|
import { availableClassifiers, classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
39
41
|
import { cleanSessionName, launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
40
42
|
import { askForSession } from "./session-questions.ts";
|
|
@@ -147,23 +149,144 @@ function failed(state: Runtime, what: string, error: Error): void {
|
|
|
147
149
|
lobbyNotify(state, `bot-lobby: ${what} — ${error.message}`, "error");
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
/** What sending to the lobby chat did: nothing to say, a notice, or a notice and the tab to open. */
|
|
153
|
+
export type LobbySendResult = string | { notice: string; open: "plan" } | undefined;
|
|
154
|
+
|
|
155
|
+
function toOracle(state: Runtime, text: string): LobbySendResult {
|
|
151
156
|
const busy = !state.ctx.isIdle();
|
|
157
|
+
const command = parseLobbyCommand(text);
|
|
158
|
+
if (command) return lobbyCommand(state, command.command, command.rest);
|
|
152
159
|
if (text.startsWith("/")) {
|
|
153
160
|
state.pi.sendUserMessage(text, { expandPromptTemplates: true, ...(busy ? { deliverAs: "followUp" as const } : {}) });
|
|
154
161
|
return `sent ${text.split(/\s+/)[0]} to pi`;
|
|
155
162
|
}
|
|
156
|
-
//
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// A task in this session: the oracle drives it, and what you say goes to it.
|
|
164
|
+
if (currentZenTask()) return toMaster(state, text);
|
|
165
|
+
const mention = parseMention(text);
|
|
166
|
+
if (mention?.agent === "master") return toMaster(state, mention.rest.trim() || text);
|
|
167
|
+
if (mention) {
|
|
168
|
+
const body = mention.rest.trim();
|
|
169
|
+
if (!body) return `say what you want ${CHAT_AGENT_SOURCES[mention.agent]} to do after @${mention.agent === "designer" ? "design" : mention.agent === "backend" ? "dev" : mention.agent === "researcher" ? "research" : mention.agent}`;
|
|
170
|
+
toAgent(state, mention.agent, body, `you named ${CHAT_AGENT_SOURCES[mention.agent]}`);
|
|
171
|
+
return undefined;
|
|
160
172
|
}
|
|
173
|
+
// While the oracle is mid-turn, what you say steers it, as it always has.
|
|
174
|
+
if (busy) return toMaster(state, text);
|
|
175
|
+
lobbyFeed.sayQueued(text);
|
|
176
|
+
void routeChat(state, text).catch((error: Error) => failed(state, "could not pass your message on", error));
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The oracle answers: a pi turn, or a steer while it is mid-turn. */
|
|
181
|
+
function toMaster(state: Runtime, text: string): undefined {
|
|
182
|
+
const busy = !state.ctx.isIdle();
|
|
161
183
|
state.pi.sendUserMessage(text, busy ? { deliverAs: "steer" } : undefined);
|
|
162
184
|
// A steer reaches the oracle only when its turn lets it in; the conversation shows it now.
|
|
163
185
|
if (busy) lobbyFeed.sayQueued(text);
|
|
164
186
|
return undefined;
|
|
165
187
|
}
|
|
166
188
|
|
|
189
|
+
/** One line of the conversation as an agent reads it. */
|
|
190
|
+
function chatLine(entry: ChatEntry): string {
|
|
191
|
+
if (entry.role === "you") return `You${entry.to ? ` (to ${entry.to})` : ""}: ${entry.text}`;
|
|
192
|
+
if (entry.role === "note") return `(${entry.text})`;
|
|
193
|
+
return `${entry.agent ?? "MASTER"}: ${entry.text}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The conversation since the oracle's context was last cleared, as lines. */
|
|
197
|
+
export function chatHistoryLines(): string[] {
|
|
198
|
+
const chat = lobbyFeed.chat;
|
|
199
|
+
const cleared = chat.map((entry) => entry.text).lastIndexOf(CLEARED_NOTE);
|
|
200
|
+
return chat.slice(cleared + 1).map(chatLine);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Who answers a message nobody was named in: the classifier's pick when it is sure, else the Master. */
|
|
204
|
+
async function routeChat(state: Runtime, text: string): Promise<void> {
|
|
205
|
+
const jev = classifier();
|
|
206
|
+
const lines = chatHistoryLines();
|
|
207
|
+
const picked = jev ? await routeChatMessage(jev, text, lines.slice(-7, -1), { at: loadConfig().classifier.thresholds.lobbyAgentAt }).catch(() => undefined) : undefined;
|
|
208
|
+
if (state !== serviceState) return;
|
|
209
|
+
if (!picked || picked.route === "master") {
|
|
210
|
+
toMaster(state, text);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (picked.route === "knowledge") {
|
|
214
|
+
lobbyFeed.log("LOBBY", `a knowledge question (${picked.probability.toFixed(2)}): the oracle reads the project's knowledge`, "info");
|
|
215
|
+
state.pi.sendUserMessage(knowledgeQuestion(text, readDataRoots(state.root, state.configDir)), state.ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
toAgent(state, picked.route, text, `the classifier picked ${CHAT_AGENT_SOURCES[picked.route]} (${picked.probability.toFixed(2)})`, true);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** An agent other than the oracle answers: the message joins the conversation (and the session) under its name, and its reply follows. */
|
|
222
|
+
function toAgent(state: Runtime, agent: ChatWorker, text: string, why: string, shown = false): void {
|
|
223
|
+
const source = CHAT_AGENT_SOURCES[agent];
|
|
224
|
+
if (shown) {
|
|
225
|
+
lobbyFeed.takeQueued(text);
|
|
226
|
+
lobbyFeed.addressed(text, source);
|
|
227
|
+
} else lobbyFeed.say("you", text, Date.now(), { to: source });
|
|
228
|
+
keepChatLine(state, { role: "you", agent: source, text });
|
|
229
|
+
lobbyFeed.log("LOBBY", `${source} answers: ${why}`, "info");
|
|
230
|
+
state.chat.submit(agent, text, why);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Keep a line the oracle did not take part in, in its session: it reads it as context, and the conversation shows it again later. */
|
|
234
|
+
function keepChatLine(state: Runtime, details: ChatMarkDetails): void {
|
|
235
|
+
try {
|
|
236
|
+
state.pi.sendMessage({ customType: CHAT_MARK, content: chatMarkContent(details), display: true, details }, { triggerTurn: false });
|
|
237
|
+
} catch {
|
|
238
|
+
// A session that cannot take it keeps the conversation in the page alone.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A chat agent finished: its reply joins the conversation and the session. */
|
|
243
|
+
export function chatReplied(state: Runtime, job: ChatJob): void {
|
|
244
|
+
const source = CHAT_AGENT_SOURCES[job.agent];
|
|
245
|
+
const text = job.reply ?? (job.status === "success" ? "(no reply)" : "");
|
|
246
|
+
if (text) {
|
|
247
|
+
lobbyFeed.say("oracle", text, job.finishedAt ?? Date.now(), { agent: source });
|
|
248
|
+
keepChatLine(state, { role: "agent", agent: source, text });
|
|
249
|
+
}
|
|
250
|
+
if (job.status !== "success" && job.status !== "cancelled") lobbyFeed.say("note", `✗ ${source}'s reply ${job.status}${job.error ? `: ${job.error.split("\n")[0]}` : ""}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The model, thinking and grants a chat agent answers with. */
|
|
254
|
+
export function chatProfile(state: Runtime, agent: ChatWorker) {
|
|
255
|
+
return resolveChatProfile(loadConfig(), agent, {
|
|
256
|
+
lookup: modelLookup(state.ctx),
|
|
257
|
+
sessionModel: sessionModel(state.ctx),
|
|
258
|
+
warn: (message) => lobbyNotify(state, message, "warning"),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The work a bare `/plan` or `/task` is about: the conversation so far. */
|
|
263
|
+
function fromConversation(): string | undefined {
|
|
264
|
+
const lines = chatHistoryLines().filter((line) => !line.startsWith("("));
|
|
265
|
+
if (!lines.some((line) => line.startsWith("You"))) return undefined;
|
|
266
|
+
return `From the lobby chat:\n\n${lines.slice(-12).join("\n\n")}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** `/plan` and `/task`: plan the work with the panel on the Plan tab, or start it as a task. */
|
|
270
|
+
function lobbyCommand(state: Runtime, command: LobbyCommand, rest: string): LobbySendResult {
|
|
271
|
+
const request = rest.trim() || fromConversation();
|
|
272
|
+
if (!request) return `type what to ${command === "plan" ? "plan" : "do"} after /${command}`;
|
|
273
|
+
if (command === "task") {
|
|
274
|
+
const current = currentZenTask();
|
|
275
|
+
if (current) return `this session already drives ${current.id}; finish or cancel it first, or start the task from another session`;
|
|
276
|
+
startTask(state.pi, state.ctx, state.configDir, request)
|
|
277
|
+
.then(() => lobbyTopics.bump("tasks"))
|
|
278
|
+
.catch((error: Error) => failed(state, "could not start the task", error));
|
|
279
|
+
return "starting a task — the oracle takes it from here";
|
|
280
|
+
}
|
|
281
|
+
if (state.planner?.busy) return "the panel is still thinking — wait for the round to finish";
|
|
282
|
+
// A plan under way stays among the project's previous plans; this one starts afresh.
|
|
283
|
+
const fresh = state.planner && state.planner.messages.length > 0;
|
|
284
|
+
const session = fresh || !state.planner ? newPlanner(state, undefined, loadConfig().lobby.planningPanel) : state.planner;
|
|
285
|
+
session.send(request).catch((error: Error) => failed(state, "could not start planning", error));
|
|
286
|
+
lobbyTopics.bump("planner");
|
|
287
|
+
return { notice: fresh ? "planning on the Plan tab — your earlier plan is under Previous plans" : "planning on the Plan tab", open: "plan" };
|
|
288
|
+
}
|
|
289
|
+
|
|
167
290
|
function startPlanned(state: Runtime, plan: PlannedTask): string {
|
|
168
291
|
const current = currentZenTask();
|
|
169
292
|
if (current) return `this session already drives ${current.id}; finish or cancel it first, or start ${plan.id} from another session`;
|
|
@@ -580,7 +703,7 @@ function liveSessions(state: Runtime): LiveSession[] {
|
|
|
580
703
|
*/
|
|
581
704
|
function chatHistory(state: Runtime, sessionId?: string): ChatEntry[] {
|
|
582
705
|
if (sessionId) return chats.history(sessionId);
|
|
583
|
-
return chatFromEntries(state.ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY).map((line, index) => ({ id: -(index + 1), at: line.at ?? 0
|
|
706
|
+
return chatFromEntries(state.ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY).map((line, index) => ({ ...line, id: -(index + 1), at: line.at ?? 0 }));
|
|
584
707
|
}
|
|
585
708
|
|
|
586
709
|
/** Leave a message for a session running in another terminal. */
|
|
@@ -914,7 +1037,11 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
914
1037
|
return { ...(snapshot.task ? { task: snapshot.task } : {}), runs: snapshot.runs };
|
|
915
1038
|
},
|
|
916
1039
|
feed: lobbyFeed,
|
|
917
|
-
masterBusy: () => !state.ctx.isIdle(),
|
|
1040
|
+
masterBusy: () => !state.ctx.isIdle() || state.chat.busy,
|
|
1041
|
+
answering: () => {
|
|
1042
|
+
const job = state.chat.running;
|
|
1043
|
+
return job ? CHAT_AGENT_SOURCES[job.agent] : undefined;
|
|
1044
|
+
},
|
|
918
1045
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
919
1046
|
work: (taskId) => taskWork(state.root, state.configDir, taskId),
|
|
920
1047
|
deliveryDeliver: (taskId, request: DeliveryRequest) => deliver(request, { cwd: state.root, exec: execCommand }, deliveryStore(state.root, state.configDir, taskId)),
|
|
@@ -952,8 +1079,10 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
952
1079
|
discardPlannedTask(state.root, state.configDir, id);
|
|
953
1080
|
lobbyTopics.bump("plans");
|
|
954
1081
|
},
|
|
955
|
-
abortMaster: () =>
|
|
956
|
-
|
|
1082
|
+
abortMaster: () => {
|
|
1083
|
+
state.chat.cancelAll();
|
|
1084
|
+
if (!state.ctx.isIdle()) state.ctx.abort();
|
|
1085
|
+
},
|
|
957
1086
|
planner: () => state.planner,
|
|
958
1087
|
newPlanner: (seed, seats) => newPlanner(state, seed, seats),
|
|
959
1088
|
previousPlans: (archived) => plansBefore(state, archived),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* parent, and the text of user and assistant messages (never tool output).
|
|
8
8
|
*/
|
|
9
9
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
10
|
-
import { chatText, CLEARED_NOTE, isClearedMark, MAX_CHAT, textOf, type ChatEntry } from "./feed.ts";
|
|
10
|
+
import { chatMarkLine, chatText, CLEARED_NOTE, isClearedMark, MAX_CHAT, textOf, type ChatEntry } from "./feed.ts";
|
|
11
11
|
|
|
12
12
|
/** What a session log keeps of one entry: its parent, and its conversation lines if it is a message. */
|
|
13
13
|
interface LogNode {
|
|
@@ -96,6 +96,9 @@ export class SessionLog {
|
|
|
96
96
|
if (chat.length > 0) node.chat = chat;
|
|
97
97
|
} else if (isClearedMark(entry)) {
|
|
98
98
|
node.chat = [{ id: ++logIds, at, role: "note", text: CLEARED_NOTE }];
|
|
99
|
+
} else {
|
|
100
|
+
const marked = chatMarkLine(entry);
|
|
101
|
+
if (marked) node.chat = [{ ...marked, id: ++logIds, at }];
|
|
99
102
|
}
|
|
100
103
|
this.nodes.set(entry.id, node);
|
|
101
104
|
this.leaf = entry.id;
|
package/src/lobby/topics.ts
CHANGED
package/src/master/master.ts
CHANGED
|
@@ -423,7 +423,7 @@ export interface ReviewerRequest {
|
|
|
423
423
|
workerSummary: string;
|
|
424
424
|
scoutOutcomes: ScoutOutcome[];
|
|
425
425
|
diff: string;
|
|
426
|
-
/** Who changed each changed file (planned,
|
|
426
|
+
/** Who changed each changed file (planned, lobby chat, pre-existing, …), one line each. */
|
|
427
427
|
provenance?: string;
|
|
428
428
|
/** What the previous QA round asked for, when there was one: this round verifies it. */
|
|
429
429
|
previousRound?: string;
|
package/src/pi/commands.ts
CHANGED
|
@@ -23,8 +23,7 @@ import { applyStatus, registerRevealShortcut } from "./ui.ts";
|
|
|
23
23
|
import { isOn, registerSwitch, turnOff, turnOn } from "./switch.ts";
|
|
24
24
|
import { classifierSummary } from "./model-settings.ts";
|
|
25
25
|
import { keyStatus } from "../classifier/instance.ts";
|
|
26
|
-
import { kickoff, startPlannedTask } from "./start-task.ts";
|
|
27
|
-
import { startRequest } from "./route.ts";
|
|
26
|
+
import { kickoff, startPlannedTask, startTask } from "./start-task.ts";
|
|
28
27
|
import { setAuto, toggleOwnAuto } from "./owner.ts";
|
|
29
28
|
import { registerWebServer, webCommand } from "../webui/command.ts";
|
|
30
29
|
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
@@ -37,8 +36,8 @@ import { livePresence } from "../state/presence.ts";
|
|
|
37
36
|
import { carryOnMessage } from "../lobby/resume.ts";
|
|
38
37
|
|
|
39
38
|
const HELP = [
|
|
40
|
-
"/bot-lobby Open the web lobby in your browser: tasks, plan,
|
|
41
|
-
"/bot-lobby <request> Start a
|
|
39
|
+
"/bot-lobby Open the web lobby in your browser: chat, tasks, plan, metrics",
|
|
40
|
+
"/bot-lobby <request> Start a task (in the lobby, /task <request> does the same)",
|
|
42
41
|
"/bot-lobby --task [--auto] <request> Always a task (also when the request begins with a subcommand word)",
|
|
43
42
|
"/bot-lobby --fast|--full <request> Start a task on the fast track (straight to the agents it needs) or the full workflow, whatever it reads as",
|
|
44
43
|
"/bot-lobby --branch|--worktree|--no-branch <request> Give the task its own git branch, or its own worktree, named after it (or neither), whatever workflow.gitIsolation says",
|
|
@@ -80,7 +79,7 @@ export interface ParsedCommand {
|
|
|
80
79
|
auto?: boolean;
|
|
81
80
|
/** `--fast` or `--full`: the task's path, whatever its request reads as. */
|
|
82
81
|
track?: "fast" | "full";
|
|
83
|
-
/** `--task`: a task,
|
|
82
|
+
/** `--task`: the rest is a task's request, even when it begins with a subcommand word. */
|
|
84
83
|
task?: boolean;
|
|
85
84
|
/** `--branch`, `--worktree` or `--no-branch`: the task's git isolation, whatever the config says. */
|
|
86
85
|
isolation?: GitIsolation;
|
|
@@ -336,13 +335,12 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
336
335
|
return filtered.length > 0 ? filtered : null;
|
|
337
336
|
},
|
|
338
337
|
handler: async (args, ctx) => {
|
|
339
|
-
const { sub, rest, restText, auto, track,
|
|
338
|
+
const { sub, rest, restText, auto, track, isolation } = parseCommand(args ?? "");
|
|
340
339
|
// A request, the bare command or anything that drives a task needs bot-lobby on (`web stop` does not).
|
|
341
340
|
if (!sub || (TURNS_ON.has(sub) && !(sub === "web" && rest[0] === "stop"))) turnOn(pi, ctx, configDir);
|
|
342
341
|
if (!sub) {
|
|
343
342
|
if (!restText) return webCommand(ctx, undefined);
|
|
344
|
-
|
|
345
|
-
await startRequest(pi, ctx, configDir, restText, { ...(auto ? { auto } : {}), ...(track ? { track } : {}), ...(task ? { task } : {}), ...(isolation ? { isolation } : {}) });
|
|
343
|
+
await startTask(pi, ctx, configDir, restText, { ...(auto ? { auto } : {}), ...(track ? { track } : {}), ...(isolation ? { isolation } : {}) });
|
|
346
344
|
return;
|
|
347
345
|
}
|
|
348
346
|
switch (sub) {
|
package/src/pi/events.ts
CHANGED
|
@@ -55,6 +55,14 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
55
55
|
* operating prompt is patched into the system prompt as one cache-stable
|
|
56
56
|
* section rather than replacing the whole prompt.
|
|
57
57
|
*/
|
|
58
|
+
/** What the oracle is told while no task runs: the lobby is a chat, and the workflow waits for /plan or /task. */
|
|
59
|
+
export const LOBBY_CHAT_NOTE = [
|
|
60
|
+
"## Bot Lobby chat",
|
|
61
|
+
"You are the Master in Bot Lobby's chat: the user talks to the team here, and everyday work (questions, code changes, updates, minor fixes) happens in the chat itself, so do it directly.",
|
|
62
|
+
"DESIGN, DEV, QA, RESEARCH and the scout answer in this chat too when the user names one with @ or the classifier gives a message to one; those lines reach you as \"The user asked …\" and \"… answered\" messages, for context: you need not answer them.",
|
|
63
|
+
"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
|
+
].join("\n");
|
|
65
|
+
|
|
58
66
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
59
67
|
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
60
68
|
let hiddenWeb: string[] = [];
|
|
@@ -121,10 +129,10 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
121
129
|
}
|
|
122
130
|
}
|
|
123
131
|
if (!task) {
|
|
124
|
-
//
|
|
132
|
+
// The lobby chat, and after a task ended and cleared the context, a line on where its record is, in case the user refers back to it.
|
|
125
133
|
const previous = hidden ? undefined : previousTaskNote(ctx, configDir);
|
|
126
|
-
if (
|
|
127
|
-
else
|
|
134
|
+
if (isSubagentProcess() || hidden) delete event.systemPromptOptions.sections["bot-lobby"];
|
|
135
|
+
else event.systemPromptOptions.sections["bot-lobby"] = [LOBBY_CHAT_NOTE, previous ?? ""].filter(Boolean).join("\n\n");
|
|
128
136
|
return;
|
|
129
137
|
}
|
|
130
138
|
const dataRoots = readDataRoots(root, configDir);
|
package/src/pi/fresh-context.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* A boundary is a custom entry in the session file, which is never sent to
|
|
11
11
|
* the model: it survives /resume and belongs to the branch it was made on.
|
|
12
|
-
* Subagents (scouts, workers, reviewers, the planning panel,
|
|
12
|
+
* Subagents (scouts, workers, reviewers, the planning panel, lobby chat agents)
|
|
13
13
|
* already run as pi processes of their own, each with its own context, and
|
|
14
14
|
* are not touched here.
|
|
15
15
|
*/
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -30,7 +30,6 @@ export function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView
|
|
|
30
30
|
if (kind === "master") return config.master;
|
|
31
31
|
if (kind === "scout") return { model: config.scout.model };
|
|
32
32
|
if (kind === "researcher") return config.researcher;
|
|
33
|
-
if (kind === "quickfix") return config.quickFix;
|
|
34
33
|
if (kind === "planner") return config.planner;
|
|
35
34
|
return config.agents[kind];
|
|
36
35
|
}
|
|
@@ -47,7 +46,6 @@ export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: En
|
|
|
47
46
|
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
48
47
|
else if (kind === "scout") next.scout = { ...config.scout, model: patch.model ?? config.scout.model };
|
|
49
48
|
else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
50
|
-
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
51
49
|
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
|
52
50
|
else next.agents[kind] = { ...config.agents[kind], ...patch } as AgentModelConfig;
|
|
53
51
|
return next;
|
package/src/pi/model-support.ts
CHANGED
|
@@ -53,7 +53,6 @@ const LABELS: Record<SubagentKind | "master", string> = {
|
|
|
53
53
|
qa: "QA",
|
|
54
54
|
scout: "Scout",
|
|
55
55
|
researcher: "Researcher",
|
|
56
|
-
quickfix: "Quick fix",
|
|
57
56
|
planner: "Planner",
|
|
58
57
|
};
|
|
59
58
|
|
|
@@ -127,6 +126,17 @@ export function resolvePanelProfile(config: BotLobbyConfig, member: PanelMember,
|
|
|
127
126
|
return resolveRunProfile(panelMemberProfile(config, member), kindLabel(member), options);
|
|
128
127
|
}
|
|
129
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A lobby chat agent's model, thinking and grants: the domain's (or the
|
|
131
|
+
* researcher's) settings, as on the planning panel; the scout's own model at
|
|
132
|
+
* its fixed thinking.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveChatProfile(config: BotLobbyConfig, agent: "designer" | "backend" | "qa" | "researcher" | "scout", options: ResolverOptions): RunProfile {
|
|
135
|
+
if (agent !== "scout") return resolvePanelProfile(config, agent, options);
|
|
136
|
+
const profile = agentProfile(config, "backend", "scout");
|
|
137
|
+
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, ...(profile.mcpTools ? { mcpTools: profile.mcpTools } : {}), ...(profile.skills ? { skills: profile.skills } : {}), ...(profile.autoCompact === false ? { autoCompact: false } : {}), ...(profile.compactAt ? { compactAt: profile.compactAt } : {}), ...(profile.harness ? { harness: profile.harness } : {}) }, kindLabel("scout"), options);
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
/**
|
|
131
141
|
* The model, thinking and grants a pull request review runs on: QA's, the
|
|
132
142
|
* agent whose job is judging work, with its custom instructions.
|
|
@@ -144,7 +154,6 @@ export function thinkingMismatches(config: BotLobbyConfig, lookup: ModelLookup,
|
|
|
144
154
|
["backend", config.agents.backend.model, config.agents.backend.thinking],
|
|
145
155
|
["qa", config.agents.qa.model, config.agents.qa.thinking],
|
|
146
156
|
["researcher", config.researcher.model, config.researcher.thinking],
|
|
147
|
-
["quickfix", config.quickFix.model, config.quickFix.thinking],
|
|
148
157
|
["planner", config.planner.model, config.planner.thinking],
|
|
149
158
|
];
|
|
150
159
|
return entries.flatMap(([kind, ref, level]) => {
|
package/src/pi/owner.ts
CHANGED
|
@@ -32,7 +32,8 @@ export type OwnerEvent =
|
|
|
32
32
|
| { kind: "messages"; count: number }
|
|
33
33
|
| { kind: "auto"; taskId: string; on: boolean }
|
|
34
34
|
| { kind: "nudge"; taskId: string }
|
|
35
|
-
| { kind: "stalled"; taskId: string }
|
|
35
|
+
| { kind: "stalled"; taskId: string }
|
|
36
|
+
| { kind: "held"; taskId: string };
|
|
36
37
|
|
|
37
38
|
interface Owner {
|
|
38
39
|
pi: ExtensionAPI;
|
|
@@ -43,6 +44,8 @@ interface Owner {
|
|
|
43
44
|
configDir: string;
|
|
44
45
|
timer?: ReturnType<typeof setInterval>;
|
|
45
46
|
auto?: AutoTrack;
|
|
47
|
+
/** The user stopped the oracle: auto mode waits until they start a turn again or switch it. */
|
|
48
|
+
held?: boolean;
|
|
46
49
|
/** The session's task as this tick read it: one read serves the whole tick. */
|
|
47
50
|
tick?: { task: Task | undefined };
|
|
48
51
|
}
|
|
@@ -177,7 +180,7 @@ export function driveAuto(): boolean {
|
|
|
177
180
|
state.auto = undefined;
|
|
178
181
|
return false;
|
|
179
182
|
}
|
|
180
|
-
if (!state.ctx.isIdle()) return false;
|
|
183
|
+
if (state.held || !state.ctx.isIdle()) return false;
|
|
181
184
|
const step = autoStep(state.auto, taskFingerprint(task));
|
|
182
185
|
state.auto = step.track;
|
|
183
186
|
if (step.stalledNow) {
|
|
@@ -199,6 +202,7 @@ export function setAuto(root: string, configDir: string, taskId: string, on: boo
|
|
|
199
202
|
const task = ownTask(state);
|
|
200
203
|
if (task?.id !== taskId) return;
|
|
201
204
|
state.auto = undefined;
|
|
205
|
+
state.held = false;
|
|
202
206
|
if (on) driveAuto();
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -213,6 +217,27 @@ export function toggleOwnAuto(): string {
|
|
|
213
217
|
return on ? `auto mode on for ${task.id}: the oracle drives it to completion without asking you` : `auto mode off for ${task.id}: the oracle asks you again`;
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
/** Whether a finished turn was stopped by the user (its last reply was aborted). */
|
|
221
|
+
export function stoppedTurn(messages: readonly unknown[] | undefined): boolean {
|
|
222
|
+
const last = [...(messages ?? [])].reverse().find((message) => (message as { role?: string })?.role === "assistant") as { stopReason?: string } | undefined;
|
|
223
|
+
return last?.stopReason === "aborted";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A turn ended. One the user stopped holds auto mode, so the next tick does
|
|
228
|
+
* not start the task again; any turn that starts after it (the user's next
|
|
229
|
+
* message) lets auto mode carry on.
|
|
230
|
+
*/
|
|
231
|
+
export function turnEnded(messages: readonly unknown[] | undefined): void {
|
|
232
|
+
const state = owner;
|
|
233
|
+
if (!state || !stoppedTurn(messages)) return;
|
|
234
|
+
const task = ownTask(state);
|
|
235
|
+
if (state.held || !live(task) || !isAutoMode(state.root, state.configDir, task.id)) return;
|
|
236
|
+
state.held = true;
|
|
237
|
+
state.auto = undefined;
|
|
238
|
+
listener?.({ kind: "held", taskId: task.id });
|
|
239
|
+
}
|
|
240
|
+
|
|
216
241
|
/** One tick of the owner's clock. */
|
|
217
242
|
export function ownerTick(): void {
|
|
218
243
|
const state = owner;
|
|
@@ -260,8 +285,14 @@ export function registerOwner(pi: ExtensionAPI, configDir: string): void {
|
|
|
260
285
|
};
|
|
261
286
|
pi.on("session_start", (_event, ctx) => startOwner(pi, ctx, configDir));
|
|
262
287
|
pi.on("session_shutdown", () => stop());
|
|
263
|
-
pi.on("agent_start", (_event, ctx) =>
|
|
264
|
-
|
|
288
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
289
|
+
track(ctx);
|
|
290
|
+
if (owner) owner.held = false;
|
|
291
|
+
});
|
|
292
|
+
pi.on("agent_end", (event, ctx) => {
|
|
293
|
+
track(ctx);
|
|
294
|
+
turnEnded((event as { messages?: readonly unknown[] }).messages);
|
|
295
|
+
});
|
|
265
296
|
// Auto mode has nobody to answer a question: tell the oracle to decide instead.
|
|
266
297
|
pi.on("tool_call", (event, ctx) => {
|
|
267
298
|
if (event.toolName !== "ask_user_question" || !owner) return undefined;
|