@kenkaiiii/gg-boss 4.3.140 → 4.3.142

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 (162) hide show
  1. package/dist/{chunk-JVQDTPYR.js → chunk-3EWLK53W.js} +18 -9
  2. package/dist/{chunk-JVQDTPYR.js.map → chunk-3EWLK53W.js.map} +1 -1
  3. package/dist/{chunk-5WNYQQPQ.js → chunk-QT366Y52.js} +4 -3
  4. package/dist/{chunk-5WNYQQPQ.js.map → chunk-QT366Y52.js.map} +1 -1
  5. package/dist/{chunk-PQAHDHVY.js → chunk-WJ4S4TOY.js} +3 -2
  6. package/dist/{chunk-PQAHDHVY.js.map → chunk-WJ4S4TOY.js.map} +1 -1
  7. package/dist/{chunk-B2WQ5E5J.js → chunk-YNWFCUMR.js} +2 -1
  8. package/dist/{chunk-B2WQ5E5J.js.map → chunk-YNWFCUMR.js.map} +1 -1
  9. package/dist/cli.js +2248 -182
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{devtools-VBUDNGEI.js → devtools-4TI4D7F2.js} +3 -2
  12. package/dist/{devtools-VBUDNGEI.js.map → devtools-4TI4D7F2.js.map} +1 -1
  13. package/dist/{dist-7DAPKZGX.js → dist-VXOVSHZ5.js} +3 -2
  14. package/dist/{dist-7DAPKZGX.js.map → dist-VXOVSHZ5.js.map} +1 -1
  15. package/dist/{ignore-3AEIALHQ.js → ignore-76P4EAAU.js} +3 -2
  16. package/dist/{ignore-3AEIALHQ.js.map → ignore-76P4EAAU.js.map} +1 -1
  17. package/dist/index.js +21 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/{out-D65DTPFZ.js → out-XEXARMKS.js} +3 -2
  20. package/dist/{out-D65DTPFZ.js.map → out-XEXARMKS.js.map} +1 -1
  21. package/dist/pixel-WPYTQADG.js +14 -0
  22. package/dist/{pixel-fix-ALWXCLTS.js → pixel-fix-4WGZAJ5W.js} +4 -3
  23. package/dist/{pixel-fix-ALWXCLTS.js.map → pixel-fix-4WGZAJ5W.js.map} +1 -1
  24. package/package.json +10 -11
  25. package/dist/audio.d.ts +0 -21
  26. package/dist/audio.d.ts.map +0 -1
  27. package/dist/audio.js +0 -231
  28. package/dist/audio.js.map +0 -1
  29. package/dist/audio.test.d.ts +0 -2
  30. package/dist/audio.test.d.ts.map +0 -1
  31. package/dist/audio.test.js +0 -13
  32. package/dist/audio.test.js.map +0 -1
  33. package/dist/auto-update.d.ts +0 -24
  34. package/dist/auto-update.d.ts.map +0 -1
  35. package/dist/auto-update.js +0 -231
  36. package/dist/auto-update.js.map +0 -1
  37. package/dist/banner.d.ts +0 -17
  38. package/dist/banner.d.ts.map +0 -1
  39. package/dist/banner.js +0 -25
  40. package/dist/banner.js.map +0 -1
  41. package/dist/boss-footer.d.ts +0 -25
  42. package/dist/boss-footer.d.ts.map +0 -1
  43. package/dist/boss-footer.js +0 -107
  44. package/dist/boss-footer.js.map +0 -1
  45. package/dist/boss-phrases.d.ts +0 -9
  46. package/dist/boss-phrases.d.ts.map +0 -1
  47. package/dist/boss-phrases.js +0 -71
  48. package/dist/boss-phrases.js.map +0 -1
  49. package/dist/boss-store.d.ts +0 -245
  50. package/dist/boss-store.d.ts.map +0 -1
  51. package/dist/boss-store.js +0 -623
  52. package/dist/boss-store.js.map +0 -1
  53. package/dist/boss-system-prompt.d.ts +0 -3
  54. package/dist/boss-system-prompt.d.ts.map +0 -1
  55. package/dist/boss-system-prompt.js +0 -180
  56. package/dist/boss-system-prompt.js.map +0 -1
  57. package/dist/boss-tasks-overlay.d.ts +0 -22
  58. package/dist/boss-tasks-overlay.d.ts.map +0 -1
  59. package/dist/boss-tasks-overlay.js +0 -157
  60. package/dist/boss-tasks-overlay.js.map +0 -1
  61. package/dist/branding.d.ts +0 -32
  62. package/dist/branding.d.ts.map +0 -1
  63. package/dist/branding.js +0 -59
  64. package/dist/branding.js.map +0 -1
  65. package/dist/cli.d.ts +0 -3
  66. package/dist/cli.d.ts.map +0 -1
  67. package/dist/cli.smoke.test.d.ts +0 -2
  68. package/dist/cli.smoke.test.d.ts.map +0 -1
  69. package/dist/cli.smoke.test.js +0 -48
  70. package/dist/cli.smoke.test.js.map +0 -1
  71. package/dist/colors.d.ts +0 -14
  72. package/dist/colors.d.ts.map +0 -1
  73. package/dist/colors.js +0 -31
  74. package/dist/colors.js.map +0 -1
  75. package/dist/discover.d.ts +0 -13
  76. package/dist/discover.d.ts.map +0 -1
  77. package/dist/discover.js +0 -92
  78. package/dist/discover.js.map +0 -1
  79. package/dist/event-queue.d.ts +0 -16
  80. package/dist/event-queue.d.ts.map +0 -1
  81. package/dist/event-queue.js +0 -39
  82. package/dist/event-queue.js.map +0 -1
  83. package/dist/index.d.ts +0 -6
  84. package/dist/index.d.ts.map +0 -1
  85. package/dist/link-command.d.ts +0 -2
  86. package/dist/link-command.d.ts.map +0 -1
  87. package/dist/link-command.js +0 -120
  88. package/dist/link-command.js.map +0 -1
  89. package/dist/links.d.ts +0 -11
  90. package/dist/links.d.ts.map +0 -1
  91. package/dist/links.js +0 -22
  92. package/dist/links.js.map +0 -1
  93. package/dist/logger.d.ts +0 -41
  94. package/dist/logger.d.ts.map +0 -1
  95. package/dist/logger.js +0 -112
  96. package/dist/logger.js.map +0 -1
  97. package/dist/orchestrator-app.d.ts +0 -15
  98. package/dist/orchestrator-app.d.ts.map +0 -1
  99. package/dist/orchestrator-app.js +0 -599
  100. package/dist/orchestrator-app.js.map +0 -1
  101. package/dist/orchestrator.d.ts +0 -147
  102. package/dist/orchestrator.d.ts.map +0 -1
  103. package/dist/orchestrator.js +0 -707
  104. package/dist/orchestrator.js.map +0 -1
  105. package/dist/orchestrator.test.d.ts +0 -2
  106. package/dist/orchestrator.test.d.ts.map +0 -1
  107. package/dist/orchestrator.test.js +0 -55
  108. package/dist/orchestrator.test.js.map +0 -1
  109. package/dist/pixel-WB6VRJWP.js +0 -13
  110. package/dist/radio-picker.d.ts +0 -20
  111. package/dist/radio-picker.d.ts.map +0 -1
  112. package/dist/radio-picker.js +0 -31
  113. package/dist/radio-picker.js.map +0 -1
  114. package/dist/radio.d.ts +0 -43
  115. package/dist/radio.d.ts.map +0 -1
  116. package/dist/radio.js +0 -150
  117. package/dist/radio.js.map +0 -1
  118. package/dist/sessions.d.ts +0 -21
  119. package/dist/sessions.d.ts.map +0 -1
  120. package/dist/sessions.js +0 -122
  121. package/dist/sessions.js.map +0 -1
  122. package/dist/settings.d.ts +0 -11
  123. package/dist/settings.d.ts.map +0 -1
  124. package/dist/settings.js +0 -38
  125. package/dist/settings.js.map +0 -1
  126. package/dist/slash-commands.d.ts +0 -19
  127. package/dist/slash-commands.d.ts.map +0 -1
  128. package/dist/slash-commands.js +0 -76
  129. package/dist/slash-commands.js.map +0 -1
  130. package/dist/splash.d.ts +0 -21
  131. package/dist/splash.d.ts.map +0 -1
  132. package/dist/splash.js +0 -137
  133. package/dist/splash.js.map +0 -1
  134. package/dist/task-tools.d.ts +0 -18
  135. package/dist/task-tools.d.ts.map +0 -1
  136. package/dist/task-tools.js +0 -172
  137. package/dist/task-tools.js.map +0 -1
  138. package/dist/tasks-store.d.ts +0 -66
  139. package/dist/tasks-store.d.ts.map +0 -1
  140. package/dist/tasks-store.js +0 -199
  141. package/dist/tasks-store.js.map +0 -1
  142. package/dist/tasks-store.test.d.ts +0 -2
  143. package/dist/tasks-store.test.d.ts.map +0 -1
  144. package/dist/tasks-store.test.js +0 -138
  145. package/dist/tasks-store.test.js.map +0 -1
  146. package/dist/tool-formatters.d.ts +0 -7
  147. package/dist/tool-formatters.d.ts.map +0 -1
  148. package/dist/tool-formatters.js +0 -111
  149. package/dist/tool-formatters.js.map +0 -1
  150. package/dist/tools.d.ts +0 -26
  151. package/dist/tools.d.ts.map +0 -1
  152. package/dist/tools.js +0 -133
  153. package/dist/tools.js.map +0 -1
  154. package/dist/types.d.ts +0 -32
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js +0 -2
  157. package/dist/types.js.map +0 -1
  158. package/dist/worker.d.ts +0 -47
  159. package/dist/worker.d.ts.map +0 -1
  160. package/dist/worker.js +0 -123
  161. package/dist/worker.js.map +0 -1
  162. /package/dist/{pixel-WB6VRJWP.js.map → pixel-WPYTQADG.js.map} +0 -0
