macroclaw 0.26.0 → 0.28.0
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/package.json +1 -1
- package/src/app.test.ts +33 -71
- package/src/app.ts +14 -14
- package/src/claude.ts +1 -1
- package/src/index.ts +4 -0
- package/src/naming.test.ts +44 -0
- package/src/naming.ts +54 -0
- package/src/orchestrator.test.ts +253 -335
- package/src/orchestrator.ts +213 -169
- package/src/prompts.test.ts +255 -7
- package/src/prompts.ts +137 -21
- package/src/scheduler.test.ts +9 -6
- package/src/scheduler.ts +10 -3
package/src/orchestrator.ts
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
Claude,
|
|
4
4
|
QueryParseError,
|
|
5
5
|
QueryProcessError,
|
|
6
|
-
type QueryResult,
|
|
7
6
|
QueryValidationError,
|
|
8
7
|
type RunningQuery
|
|
9
8
|
} from "./claude";
|
|
10
9
|
import { writeHistoryPrompt, writeHistoryResult } from "./history";
|
|
11
10
|
import { createLogger } from "./logger";
|
|
12
|
-
import {
|
|
11
|
+
import { generateName } from "./naming";
|
|
12
|
+
import { buildEvent, type EventInput, SYSTEM_PROMPT } from "./prompts";
|
|
13
13
|
import { Queue } from "./queue";
|
|
14
14
|
import { loadSessions, saveSessions } from "./sessions";
|
|
15
15
|
|
|
@@ -17,6 +17,10 @@ type ButtonSpec = string | { text: string; data: string };
|
|
|
17
17
|
|
|
18
18
|
const log = createLogger("orchestrator");
|
|
19
19
|
|
|
20
|
+
// --- Constants ---
|
|
21
|
+
|
|
22
|
+
const WAIT_THRESHOLD = 60_000;
|
|
23
|
+
|
|
20
24
|
// --- Response schema ---
|
|
21
25
|
|
|
22
26
|
const backgroundAgentSchema = z.object({
|
|
@@ -55,21 +59,21 @@ export interface OrchestratorResponse {
|
|
|
55
59
|
|
|
56
60
|
type OrchestratorRequest =
|
|
57
61
|
| { type: "user"; message: string; files?: string[] }
|
|
58
|
-
| { type: "
|
|
59
|
-
| { type: "background-agent-result"; name: string; response: AgentOutput; sessionId?: string }
|
|
62
|
+
| { type: "background-agent-result"; name: string; response: AgentOutput }
|
|
60
63
|
| { type: "button"; label: string };
|
|
61
64
|
|
|
62
65
|
function escapeHtml(text: string): string {
|
|
63
66
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
// ---
|
|
69
|
+
// --- Session tracking ---
|
|
67
70
|
|
|
68
|
-
interface
|
|
71
|
+
interface SessionInfo {
|
|
69
72
|
name: string;
|
|
70
73
|
prompt: string;
|
|
71
74
|
model?: string;
|
|
72
75
|
query: RunningQuery<AgentOutput>;
|
|
76
|
+
lastMessageAt: Date;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
export interface OrchestratorConfig {
|
|
@@ -78,19 +82,23 @@ export interface OrchestratorConfig {
|
|
|
78
82
|
settingsDir?: string;
|
|
79
83
|
onResponse: (response: OrchestratorResponse) => Promise<void>;
|
|
80
84
|
claude?: Claude;
|
|
85
|
+
/** How long to wait for a running main session before demoting it (ms). Default: 60000 */
|
|
86
|
+
waitThreshold?: number;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
export class Orchestrator {
|
|
84
90
|
#config: Omit<OrchestratorConfig , 'claude'>;
|
|
85
91
|
#claude: Claude;
|
|
92
|
+
#waitThreshold: number;
|
|
86
93
|
|
|
87
94
|
#mainSessionId: string | undefined;
|
|
88
|
-
#
|
|
95
|
+
#runningSessions = new Map<string, SessionInfo>();
|
|
89
96
|
#queue: Queue<OrchestratorRequest>;
|
|
90
97
|
|
|
91
98
|
constructor(config: OrchestratorConfig) {
|
|
92
99
|
this.#config = config;
|
|
93
100
|
this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: SYSTEM_PROMPT });
|
|
101
|
+
this.#waitThreshold = config.waitThreshold ?? WAIT_THRESHOLD;
|
|
94
102
|
this.#queue = new Queue<OrchestratorRequest>();
|
|
95
103
|
this.#queue.setHandler((request) => this.#handleRequest(request));
|
|
96
104
|
|
|
@@ -107,48 +115,60 @@ export class Orchestrator {
|
|
|
107
115
|
this.#queue.push({ type: "button", label });
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
handleCron(name: string, prompt: string, model?: string): void {
|
|
111
|
-
|
|
118
|
+
handleCron(name: string, prompt: string, model?: string, missed?: { missedBy: string; scheduledAt: string }): void {
|
|
119
|
+
const cronName = `cron-${name}`;
|
|
120
|
+
const formatted = buildEvent({
|
|
121
|
+
name: cronName,
|
|
122
|
+
type: "schedule-trigger",
|
|
123
|
+
session: "background",
|
|
124
|
+
schedule: { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
|
|
125
|
+
text: prompt,
|
|
126
|
+
});
|
|
127
|
+
this.#spawnBackgroundRaw(cronName, prompt, formatted, model ?? this.#config.model);
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
handleBackgroundCommand(prompt: string): void {
|
|
115
|
-
const name = prompt
|
|
131
|
+
const name = generateName(prompt);
|
|
116
132
|
this.#spawnBackground(name, prompt, this.#config.model);
|
|
117
133
|
this.#callOnResponse({ message: `Background agent "${escapeHtml(name)}" started.` });
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
this.#callOnResponse({ message: "No
|
|
136
|
+
handleSessions(): void {
|
|
137
|
+
const sessions = [...this.#runningSessions.entries()];
|
|
138
|
+
if (sessions.length === 0) {
|
|
139
|
+
this.#callOnResponse({ message: "No running sessions." });
|
|
124
140
|
return;
|
|
125
141
|
}
|
|
126
|
-
const lines =
|
|
127
|
-
const elapsed = Math.round((Date.now() -
|
|
128
|
-
|
|
142
|
+
const lines = sessions.map(([sid, s]) => {
|
|
143
|
+
const elapsed = Math.round((Date.now() - s.query.startedAt.getTime()) / 1000);
|
|
144
|
+
const isMain = sid === this.#mainSessionId;
|
|
145
|
+
return isMain
|
|
146
|
+
? `▶ ${escapeHtml(s.name)} (${elapsed}s) [main]`
|
|
147
|
+
: `- ${escapeHtml(s.name)} (${elapsed}s)`;
|
|
129
148
|
});
|
|
130
|
-
const buttons: ButtonSpec[] =
|
|
131
|
-
const elapsed = Math.round((Date.now() -
|
|
132
|
-
const text = `${
|
|
133
|
-
return { text, data: `detail:${
|
|
149
|
+
const buttons: ButtonSpec[] = sessions.map(([sid, s]) => {
|
|
150
|
+
const elapsed = Math.round((Date.now() - s.query.startedAt.getTime()) / 1000);
|
|
151
|
+
const text = `${s.name} (${elapsed}s)`.slice(0, 27);
|
|
152
|
+
return { text, data: `detail:${sid}` };
|
|
134
153
|
});
|
|
135
|
-
buttons.push({text: "Dismiss", data: "_dismiss"});
|
|
154
|
+
buttons.push({ text: "Dismiss", data: "_dismiss" });
|
|
136
155
|
this.#callOnResponse({ message: lines.join("\n"), buttons });
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
handleDetail(sessionId: string): void {
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
this.#callOnResponse({ message: "
|
|
159
|
+
const session = this.#runningSessions.get(sessionId);
|
|
160
|
+
if (!session) {
|
|
161
|
+
this.#callOnResponse({ message: "Session not found or already finished." });
|
|
143
162
|
return;
|
|
144
163
|
}
|
|
145
164
|
|
|
146
|
-
const elapsed = Math.round((Date.now() -
|
|
147
|
-
const truncatedPrompt =
|
|
165
|
+
const elapsed = Math.round((Date.now() - session.query.startedAt.getTime()) / 1000);
|
|
166
|
+
const truncatedPrompt = session.prompt.length > 300 ? `${session.prompt.slice(0, 300)}…` : session.prompt;
|
|
167
|
+
const isMain = sessionId === this.#mainSessionId;
|
|
148
168
|
const lines = [
|
|
149
|
-
`<b>${escapeHtml(
|
|
169
|
+
`<b>${escapeHtml(session.name)}</b>${isMain ? " [main]" : ""}`,
|
|
150
170
|
`Prompt: ${escapeHtml(truncatedPrompt)}`,
|
|
151
|
-
`Model: ${
|
|
171
|
+
`Model: ${session.model ?? "default"}`,
|
|
152
172
|
`Elapsed: ${elapsed}s`,
|
|
153
173
|
"Status: running",
|
|
154
174
|
];
|
|
@@ -161,82 +181,98 @@ export class Orchestrator {
|
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
async handlePeek(sessionId: string): Promise<void> {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
166
|
-
this.#callOnResponse({ message: "
|
|
184
|
+
const session = this.#runningSessions.get(sessionId);
|
|
185
|
+
if (!session) {
|
|
186
|
+
this.#callOnResponse({ message: "Session not found or already finished." });
|
|
167
187
|
return;
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(
|
|
190
|
+
this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(session.name)}</b>...` });
|
|
171
191
|
|
|
172
192
|
try {
|
|
173
|
-
const
|
|
193
|
+
const prompt = buildEvent({
|
|
194
|
+
name: `peek-${session.name}`,
|
|
195
|
+
type: "peek",
|
|
196
|
+
session: "background",
|
|
197
|
+
targetEvent: session.name,
|
|
198
|
+
instructions: `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
|
|
199
|
+
});
|
|
174
200
|
const query = this.#claude.forkSession(
|
|
175
201
|
sessionId,
|
|
176
|
-
|
|
202
|
+
prompt,
|
|
177
203
|
textResultType,
|
|
178
204
|
{ model: "haiku" },
|
|
179
205
|
);
|
|
180
206
|
const { value } = await query.result;
|
|
181
|
-
this.#callOnResponse({ message: `<b>[${escapeHtml(
|
|
207
|
+
this.#callOnResponse({ message: `<b>[${escapeHtml(session.name)}]</b> ${value || "[No output]"}` });
|
|
182
208
|
} catch (err) {
|
|
183
|
-
this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(
|
|
209
|
+
this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(session.name)}: ${err}` });
|
|
184
210
|
}
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
async handleKill(sessionId: string): Promise<void> {
|
|
188
|
-
const
|
|
189
|
-
if (!
|
|
190
|
-
this.#callOnResponse({ message: "
|
|
214
|
+
const session = this.#runningSessions.get(sessionId);
|
|
215
|
+
if (!session) {
|
|
216
|
+
this.#callOnResponse({ message: "Session not found or already finished." });
|
|
191
217
|
return;
|
|
192
218
|
}
|
|
193
219
|
|
|
194
|
-
this.#
|
|
220
|
+
this.#runningSessions.delete(sessionId);
|
|
195
221
|
|
|
196
222
|
try {
|
|
197
|
-
await
|
|
223
|
+
await session.query.kill();
|
|
198
224
|
} catch (err) {
|
|
199
|
-
log.error({ err, name:
|
|
225
|
+
log.error({ err, name: session.name }, "Kill failed");
|
|
200
226
|
}
|
|
201
227
|
|
|
202
|
-
this.#callOnResponse({ message: `Killed <b>${escapeHtml(
|
|
228
|
+
this.#callOnResponse({ message: `Killed <b>${escapeHtml(session.name)}</b>.` });
|
|
203
229
|
}
|
|
204
230
|
|
|
205
|
-
handleSessionCommand(): void {
|
|
206
|
-
this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
|
|
207
|
-
}
|
|
208
231
|
|
|
209
232
|
// --- Internal queue handler ---
|
|
210
233
|
|
|
211
234
|
async #handleRequest(request: OrchestratorRequest): Promise<void> {
|
|
212
235
|
log.debug({ type: request.type }, "Incoming request");
|
|
213
236
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
237
|
+
const mainInfo = this.#mainSessionId ? this.#runningSessions.get(this.#mainSessionId) : undefined;
|
|
238
|
+
let movedToBackground: string | undefined;
|
|
239
|
+
|
|
240
|
+
if (mainInfo) {
|
|
241
|
+
const elapsed = Date.now() - mainInfo.lastMessageAt.getTime();
|
|
242
|
+
if (elapsed >= this.#waitThreshold) {
|
|
243
|
+
// Main has been running too long — move to background immediately
|
|
244
|
+
log.info({ name: mainInfo.name, sessionId: mainInfo.query.sessionId }, "Moving main session to background (exceeded threshold)");
|
|
245
|
+
movedToBackground = mainInfo.prompt;
|
|
246
|
+
} else {
|
|
247
|
+
// Main started recently — wait for it to finish or threshold
|
|
248
|
+
const remaining = this.#waitThreshold - elapsed;
|
|
249
|
+
const finished = await Promise.race([
|
|
250
|
+
mainInfo.query.result.then(() => true as const, () => true as const),
|
|
251
|
+
new Promise<false>((r) => setTimeout(() => r(false), remaining)),
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
if (!finished) {
|
|
255
|
+
log.info({ name: mainInfo.name, sessionId: mainInfo.query.sessionId }, "Moving main session to background (wait timed out)");
|
|
256
|
+
movedToBackground = mainInfo.prompt;
|
|
257
|
+
}
|
|
258
|
+
// If finished: completion handler already delivered the result and removed from map.
|
|
259
|
+
}
|
|
219
260
|
}
|
|
220
261
|
|
|
221
262
|
await writeHistoryPrompt(request);
|
|
222
263
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (result.sessionId !== this.#mainSessionId) {
|
|
228
|
-
log.info({ oldSessionId: this.#mainSessionId, newSessionId: result.sessionId }, "Session updated");
|
|
229
|
-
this.#mainSessionId = result.sessionId;
|
|
230
|
-
saveSessions({ mainSessionId: this.#mainSessionId }, this.#config.settingsDir);
|
|
231
|
-
}
|
|
264
|
+
const label = Orchestrator.#requestLabel(request);
|
|
265
|
+
const name = generateName(label);
|
|
266
|
+
const backgroundedName = movedToBackground ? mainInfo?.name : undefined;
|
|
267
|
+
const prompt = this.#formatPrompt(request, name, backgroundedName);
|
|
232
268
|
|
|
233
|
-
|
|
234
|
-
await this.#deliverResponse(result.value);
|
|
269
|
+
this.#startMainQuery(name, prompt, this.#config.model);
|
|
235
270
|
}
|
|
236
271
|
|
|
237
272
|
// --- Response delivery ---
|
|
238
273
|
|
|
239
274
|
async #deliverResponse(response: AgentOutput): Promise<void> {
|
|
275
|
+
await writeHistoryResult(response);
|
|
240
276
|
if (response.action === "send") {
|
|
241
277
|
this.#callOnResponse({
|
|
242
278
|
message: response.message || "[No output]",
|
|
@@ -262,92 +298,111 @@ export class Orchestrator {
|
|
|
262
298
|
});
|
|
263
299
|
}
|
|
264
300
|
|
|
265
|
-
// ---
|
|
266
|
-
|
|
267
|
-
async #queryWithRetry(request: OrchestratorRequest): Promise<QueryResult<AgentOutput> | null> {
|
|
268
|
-
const timeout = request.type === "cron" ? CRON_TIMEOUT : MAIN_TIMEOUT;
|
|
269
|
-
const query = this.#query(request);
|
|
301
|
+
// --- Main session query ---
|
|
270
302
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return result;
|
|
275
|
-
} catch (err) {
|
|
276
|
-
// Resume failed — retry with a fresh session
|
|
277
|
-
if (err instanceof QueryProcessError && this.#mainSessionId) {
|
|
278
|
-
log.info("Resume failed, retrying with new session");
|
|
279
|
-
this.#mainSessionId = undefined;
|
|
280
|
-
const retryQuery = this.#query(request);
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const retryResult = await this.#awaitOrBackground(retryQuery, request, timeout);
|
|
284
|
-
if (!retryResult) return null;
|
|
285
|
-
return retryResult;
|
|
286
|
-
} catch (retryErr) {
|
|
287
|
-
return { value: this.#errorResponse(retryErr), sessionId: retryQuery.sessionId };
|
|
288
|
-
}
|
|
289
|
-
}
|
|
303
|
+
#startMainQuery(name: string, prompt: string, model: string | undefined): void {
|
|
304
|
+
const opts = { model };
|
|
305
|
+
let query: RunningQuery<AgentOutput>;
|
|
290
306
|
|
|
291
|
-
|
|
307
|
+
if (this.#mainSessionId && this.#runningSessions.has(this.#mainSessionId)) {
|
|
308
|
+
query = this.#claude.forkSession(this.#mainSessionId, prompt, responseResultType, opts);
|
|
309
|
+
} else if (this.#mainSessionId) {
|
|
310
|
+
query = this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
|
|
311
|
+
} else {
|
|
312
|
+
query = this.#claude.newSession(prompt, responseResultType, opts);
|
|
292
313
|
}
|
|
293
|
-
}
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
|
|
298
|
-
const opts = { model };
|
|
315
|
+
const sid = query.sessionId;
|
|
316
|
+
this.#runningSessions.set(sid, { name, prompt, model, query, lastMessageAt: new Date() });
|
|
299
317
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
318
|
+
if (sid !== this.#mainSessionId) {
|
|
319
|
+
log.info({ oldSessionId: this.#mainSessionId, newSessionId: sid }, "Session updated");
|
|
320
|
+
this.#mainSessionId = sid;
|
|
321
|
+
saveSessions({ mainSessionId: sid }, this.#config.settingsDir);
|
|
303
322
|
}
|
|
304
323
|
|
|
305
|
-
|
|
306
|
-
if (this.#mainSessionId) {
|
|
307
|
-
return this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
|
|
308
|
-
}
|
|
324
|
+
log.debug({ name, sessionId: sid }, "Main query started");
|
|
309
325
|
|
|
310
|
-
|
|
311
|
-
|
|
326
|
+
query.result.then(
|
|
327
|
+
async ({ value: response }) => {
|
|
328
|
+
if (!this.#runningSessions.has(sid)) {
|
|
329
|
+
log.error({ name, sessionId: sid }, "Completed session not in runningSessions — delivering anyway");
|
|
330
|
+
await this.#deliverResponse(response);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.#runningSessions.delete(sid);
|
|
334
|
+
|
|
335
|
+
if (sid === this.#mainSessionId) {
|
|
336
|
+
log.debug({ name, sessionId: sid }, "Main query finished, delivering directly");
|
|
337
|
+
await this.#deliverResponse(response);
|
|
338
|
+
} else {
|
|
339
|
+
log.debug({ name, sessionId: sid }, "Non-main query finished, feeding to main session");
|
|
340
|
+
this.#queue.push({ type: "background-agent-result", name, response });
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async (err) => {
|
|
344
|
+
if (!this.#runningSessions.has(sid)) {
|
|
345
|
+
log.error({ name, sessionId: sid, err }, "Failed session not in runningSessions — delivering error");
|
|
346
|
+
} else {
|
|
347
|
+
this.#runningSessions.delete(sid);
|
|
348
|
+
log.error({ name, sessionId: sid, err }, "Main query failed");
|
|
349
|
+
}
|
|
350
|
+
await this.#deliverResponse(this.#errorResponse(err));
|
|
351
|
+
},
|
|
352
|
+
);
|
|
312
353
|
}
|
|
313
354
|
|
|
314
|
-
#formatPrompt(request: OrchestratorRequest): string {
|
|
355
|
+
#formatPrompt(request: OrchestratorRequest, name: string, backgroundedEvent?: string): string {
|
|
356
|
+
let input: EventInput;
|
|
357
|
+
|
|
315
358
|
switch (request.type) {
|
|
316
|
-
case "user":
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
359
|
+
case "user":
|
|
360
|
+
input = {
|
|
361
|
+
name,
|
|
362
|
+
type: "user-message",
|
|
363
|
+
session: "main",
|
|
364
|
+
text: request.message || undefined,
|
|
365
|
+
files: request.files,
|
|
366
|
+
backgroundedEvent,
|
|
367
|
+
};
|
|
368
|
+
break;
|
|
323
369
|
case "background-agent-result":
|
|
324
|
-
|
|
370
|
+
input = {
|
|
371
|
+
name,
|
|
372
|
+
type: "background-agent-result",
|
|
373
|
+
session: "main",
|
|
374
|
+
originalEvent: request.name,
|
|
375
|
+
result: {
|
|
376
|
+
text: request.response.message || "[No output]",
|
|
377
|
+
files: request.response.files,
|
|
378
|
+
},
|
|
379
|
+
backgroundedEvent,
|
|
380
|
+
instructions: "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
|
|
381
|
+
};
|
|
382
|
+
break;
|
|
325
383
|
case "button":
|
|
326
|
-
|
|
384
|
+
input = {
|
|
385
|
+
name,
|
|
386
|
+
type: "button-click",
|
|
387
|
+
session: "main",
|
|
388
|
+
button: request.label,
|
|
389
|
+
backgroundedEvent,
|
|
390
|
+
};
|
|
391
|
+
break;
|
|
327
392
|
}
|
|
393
|
+
|
|
394
|
+
return buildEvent(input);
|
|
328
395
|
}
|
|
329
396
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (result !== null) return result;
|
|
341
|
-
|
|
342
|
-
const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
|
|
343
|
-
: request.type === "cron" ? `cron-${request.name}`
|
|
344
|
-
: "task";
|
|
345
|
-
const prompt = this.#formatPrompt(request);
|
|
346
|
-
const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
|
|
347
|
-
log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
|
|
348
|
-
this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
|
|
349
|
-
this.#adoptBackground(name, prompt, model, query);
|
|
350
|
-
return null;
|
|
397
|
+
static #requestLabel(request: OrchestratorRequest): string {
|
|
398
|
+
switch (request.type) {
|
|
399
|
+
case "user":
|
|
400
|
+
return request.message;
|
|
401
|
+
case "background-agent-result":
|
|
402
|
+
return `bg:${request.name}`;
|
|
403
|
+
case "button":
|
|
404
|
+
return `btn:${request.label}`;
|
|
405
|
+
}
|
|
351
406
|
}
|
|
352
407
|
|
|
353
408
|
#errorResponse(err: unknown): AgentOutput {
|
|
@@ -368,51 +423,40 @@ export class Orchestrator {
|
|
|
368
423
|
// --- Background management ---
|
|
369
424
|
|
|
370
425
|
#spawnBackground(name: string, prompt: string, model: string | undefined) {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.#
|
|
378
|
-
|
|
379
|
-
log.debug({ name, sessionId }, "Starting background agent");
|
|
426
|
+
const formatted = buildEvent({
|
|
427
|
+
name,
|
|
428
|
+
type: "background-agent-start",
|
|
429
|
+
session: "background",
|
|
430
|
+
text: prompt,
|
|
431
|
+
});
|
|
432
|
+
this.#spawnBackgroundRaw(name, prompt, formatted, model);
|
|
433
|
+
}
|
|
380
434
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
this.#queue.push({ type: "background-agent-result", name, response });
|
|
387
|
-
},
|
|
388
|
-
(err) => {
|
|
389
|
-
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
390
|
-
this.#backgroundAgents.delete(sessionId);
|
|
391
|
-
log.error({ name, err }, "Background agent failed");
|
|
392
|
-
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
|
|
393
|
-
},
|
|
394
|
-
);
|
|
435
|
+
#spawnBackgroundRaw(name: string, prompt: string, formatted: string, model: string | undefined) {
|
|
436
|
+
const query = this.#mainSessionId
|
|
437
|
+
? this.#claude.forkSession(this.#mainSessionId, formatted, responseResultType, { model })
|
|
438
|
+
: this.#claude.newSession(formatted, responseResultType, { model });
|
|
439
|
+
this.#registerBackground(name, prompt, model, query);
|
|
395
440
|
}
|
|
396
441
|
|
|
397
|
-
#
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
this.#backgroundAgents.set(sessionId, info);
|
|
442
|
+
#registerBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
|
|
443
|
+
const sid = query.sessionId;
|
|
444
|
+
this.#runningSessions.set(sid, { name, prompt, model, query, lastMessageAt: new Date() });
|
|
401
445
|
|
|
402
|
-
log.debug({ name, sessionId }, "
|
|
446
|
+
log.debug({ name, sessionId: sid }, "Background session registered");
|
|
403
447
|
|
|
404
448
|
query.result.then(
|
|
405
449
|
({ value: response }) => {
|
|
406
|
-
if (!this.#
|
|
407
|
-
this.#
|
|
408
|
-
log.debug({ name }, "
|
|
409
|
-
this.#queue.push({ type: "background-agent-result", name, response
|
|
450
|
+
if (!this.#runningSessions.has(sid)) return;
|
|
451
|
+
this.#runningSessions.delete(sid);
|
|
452
|
+
log.debug({ name, message: response.message }, "Background session finished");
|
|
453
|
+
this.#queue.push({ type: "background-agent-result", name, response });
|
|
410
454
|
},
|
|
411
455
|
(err) => {
|
|
412
|
-
if (!this.#
|
|
413
|
-
this.#
|
|
414
|
-
log.error({ name, err }, "
|
|
415
|
-
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "
|
|
456
|
+
if (!this.#runningSessions.has(sid)) return;
|
|
457
|
+
this.#runningSessions.delete(sid);
|
|
458
|
+
log.error({ name, err }, "Background session failed");
|
|
459
|
+
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-failed" } });
|
|
416
460
|
},
|
|
417
461
|
);
|
|
418
462
|
}
|