@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.
- package/dist/{chunk-JVQDTPYR.js → chunk-3EWLK53W.js} +18 -9
- package/dist/{chunk-JVQDTPYR.js.map → chunk-3EWLK53W.js.map} +1 -1
- package/dist/{chunk-5WNYQQPQ.js → chunk-QT366Y52.js} +4 -3
- package/dist/{chunk-5WNYQQPQ.js.map → chunk-QT366Y52.js.map} +1 -1
- package/dist/{chunk-PQAHDHVY.js → chunk-WJ4S4TOY.js} +3 -2
- package/dist/{chunk-PQAHDHVY.js.map → chunk-WJ4S4TOY.js.map} +1 -1
- package/dist/{chunk-B2WQ5E5J.js → chunk-YNWFCUMR.js} +2 -1
- package/dist/{chunk-B2WQ5E5J.js.map → chunk-YNWFCUMR.js.map} +1 -1
- package/dist/cli.js +2248 -182
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-VBUDNGEI.js → devtools-4TI4D7F2.js} +3 -2
- package/dist/{devtools-VBUDNGEI.js.map → devtools-4TI4D7F2.js.map} +1 -1
- package/dist/{dist-7DAPKZGX.js → dist-VXOVSHZ5.js} +3 -2
- package/dist/{dist-7DAPKZGX.js.map → dist-VXOVSHZ5.js.map} +1 -1
- package/dist/{ignore-3AEIALHQ.js → ignore-76P4EAAU.js} +3 -2
- package/dist/{ignore-3AEIALHQ.js.map → ignore-76P4EAAU.js.map} +1 -1
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/{out-D65DTPFZ.js → out-XEXARMKS.js} +3 -2
- package/dist/{out-D65DTPFZ.js.map → out-XEXARMKS.js.map} +1 -1
- package/dist/pixel-WPYTQADG.js +14 -0
- package/dist/{pixel-fix-ALWXCLTS.js → pixel-fix-4WGZAJ5W.js} +4 -3
- package/dist/{pixel-fix-ALWXCLTS.js.map → pixel-fix-4WGZAJ5W.js.map} +1 -1
- package/package.json +10 -11
- package/dist/audio.d.ts +0 -21
- package/dist/audio.d.ts.map +0 -1
- package/dist/audio.js +0 -231
- package/dist/audio.js.map +0 -1
- package/dist/audio.test.d.ts +0 -2
- package/dist/audio.test.d.ts.map +0 -1
- package/dist/audio.test.js +0 -13
- package/dist/audio.test.js.map +0 -1
- package/dist/auto-update.d.ts +0 -24
- package/dist/auto-update.d.ts.map +0 -1
- package/dist/auto-update.js +0 -231
- package/dist/auto-update.js.map +0 -1
- package/dist/banner.d.ts +0 -17
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js +0 -25
- package/dist/banner.js.map +0 -1
- package/dist/boss-footer.d.ts +0 -25
- package/dist/boss-footer.d.ts.map +0 -1
- package/dist/boss-footer.js +0 -107
- package/dist/boss-footer.js.map +0 -1
- package/dist/boss-phrases.d.ts +0 -9
- package/dist/boss-phrases.d.ts.map +0 -1
- package/dist/boss-phrases.js +0 -71
- package/dist/boss-phrases.js.map +0 -1
- package/dist/boss-store.d.ts +0 -245
- package/dist/boss-store.d.ts.map +0 -1
- package/dist/boss-store.js +0 -623
- package/dist/boss-store.js.map +0 -1
- package/dist/boss-system-prompt.d.ts +0 -3
- package/dist/boss-system-prompt.d.ts.map +0 -1
- package/dist/boss-system-prompt.js +0 -180
- package/dist/boss-system-prompt.js.map +0 -1
- package/dist/boss-tasks-overlay.d.ts +0 -22
- package/dist/boss-tasks-overlay.d.ts.map +0 -1
- package/dist/boss-tasks-overlay.js +0 -157
- package/dist/boss-tasks-overlay.js.map +0 -1
- package/dist/branding.d.ts +0 -32
- package/dist/branding.d.ts.map +0 -1
- package/dist/branding.js +0 -59
- package/dist/branding.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.smoke.test.d.ts +0 -2
- package/dist/cli.smoke.test.d.ts.map +0 -1
- package/dist/cli.smoke.test.js +0 -48
- package/dist/cli.smoke.test.js.map +0 -1
- package/dist/colors.d.ts +0 -14
- package/dist/colors.d.ts.map +0 -1
- package/dist/colors.js +0 -31
- package/dist/colors.js.map +0 -1
- package/dist/discover.d.ts +0 -13
- package/dist/discover.d.ts.map +0 -1
- package/dist/discover.js +0 -92
- package/dist/discover.js.map +0 -1
- package/dist/event-queue.d.ts +0 -16
- package/dist/event-queue.d.ts.map +0 -1
- package/dist/event-queue.js +0 -39
- package/dist/event-queue.js.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/link-command.d.ts +0 -2
- package/dist/link-command.d.ts.map +0 -1
- package/dist/link-command.js +0 -120
- package/dist/link-command.js.map +0 -1
- package/dist/links.d.ts +0 -11
- package/dist/links.d.ts.map +0 -1
- package/dist/links.js +0 -22
- package/dist/links.js.map +0 -1
- package/dist/logger.d.ts +0 -41
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -112
- package/dist/logger.js.map +0 -1
- package/dist/orchestrator-app.d.ts +0 -15
- package/dist/orchestrator-app.d.ts.map +0 -1
- package/dist/orchestrator-app.js +0 -599
- package/dist/orchestrator-app.js.map +0 -1
- package/dist/orchestrator.d.ts +0 -147
- package/dist/orchestrator.d.ts.map +0 -1
- package/dist/orchestrator.js +0 -707
- package/dist/orchestrator.js.map +0 -1
- package/dist/orchestrator.test.d.ts +0 -2
- package/dist/orchestrator.test.d.ts.map +0 -1
- package/dist/orchestrator.test.js +0 -55
- package/dist/orchestrator.test.js.map +0 -1
- package/dist/pixel-WB6VRJWP.js +0 -13
- package/dist/radio-picker.d.ts +0 -20
- package/dist/radio-picker.d.ts.map +0 -1
- package/dist/radio-picker.js +0 -31
- package/dist/radio-picker.js.map +0 -1
- package/dist/radio.d.ts +0 -43
- package/dist/radio.d.ts.map +0 -1
- package/dist/radio.js +0 -150
- package/dist/radio.js.map +0 -1
- package/dist/sessions.d.ts +0 -21
- package/dist/sessions.d.ts.map +0 -1
- package/dist/sessions.js +0 -122
- package/dist/sessions.js.map +0 -1
- package/dist/settings.d.ts +0 -11
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -38
- package/dist/settings.js.map +0 -1
- package/dist/slash-commands.d.ts +0 -19
- package/dist/slash-commands.d.ts.map +0 -1
- package/dist/slash-commands.js +0 -76
- package/dist/slash-commands.js.map +0 -1
- package/dist/splash.d.ts +0 -21
- package/dist/splash.d.ts.map +0 -1
- package/dist/splash.js +0 -137
- package/dist/splash.js.map +0 -1
- package/dist/task-tools.d.ts +0 -18
- package/dist/task-tools.d.ts.map +0 -1
- package/dist/task-tools.js +0 -172
- package/dist/task-tools.js.map +0 -1
- package/dist/tasks-store.d.ts +0 -66
- package/dist/tasks-store.d.ts.map +0 -1
- package/dist/tasks-store.js +0 -199
- package/dist/tasks-store.js.map +0 -1
- package/dist/tasks-store.test.d.ts +0 -2
- package/dist/tasks-store.test.d.ts.map +0 -1
- package/dist/tasks-store.test.js +0 -138
- package/dist/tasks-store.test.js.map +0 -1
- package/dist/tool-formatters.d.ts +0 -7
- package/dist/tool-formatters.d.ts.map +0 -1
- package/dist/tool-formatters.js +0 -111
- package/dist/tool-formatters.js.map +0 -1
- package/dist/tools.d.ts +0 -26
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -133
- package/dist/tools.js.map +0 -1
- package/dist/types.d.ts +0 -32
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/worker.d.ts +0 -47
- package/dist/worker.d.ts.map +0 -1
- package/dist/worker.js +0 -123
- package/dist/worker.js.map +0 -1
- /package/dist/{pixel-WB6VRJWP.js.map → pixel-WPYTQADG.js.map} +0 -0
package/dist/orchestrator.js
DELETED
|
@@ -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
|