@@ -1,707 +0,0 @@
1
- import { Agent, isAbortError } from "@kenkaiiii/gg-agent";
2
- import { AuthStorage, compact, estimateConversationTokens, getContextWindow, shouldCompact, } from "@kenkaiiii/ggcoder";
3
- import { Worker } from "./worker.js";
4
- import { EventQueue } from "./event-queue.js";
5
- import { createBossTools, WORKER_PROMPT_BRIEF } from "./tools.js";
6
- import { createTaskTools } from "./task-tools.js";
7
- import { tasksStore } from "./tasks-store.js";
8
- import { saveSettings } from "./settings.js";
9
- import { playDoneAudio, playReadyAudio } from "./audio.js";
10
- import { log } from "./logger.js";
11
- import { buildBossSystemPrompt } from "./boss-system-prompt.js";
12
- import { bossStore } from "./boss-store.js";
13
- import { appendMessages, createSession, getMostRecent, getSessionById, loadSession, } from "./sessions.js";
14
- /**
15
- * The orchestrator. Owns N workers, a single shared event queue, and the boss Agent.
16
- * Each loop iteration: pop one event, format it as a user message, run the boss for
17
- * one full prompt (which may dispatch tool calls to workers), then await the next event.
18
- *
19
- * UI state is mirrored into bossStore — components subscribe via useBossState().
20
- */
21
- export class GGBoss {
22
- workers = new Map();
23
- lastSummaries = new Map();
24
- queue = new EventQueue();
25
- bossAgent;
26
- ac = new AbortController();
27
- /** Per-turn AbortController so ESC can cancel the current LLM call without killing workers. */
28
- turnAc = null;
29
- running = false;
30
- pendingUserMessages = 0;
31
- opts;
32
- authStorage = new AuthStorage();
33
- /** Path to the boss's per-session jsonl log under ~/.gg/boss/sessions/. */
34
- sessionPath = "";
35
- /** Last index in the boss's messages array we've persisted to disk. */
36
- lastPersistedIndex = 0;
37
- /** project → task id currently dispatched to that worker. Used to mark
38
- * the right task done/blocked when the worker_turn_complete event arrives. */
39
- inFlightTaskByProject = new Map();
40
- /**
41
- * Auto-chain notices waiting to be delivered to the boss. When the
42
- * orchestrator deterministically dispatches the next pending task for a
43
- * project (because the boss didn't), the boss has no other way to know it
44
- * happened — it'd see "X(working)" in the next event's other_workers
45
- * trailer and dismiss it as stale because it remembers receiving X's prior
46
- * completion event. We attach an explicit note to the next event so the
47
- * boss's mental model stays in sync with reality.
48
- */
49
- pendingAutoChainNotices = [];
50
- /**
51
- * "Had any worker activity since the last all-clear chime?" Set true when
52
- * a worker_turn_complete or worker_error event arrives, cleared when we
53
- * detect the orchestrator has fully wound down (all workers idle, queue
54
- * empty, boss turn finished). Drives playReadyAudio so the chime fires
55
- * once per workflow instead of every time the boss replies to a chat
56
- * message that didn't dispatch any workers.
57
- */
58
- hadWorkerActivitySinceReady = false;
59
- constructor(opts) {
60
- this.opts = opts;
61
- }
62
- async initialize() {
63
- await this.authStorage.load();
64
- await tasksStore.load();
65
- const loggedInProviders = (await this.authStorage.listProviders());
66
- bossStore.init({
67
- bossProvider: this.opts.bossProvider,
68
- bossModel: this.opts.bossModel,
69
- bossThinkingLevel: this.opts.bossThinkingLevel,
70
- workerProvider: this.opts.workerProvider,
71
- workerModel: this.opts.workerModel,
72
- loggedInProviders,
73
- workers: this.opts.projects.map((p) => ({ name: p.name, cwd: p.cwd })),
74
- });
75
- await Promise.all(this.opts.projects.map(async (p) => {
76
- const worker = new Worker({
77
- name: p.name,
78
- cwd: p.cwd,
79
- provider: this.opts.workerProvider,
80
- model: this.opts.workerModel,
81
- thinkingLevel: this.opts.workerThinkingLevel,
82
- signal: this.ac.signal,
83
- queue: this.queue,
84
- });
85
- await worker.initialize();
86
- this.workers.set(p.name, worker);
87
- }));
88
- const creds = await this.authStorage.resolveCredentials(this.opts.bossProvider);
89
- const tools = this.buildToolSet();
90
- // Either resume a prior session (load messages from jsonl), or create a
91
- // new one. Either way we end up with `sessionPath` to persist into.
92
- let priorMessages;
93
- if (this.opts.resumeSessionId) {
94
- const info = await getSessionById(this.opts.resumeSessionId);
95
- if (info) {
96
- this.sessionPath = info.path;
97
- priorMessages = (await loadSession(info.path)).filter((m) => m.role !== "system");
98
- }
99
- }
100
- else if (this.opts.continueRecent) {
101
- const recent = await getMostRecent();
102
- if (recent) {
103
- this.sessionPath = recent.path;
104
- priorMessages = (await loadSession(recent.path)).filter((m) => m.role !== "system");
105
- }
106
- }
107
- if (!this.sessionPath) {
108
- const session = await createSession();
109
- this.sessionPath = session.filePath;
110
- }
111
- // Rebuild the visible TUI history from the loaded messages so the chat
112
- // shows the prior conversation, not just the agent's hidden context.
113
- if (priorMessages && priorMessages.length > 0) {
114
- bossStore.restoreHistory(priorMessages);
115
- }
116
- this.bossAgent = new Agent({
117
- provider: this.opts.bossProvider,
118
- model: this.opts.bossModel,
119
- system: buildBossSystemPrompt(this.opts.projects),
120
- tools,
121
- apiKey: creds.accessToken,
122
- accountId: creds.accountId,
123
- signal: this.ac.signal,
124
- cacheRetention: "short",
125
- thinking: this.opts.bossThinkingLevel,
126
- priorMessages,
127
- });
128
- // Mark every loaded message as already persisted so we only append NEW
129
- // turns going forward. The system message is added by Agent's constructor
130
- // and we never want to write the system prompt to disk (it's rebuilt each
131
- // session from current project list) — so subtract one for it.
132
- this.lastPersistedIndex = this.bossAgent.getMessages().length;
133
- // Seed the context-bar estimate so it shows real progress before the first
134
- // turn_end event fires. Especially critical on `ggboss continue` where
135
- // we'd otherwise show 0% over a session that's already half-full.
136
- const initialMessages = this.bossAgent.getMessages();
137
- if (initialMessages.length > 1) {
138
- bossStore.setBossInputTokens(estimateConversationTokens(initialMessages));
139
- }
140
- }
141
- enqueueUserMessage(text) {
142
- this.pendingUserMessages++;
143
- bossStore.setPendingMessages(this.pendingUserMessages);
144
- this.queue.push({
145
- kind: "user_message",
146
- text,
147
- timestamp: new Date().toISOString(),
148
- });
149
- }
150
- /**
151
- * Abort the boss's current LLM call (e.g. user pressed ESC). Workers and the
152
- * orchestrator's run loop keep going. The next event in the queue gets a
153
- * fresh AbortController.
154
- */
155
- abort() {
156
- this.turnAc?.abort();
157
- }
158
- /** Boss tool set = orchestration tools + task management tools. */
159
- buildToolSet() {
160
- const bossTools = createBossTools({
161
- workers: this.workers,
162
- lastSummaries: this.lastSummaries,
163
- });
164
- const taskTools = createTaskTools({
165
- workers: this.workers,
166
- dispatchTaskByDescription: (project, description, fresh, taskId) => this.dispatchTaskByDescription(project, description, fresh, taskId),
167
- });
168
- return [...bossTools, ...taskTools];
169
- }
170
- /**
171
- * Dispatch a single task to a specific worker, marking it in_progress and
172
- * (eventually) done when the worker_turn_complete event arrives. Used by:
173
- * - the dispatch_pending tool (called by the boss agent)
174
- * - the Tasks overlay (when user presses Enter on a task)
175
- *
176
- * Returns immediately — fire-and-forget like prompt_worker.
177
- */
178
- async dispatchTaskById(taskId) {
179
- const task = tasksStore.byId(taskId);
180
- if (!task)
181
- return { ok: false, reason: "unknown task id" };
182
- const w = this.workers.get(task.project);
183
- if (!w)
184
- return { ok: false, reason: `unknown project: ${task.project}` };
185
- if (w.getStatus() === "working")
186
- return { ok: false, reason: "worker is busy" };
187
- await tasksStore.update(task.id, { status: "in_progress" });
188
- return this.dispatchTaskByDescription(task.project, task.description, task.fresh === true, task.id);
189
- }
190
- /**
191
- * Dispatch a task description to a worker. Used by both the task tool and
192
- * the overlay (via dispatchTaskById). Tracks the in-flight task id per
193
- * project so worker_turn_complete can resolve it back to the right task.
194
- */
195
- async dispatchTaskByDescription(project, description, fresh, taskId) {
196
- const w = this.workers.get(project);
197
- if (!w) {
198
- log("WARN", "dispatch", "unknown project", { project, taskId });
199
- return { ok: false, reason: `unknown project: ${project}` };
200
- }
201
- if (w.getStatus() === "working") {
202
- log("WARN", "dispatch", "worker busy", { project, taskId });
203
- return { ok: false, reason: "worker is busy" };
204
- }
205
- if (fresh)
206
- await w.newSession();
207
- this.inFlightTaskByProject.set(project, taskId);
208
- log("INFO", "dispatch", "task dispatched", { project, taskId, fresh });
209
- await w.prompt(WORKER_PROMPT_BRIEF + description);
210
- return { ok: true };
211
- }
212
- /**
213
- * Swap the boss's LLM model. Preserves message history so the conversation
214
- * continues seamlessly under the new model.
215
- */
216
- async switchBossModel(provider, model) {
217
- const tools = this.buildToolSet();
218
- const creds = await this.authStorage.resolveCredentials(provider);
219
- // Capture history minus the system message — Agent re-adds system from options.
220
- const oldMessages = this.bossAgent.getMessages().filter((m) => m.role !== "system");
221
- this.opts.bossProvider = provider;
222
- this.opts.bossModel = model;
223
- this.bossAgent = new Agent({
224
- provider,
225
- model,
226
- system: buildBossSystemPrompt(this.opts.projects),
227
- tools,
228
- apiKey: creds.accessToken,
229
- accountId: creds.accountId,
230
- signal: this.ac.signal,
231
- cacheRetention: "short",
232
- thinking: this.opts.bossThinkingLevel,
233
- priorMessages: oldMessages,
234
- });
235
- bossStore.setBossModel(provider, model);
236
- await saveSettings({ bossProvider: provider, bossModel: model });
237
- }
238
- /** Swap every worker's model. Workers keep their per-project sessions. */
239
- async switchWorkerModel(provider, model) {
240
- await Promise.all([...this.workers.values()].map((w) => w.switchModel(provider, model)));
241
- this.opts.workerProvider = provider;
242
- this.opts.workerModel = model;
243
- bossStore.setWorkerModel(provider, model);
244
- await saveSettings({ workerProvider: provider, workerModel: model });
245
- }
246
- /**
247
- * Run a manual compaction now (driven by /compact). Will compact even if the
248
- * threshold isn't reached yet — useful for trimming context before a long task.
249
- */
250
- async manualCompact() {
251
- await this.runCompaction(true);
252
- }
253
- /** Compact only when threshold (default 80%) is exceeded. */
254
- async runCompaction(force) {
255
- const messages = this.bossAgent.getMessages();
256
- const contextWindow = getContextWindow(this.opts.bossModel);
257
- const tokens = bossStore.getInputTokens();
258
- if (!force && !shouldCompact(messages, contextWindow, 0.8, tokens))
259
- return;
260
- bossStore.startCompaction();
261
- try {
262
- const creds = await this.authStorage.resolveCredentials(this.opts.bossProvider);
263
- const { messages: compactedMessages, result } = await compact(messages, {
264
- provider: this.opts.bossProvider,
265
- model: this.opts.bossModel,
266
- apiKey: creds.accessToken,
267
- contextWindow,
268
- signal: this.ac.signal,
269
- });
270
- await this.replaceBossMessages(compactedMessages);
271
- // Start a new session file so `ggboss continue` resumes the COMPACTED
272
- // history, not the full original. Mirrors ggcoder/AgentSession.compact.
273
- const session = await createSession();
274
- this.sessionPath = session.filePath;
275
- this.lastPersistedIndex = 0;
276
- await this.persistNewMessages();
277
- bossStore.setBossInputTokens(0);
278
- bossStore.endCompaction(result.originalCount, result.newCount);
279
- }
280
- catch (err) {
281
- bossStore.cancelCompaction();
282
- if (!isAbortError(err)) {
283
- const message = err instanceof Error ? err.message : String(err);
284
- bossStore.appendInfo(`Compaction failed: ${message}`, "error");
285
- }
286
- }
287
- }
288
- /**
289
- * Append any boss messages that haven't been written yet to the session log.
290
- * Skips the system message (regenerated each session from current project list).
291
- */
292
- async persistNewMessages() {
293
- if (!this.sessionPath)
294
- return;
295
- const all = this.bossAgent.getMessages();
296
- const newOnes = all.slice(this.lastPersistedIndex).filter((m) => m.role !== "system");
297
- if (newOnes.length === 0)
298
- return;
299
- try {
300
- await appendMessages(this.sessionPath, newOnes);
301
- this.lastPersistedIndex = all.length;
302
- }
303
- catch {
304
- // Persistence is best-effort — never crash the run loop on disk errors.
305
- }
306
- }
307
- /**
308
- * Toggle the boss's extended-thinking level. Recreates bossAgent with the
309
- * new setting (Anthropic SDK reads `thinking` once on construction). Mirrors
310
- * ggcoder's Shift+Tab UX. Persists to settings.json so the choice sticks
311
- * across restarts.
312
- */
313
- async setBossThinking(level) {
314
- this.opts.bossThinkingLevel = level;
315
- const tools = this.buildToolSet();
316
- const creds = await this.authStorage.resolveCredentials(this.opts.bossProvider);
317
- const oldMessages = this.bossAgent.getMessages().filter((m) => m.role !== "system");
318
- this.bossAgent = new Agent({
319
- provider: this.opts.bossProvider,
320
- model: this.opts.bossModel,
321
- system: buildBossSystemPrompt(this.opts.projects),
322
- tools,
323
- apiKey: creds.accessToken,
324
- accountId: creds.accountId,
325
- signal: this.ac.signal,
326
- cacheRetention: "short",
327
- thinking: level,
328
- priorMessages: oldMessages,
329
- });
330
- bossStore.setBossThinking(level);
331
- await saveSettings({ bossThinkingLevel: level });
332
- }
333
- /** Recreate bossAgent with a new message history (used by compact + /clear). */
334
- async replaceBossMessages(newMessages) {
335
- const tools = this.buildToolSet();
336
- const creds = await this.authStorage.resolveCredentials(this.opts.bossProvider);
337
- // Strip system — Agent re-adds it from `system`.
338
- const priorMessages = newMessages.filter((m) => m.role !== "system");
339
- this.bossAgent = new Agent({
340
- provider: this.opts.bossProvider,
341
- model: this.opts.bossModel,
342
- system: buildBossSystemPrompt(this.opts.projects),
343
- tools,
344
- apiKey: creds.accessToken,
345
- accountId: creds.accountId,
346
- signal: this.ac.signal,
347
- cacheRetention: "short",
348
- thinking: this.opts.bossThinkingLevel,
349
- priorMessages,
350
- });
351
- }
352
- /**
353
- * Start a brand-new boss session — fresh agent with no message history,
354
- * fresh session file on disk so `ggboss continue` picks up the new chat.
355
- * Workers are unaffected.
356
- */
357
- async newSession() {
358
- const session = await createSession();
359
- this.sessionPath = session.filePath;
360
- this.lastPersistedIndex = 0;
361
- await this.replaceBossMessages([]);
362
- bossStore.setBossInputTokens(0);
363
- // Mark the post-construction message count (just system) as persisted so
364
- // we don't try to write it.
365
- this.lastPersistedIndex = this.bossAgent.getMessages().length;
366
- }
367
- /** Alias kept for the existing /clear path which used "reset" terminology. */
368
- async resetConversation() {
369
- return this.newSession();
370
- }
371
- async run() {
372
- this.running = true;
373
- while (this.running) {
374
- const event = await this.queue.next();
375
- if (!this.running)
376
- break;
377
- if (event.kind === "user_message") {
378
- this.pendingUserMessages = Math.max(0, this.pendingUserMessages - 1);
379
- bossStore.setPendingMessages(this.pendingUserMessages);
380
- }
381
- // Captured so the post-turn auto-chain can tell whether THIS event was
382
- // a dispatched task (chain on) vs an ad-hoc prompt_worker like recon
383
- // (chain off). Lives outside the `if` so it stays in scope down below.
384
- let finishedTaskId = null;
385
- if (event.kind === "worker_turn_complete") {
386
- // Play the completion chime — fire-and-forget. Multiple workers
387
- // finishing in quick succession will layer their sounds, which is
388
- // fine: it's a chime, not a long jingle.
389
- void playDoneAudio();
390
- this.hadWorkerActivitySinceReady = true;
391
- this.lastSummaries.set(event.summary.project, event.summary);
392
- log("INFO", "worker_turn_complete", "worker finished", {
393
- project: event.summary.project,
394
- turn: event.summary.turnIndex,
395
- tools: event.summary.toolsUsed.length,
396
- failed: event.summary.toolsUsed.filter((t) => !t.ok).length,
397
- });
398
- // Resolve any in-flight task for this project to its final status.
399
- // Boss can still override via update_task — this just gives it a sane
400
- // default so the user's overlay-driven dispatches close out cleanly.
401
- const taskId = this.inFlightTaskByProject.get(event.summary.project);
402
- finishedTaskId = taskId ?? null;
403
- if (taskId) {
404
- this.inFlightTaskByProject.delete(event.summary.project);
405
- const task = tasksStore.byId(taskId);
406
- if (task && task.status === "in_progress") {
407
- // Use the worker's SELF-REPORTED status from the trailer ("Status:
408
- // DONE | UNVERIFIED | PARTIAL | BLOCKED | INFO"). The previous
409
- // heuristic "any tool failed → blocked" was way too aggressive —
410
- // workers commonly have an incidental bash non-zero (grep with no
411
- // match, cd to wrong path) during exploration even when the task
412
- // itself was completed cleanly. Self-report is what the boss reads
413
- // anyway, so we should mark off the same signal.
414
- const reported = parseReportedStatus(event.summary.finalText);
415
- const newStatus = reportedToTaskStatus(reported, event.summary.toolsUsed.some((t) => !t.ok));
416
- await tasksStore.update(taskId, {
417
- status: newStatus,
418
- resultSummary: event.summary.finalText,
419
- });
420
- }
421
- }
422
- }
423
- if (event.kind === "worker_error") {
424
- this.hadWorkerActivitySinceReady = true;
425
- log("ERROR", "worker_error", event.message, { project: event.project });
426
- const taskId = this.inFlightTaskByProject.get(event.project);
427
- if (taskId) {
428
- this.inFlightTaskByProject.delete(event.project);
429
- await tasksStore.update(taskId, {
430
- status: "blocked",
431
- notes: `Worker error: ${event.message}`,
432
- });
433
- }
434
- }
435
- // Auto-compact when over 80% of context — mirrors AgentSession.runLoop.
436
- // Workers handle their own compaction independently (via AgentSession).
437
- await this.runCompaction(false);
438
- // Snapshot every worker's status at the moment the event arrives so the
439
- // boss reasons from live state, not from its memory of past dispatches.
440
- // Without this the boss can hallucinate "all idle" mid-batch — by event
441
- // 3 of 5 it has heard 3 completions and may assume the run is over even
442
- // though workers 4 and 5 are still active.
443
- const workerSnapshot = [...this.workers.entries()].map(([name, w]) => ({
444
- name,
445
- status: w.getStatus(),
446
- }));
447
- // Drain any auto-chain notices accumulated since the last event so the
448
- // boss is told explicitly which projects we re-dispatched on its behalf.
449
- const notices = this.pendingAutoChainNotices.splice(0);
450
- const text = formatEventForBoss(event, workerSnapshot, notices);
451
- bossStore.startStreaming();
452
- // Fresh AbortController for this turn so ESC can cancel just this call.
453
- this.turnAc = new AbortController();
454
- this.bossAgent.setSignal(this.turnAc.signal);
455
- try {
456
- const stream = this.bossAgent.prompt(text);
457
- for await (const e of stream) {
458
- switch (e.type) {
459
- case "text_delta":
460
- bossStore.appendStreamText(e.text);
461
- break;
462
- case "thinking_delta":
463
- bossStore.appendStreamThinking(e.text);
464
- break;
465
- case "tool_call_start":
466
- // Flush any preceding text so chronological order is preserved
467
- // in scrollback (text → tool → text → tool, not text-block then tool-block).
468
- bossStore.flushPendingText();
469
- bossStore.startTool(e.toolCallId, e.name, e.args);
470
- bossStore.setActivityPhase("tools");
471
- break;
472
- case "tool_call_end":
473
- bossStore.endTool(e.toolCallId, e.isError, e.durationMs, e.result, e.details);
474
- break;
475
- case "turn_end":
476
- // Mirror ggcoder/useAgentLoop: total context = uncached input +
477
- // cache reads + cache writes (Anthropic separates input/output,
478
- // others share the window so include output too). Without adding
479
- // cache, prompt-cached calls report a tiny inputTokens delta and
480
- // the footer bar appears stuck at 0%.
481
- if (e.usage) {
482
- bossStore.setBossInputTokens(computeContextUsed(e.usage, this.opts.bossProvider));
483
- }
484
- // Flush trailing text from this turn. Subsequent turns may add more.
485
- bossStore.flushPendingText();
486
- // Flush any tool-queued end-of-turn infos (e.g. add_task's
487
- // Ctrl+T hint) so they land AFTER the boss's tool calls, not
488
- // interleaved with them.
489
- bossStore.flushEndOfTurnInfos();
490
- break;
491
- case "retry":
492
- if (!e.silent) {
493
- bossStore.setRetryInfo({
494
- reason: e.reason,
495
- attempt: e.attempt,
496
- maxAttempts: e.maxAttempts,
497
- delayMs: e.delayMs,
498
- });
499
- }
500
- break;
501
- case "error":
502
- bossStore.appendInfo(formatProviderError(e.error.message), "error");
503
- break;
504
- default:
505
- break;
506
- }
507
- }
508
- }
509
- catch (err) {
510
- if (isAbortError(err)) {
511
- // Mirror ggcoder's onAborted: convert any in-flight tools to
512
- // "Stopped." entries so the user sees the same visual feedback.
513
- bossStore.interruptStreaming();
514
- if (!this.running) {
515
- bossStore.finishStreaming();
516
- return;
517
- }
518
- bossStore.appendInfo("Interrupted by user.", "warning");
519
- bossStore.finishStreaming();
520
- await this.persistNewMessages();
521
- continue;
522
- }
523
- const message = err instanceof Error ? err.message : String(err);
524
- log("ERROR", "boss_turn", message);
525
- bossStore.appendInfo(formatProviderError(message), "error");
526
- }
527
- bossStore.finishStreaming();
528
- await this.persistNewMessages();
529
- // Auto-chain: after the boss finishes processing a worker_turn_complete,
530
- // if it didn't dispatch anything for that project (worker is still idle)
531
- // AND there are more pending tasks for that project, fire the next one
532
- // automatically. The idle check arbitrates with the boss — if the boss
533
- // DID prompt_worker / dispatch_pending / re-prompt during its turn, the
534
- // worker is now "working", we skip. So this only kicks in when the boss
535
- // implicitly leaves the project parked.
536
- // Auto-chain ONLY fires when the just-finished event was itself a
537
- // dispatched task (had a taskId tracked in inFlightTaskByProject above).
538
- // Otherwise we'd hijack ad-hoc prompt_worker calls — e.g. recon prompts
539
- // — by dispatching pending backlog tasks the user never asked to run.
540
- if (event.kind === "worker_turn_complete" && finishedTaskId) {
541
- await this.maybeAutoChain(event.summary.project);
542
- }
543
- // All-clear chime — fires when the orchestrator winds down after a
544
- // burst of activity. Conditions: at least one worker event happened
545
- // since the last chime, every worker is now idle, and the queue is
546
- // drained (no more events queued for the boss). Resets the flag so
547
- // the next workflow gets its own chime.
548
- const allWorkersIdle = [...this.workers.values()].every((w) => w.getStatus() === "idle");
549
- if (this.hadWorkerActivitySinceReady && allWorkersIdle && this.queue.size() === 0) {
550
- this.hadWorkerActivitySinceReady = false;
551
- log("INFO", "all_clear", "all workers idle, queue empty");
552
- void playReadyAudio();
553
- }
554
- }
555
- }
556
- async maybeAutoChain(project) {
557
- const worker = this.workers.get(project);
558
- if (!worker || worker.getStatus() !== "idle") {
559
- log("DEBUG", "auto_chain", "skip — worker not idle", { project });
560
- return;
561
- }
562
- if (this.inFlightTaskByProject.has(project)) {
563
- log("DEBUG", "auto_chain", "skip — task already in flight", { project });
564
- return;
565
- }
566
- // Pull pending OR blocked — auto-chain retries blocked tasks too so a
567
- // single bad turn doesn't park the whole project. Pending is preferred.
568
- const next = tasksStore.nextDispatchable(project);
569
- if (!next) {
570
- log("DEBUG", "auto_chain", "skip — no dispatchable tasks", { project });
571
- return;
572
- }
573
- if (next.status === "blocked") {
574
- await tasksStore.update(next.id, { status: "pending", notes: undefined });
575
- }
576
- log("INFO", "auto_chain", "dispatching next task", {
577
- project,
578
- taskId: next.id,
579
- title: next.title,
580
- previousStatus: next.status,
581
- });
582
- await this.dispatchTaskByDescription(project, next.description, next.fresh === true, next.id);
583
- // Queue a note for the boss so it knows this project is on a fresh task.
584
- // Without this the boss sees "X(working)" in the next event's trailer and
585
- // dismisses it as stale.
586
- this.pendingAutoChainNotices.push({ project, title: next.title });
587
- }
588
- async dispose() {
589
- this.running = false;
590
- this.ac.abort();
591
- // Wake the queue if it's blocked on next() so the run loop can exit.
592
- this.queue.push({
593
- kind: "user_message",
594
- text: "[shutdown]",
595
- timestamp: new Date().toISOString(),
596
- });
597
- await Promise.all([...this.workers.values()].map((w) => w.dispose()));
598
- }
599
- }
600
- /**
601
- * Pull the worker's self-reported "Status: X" line out of its final text. The
602
- * trailer is appended by every prompt via WORKER_PROMPT_BRIEF, so it should
603
- * always be there for task-style runs. Returns null if missing or unrecognised.
604
- */
605
- export function parseReportedStatus(finalText) {
606
- // Match the LAST "Status: X" line — workers occasionally mention statuses
607
- // mid-text and we want the trailer's value, not an example sentence.
608
- const matches = [
609
- ...finalText.matchAll(/^\s*Status:\s*(DONE|UNVERIFIED|PARTIAL|BLOCKED|INFO)\b/gim),
610
- ];
611
- const last = matches[matches.length - 1];
612
- if (!last)
613
- return null;
614
- return last[1].toUpperCase();
615
- }
616
- /**
617
- * Map worker self-report to the task plan's status enum. Falls back to the
618
- * tool-failure heuristic ONLY when the trailer is missing — that's the only
619
- * way to recover useful state for non-compliant workers.
620
- */
621
- export function reportedToTaskStatus(reported, anyToolFailed) {
622
- if (reported === "DONE")
623
- return "done";
624
- if (reported === "INFO")
625
- return "done"; // question answered, nothing to retry
626
- if (reported === "BLOCKED")
627
- return "blocked";
628
- // UNVERIFIED / PARTIAL: keep the task as "in_progress" so the boss's next
629
- // re-prompt picks it up. Tasks-overlay shows it as the active row.
630
- if (reported === "UNVERIFIED" || reported === "PARTIAL")
631
- return "in_progress";
632
- // No trailer — last-resort heuristic.
633
- return anyToolFailed ? "blocked" : "done";
634
- }
635
- function formatEventForBoss(event, workerSnapshot, autoChainNotices) {
636
- if (event.kind === "user_message") {
637
- return event.text;
638
- }
639
- // Live worker statuses, formatted as a single trailing line so the boss
640
- // always sees who's still running. Excludes the worker the event is FROM
641
- // (the boss can read that worker's outcome from the event body itself).
642
- const renderOthers = (excludeName) => {
643
- const others = workerSnapshot
644
- .filter((w) => w.name !== excludeName)
645
- .map((w) => `${w.name}(${w.status})`)
646
- .join(" ");
647
- return others.length > 0 ? `\nother_workers: ${others}` : "";
648
- };
649
- // Auto-chain trailer — explicit per-project list so the boss can't dismiss
650
- // the trailer's "(working)" entries as stale.
651
- const renderAutoChain = () => {
652
- if (autoChainNotices.length === 0)
653
- return "";
654
- const lines = autoChainNotices.map((n) => ` - ${n.project}: "${n.title}"`);
655
- return `\nauto_dispatched_since_last_event:\n${lines.join("\n")}`;
656
- };
657
- if (event.kind === "worker_turn_complete") {
658
- const s = event.summary;
659
- const tools = s.toolsUsed.length > 0
660
- ? s.toolsUsed.map((t) => `${t.ok ? "✓" : "✗"}${t.name}`).join(", ")
661
- : "(none)";
662
- return `[event:worker_turn_complete] project="${s.project}" turn=${s.turnIndex} timestamp=${s.timestamp}
663
- tools_used: ${tools}
664
- final_text:
665
- ${s.finalText || "(empty)"}${renderOthers(s.project)}${renderAutoChain()}`;
666
- }
667
- return `[event:worker_error] project="${event.project}" timestamp=${event.timestamp}
668
- ${event.message}${renderOthers(event.project)}${renderAutoChain()}`;
669
- }
670
- /**
671
- * Total context used in tokens. Mirrors ggcoder/useAgentLoop: Anthropic counts
672
- * uncached input + cache reads/writes (output is metered separately); other
673
- * providers share a single window so output counts too.
674
- */
675
- function computeContextUsed(usage, provider) {
676
- const inputContext = (usage.inputTokens ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
677
- return provider === "anthropic" ? inputContext : inputContext + (usage.outputTokens ?? 0);
678
- }
679
- /**
680
- * Map raw provider error text to a human-friendly hint. Mirrors ggcoder's
681
- * pattern in App.tsx so users see the same diagnostic phrasing.
682
- */
683
- function formatProviderError(message) {
684
- const lower = message.toLowerCase();
685
- if (lower.includes("overloaded") || lower.includes("engine_overloaded")) {
686
- return `${message}\nHint: provider is under heavy load — try again in a moment.`;
687
- }
688
- if (lower.includes("insufficient balance") ||
689
- lower.includes("quota exceeded") ||
690
- lower.includes("recharge")) {
691
- return `${message}\nHint: billing or quota issue — check your account balance.`;
692
- }
693
- if (lower.includes("rate limit") ||
694
- lower.includes("too many requests") ||
695
- lower.includes("429")) {
696
- return `${message}\nHint: provider rate limit — wait a moment before retrying.`;
697
- }
698
- if (lower.includes("timeout") || lower.includes("timed out")) {
699
- return `${message}\nHint: provider timed out — their servers may be slow.`;
700
- }
701
- if (lower.includes("does not recognize the requested model") ||
702
- (lower.includes("model") && (lower.includes("not exist") || lower.includes("not found")))) {
703
- return `${message}\nHint: use /model to switch, or check that your account has access.`;
704
- }
705
- return message;
706
- }
707
- //# sourceMappingURL=orchestrator.js.map