macroclaw 0.8.0 → 0.9.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 +3 -3
- package/src/app.test.ts +126 -97
- package/src/claude.integration-test.ts +47 -79
- package/src/claude.test.ts +263 -213
- package/src/claude.ts +129 -97
- package/src/history.test.ts +10 -10
- package/src/history.ts +2 -2
- package/src/main.ts +3 -0
- package/src/orchestrator.test.ts +320 -197
- package/src/orchestrator.ts +172 -237
package/src/orchestrator.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
Claude,
|
|
4
|
+
QueryParseError,
|
|
5
|
+
QueryProcessError,
|
|
6
|
+
type QueryResult,
|
|
7
|
+
QueryValidationError,
|
|
8
|
+
type RunningQuery
|
|
9
|
+
} from "./claude";
|
|
10
|
+
import { writeHistoryPrompt, writeHistoryResult } from "./history";
|
|
4
11
|
import { createLogger } from "./logger";
|
|
5
|
-
import {
|
|
12
|
+
import { CRON_TIMEOUT, MAIN_TIMEOUT, SYSTEM_PROMPT } from "./prompts";
|
|
6
13
|
import { Queue } from "./queue";
|
|
7
|
-
import { loadSessions,
|
|
14
|
+
import { loadSessions, saveSessions } from "./sessions";
|
|
8
15
|
import type { ButtonSpec } from "./telegram";
|
|
9
16
|
|
|
10
17
|
const log = createLogger("orchestrator");
|
|
11
18
|
|
|
12
|
-
// --- Response schema
|
|
13
|
-
// Flat object (no oneOf/discriminatedUnion) — Claude CLI --json-schema requires a single top-level object.
|
|
19
|
+
// --- Response schema ---
|
|
14
20
|
|
|
15
21
|
const backgroundAgentSchema = z.object({
|
|
16
22
|
name: z.string().describe("Label for the background agent"),
|
|
@@ -18,7 +24,7 @@ const backgroundAgentSchema = z.object({
|
|
|
18
24
|
model: z.enum(["haiku", "sonnet", "opus"]).describe("Model to use for the background agent").optional(),
|
|
19
25
|
});
|
|
20
26
|
|
|
21
|
-
const
|
|
27
|
+
const agentOutputSchema = z.object({
|
|
22
28
|
action: z.enum(["send", "silent"]).describe("'send' to reply to the user, 'silent' to do nothing"),
|
|
23
29
|
actionReason: z.string().describe("Why the agent chose this action (logged, not sent)"),
|
|
24
30
|
message: z.string().describe("The message to send to Telegram (required when action is 'send')").optional(),
|
|
@@ -27,9 +33,11 @@ const claudeResponseSchema = z.object({
|
|
|
27
33
|
backgroundAgents: z.array(backgroundAgentSchema).describe("Background agents to spawn alongside this response").optional(),
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
type
|
|
36
|
+
type AgentOutput = z.infer<typeof agentOutputSchema>;
|
|
31
37
|
|
|
32
|
-
const
|
|
38
|
+
const responseResultType = { type: "object" as const, schema: agentOutputSchema };
|
|
39
|
+
|
|
40
|
+
const textResultType = { type: "text" } as const;
|
|
33
41
|
|
|
34
42
|
// --- Public response type ---
|
|
35
43
|
|
|
@@ -46,8 +54,7 @@ export interface OrchestratorResponse {
|
|
|
46
54
|
type OrchestratorRequest =
|
|
47
55
|
| { type: "user"; message: string; files?: string[] }
|
|
48
56
|
| { type: "cron"; name: string; prompt: string; model?: string }
|
|
49
|
-
| { type: "background-agent-result"; name: string; response:
|
|
50
|
-
| { type: "background-agent"; name: string; prompt: string; model?: string }
|
|
57
|
+
| { type: "background-agent-result"; name: string; response: AgentOutput; sessionId?: string }
|
|
51
58
|
| { type: "button"; label: string };
|
|
52
59
|
|
|
53
60
|
function escapeHtml(text: string): string {
|
|
@@ -62,8 +69,6 @@ interface BackgroundInfo {
|
|
|
62
69
|
startTime: Date;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
// --- Orchestrator ---
|
|
66
|
-
|
|
67
72
|
export interface OrchestratorConfig {
|
|
68
73
|
model?: string;
|
|
69
74
|
workspace: string;
|
|
@@ -72,45 +77,21 @@ export interface OrchestratorConfig {
|
|
|
72
77
|
claude?: Claude;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
interface BuiltRequest {
|
|
76
|
-
prompt: string;
|
|
77
|
-
model: string | undefined;
|
|
78
|
-
timeout: number;
|
|
79
|
-
files?: string[];
|
|
80
|
-
useMainSession: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface CallResult {
|
|
84
|
-
response: ClaudeResponseInternal;
|
|
85
|
-
sessionId: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
80
|
export class Orchestrator {
|
|
81
|
+
#config: Omit<OrchestratorConfig , 'claude'>;
|
|
89
82
|
#claude: Claude;
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#sessionResolved = false;
|
|
94
|
-
#config: OrchestratorConfig;
|
|
95
|
-
#active = new Map<string, BackgroundInfo>();
|
|
83
|
+
|
|
84
|
+
#mainSessionId: string | undefined;
|
|
85
|
+
#backgroundAgents = new Map<string, BackgroundInfo>();
|
|
96
86
|
#queue: Queue<OrchestratorRequest>;
|
|
97
87
|
|
|
98
88
|
constructor(config: OrchestratorConfig) {
|
|
99
89
|
this.#config = config;
|
|
100
|
-
this.#claude = config.claude ?? new Claude({ workspace: config.workspace,
|
|
101
|
-
this.#sessions = loadSessions(config.settingsDir);
|
|
90
|
+
this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: SYSTEM_PROMPT });
|
|
102
91
|
this.#queue = new Queue<OrchestratorRequest>();
|
|
103
92
|
this.#queue.setHandler((request) => this.#handleRequest(request));
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
this.#sessionId = this.#sessions.mainSessionId;
|
|
107
|
-
this.#resumeSession = true;
|
|
108
|
-
} else {
|
|
109
|
-
this.#sessionId = newSessionId();
|
|
110
|
-
this.#resumeSession = false;
|
|
111
|
-
saveSessions({ mainSessionId: this.#sessionId }, config.settingsDir);
|
|
112
|
-
log.info({ sessionId: this.#sessionId }, "Created new session");
|
|
113
|
-
}
|
|
94
|
+
this.#mainSessionId = loadSessions(config.settingsDir).mainSessionId;
|
|
114
95
|
}
|
|
115
96
|
|
|
116
97
|
// --- Public handle methods ---
|
|
@@ -134,7 +115,7 @@ export class Orchestrator {
|
|
|
134
115
|
}
|
|
135
116
|
|
|
136
117
|
handleBackgroundList(): void {
|
|
137
|
-
const agents = [...this.#
|
|
118
|
+
const agents = [...this.#backgroundAgents.values()];
|
|
138
119
|
if (agents.length === 0) {
|
|
139
120
|
this.#callOnResponse({ message: "No background agents running." });
|
|
140
121
|
return;
|
|
@@ -148,12 +129,12 @@ export class Orchestrator {
|
|
|
148
129
|
const text = `${a.name} (${elapsed}s)`.slice(0, 27);
|
|
149
130
|
return { text, data: `peek:${a.sessionId}` };
|
|
150
131
|
});
|
|
151
|
-
buttons.push("_dismiss");
|
|
132
|
+
buttons.push({text: "Dismiss", data: "_dismiss"});
|
|
152
133
|
this.#callOnResponse({ message: lines.join("\n"), buttons });
|
|
153
134
|
}
|
|
154
135
|
|
|
155
136
|
async handlePeek(sessionId: string): Promise<void> {
|
|
156
|
-
const agent = this.#
|
|
137
|
+
const agent = this.#backgroundAgents.get(sessionId);
|
|
157
138
|
if (!agent) {
|
|
158
139
|
this.#callOnResponse({ message: "Agent not found or already finished." });
|
|
159
140
|
return;
|
|
@@ -162,24 +143,21 @@ export class Orchestrator {
|
|
|
162
143
|
this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(agent.name)}</b>...` });
|
|
163
144
|
|
|
164
145
|
try {
|
|
165
|
-
const
|
|
166
|
-
resume: true,
|
|
146
|
+
const query = this.#claude.forkSession(
|
|
167
147
|
sessionId,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const text = isDeferred(result) ? "Agent is still working..." : (result.result ?? "[No output]");
|
|
175
|
-
this.#callOnResponse({ message: `<b>[${escapeHtml(agent.name)}]</b> ${text}` });
|
|
148
|
+
"Give a brief status update: what has been done so far, what's currently happening, and what's remaining. 2-3 sentences max.",
|
|
149
|
+
textResultType,
|
|
150
|
+
{ model: "haiku" },
|
|
151
|
+
);
|
|
152
|
+
const { value } = await query.result;
|
|
153
|
+
this.#callOnResponse({ message: `<b>[${escapeHtml(agent.name)}]</b> ${value || "[No output]"}` });
|
|
176
154
|
} catch (err) {
|
|
177
155
|
this.#callOnResponse({ message: `Couldn't peek at ${escapeHtml(agent.name)}: ${err}` });
|
|
178
156
|
}
|
|
179
157
|
}
|
|
180
158
|
|
|
181
159
|
handleSessionCommand(): void {
|
|
182
|
-
this.#callOnResponse({ message: `Session: <code>${this.#
|
|
160
|
+
this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
|
|
183
161
|
}
|
|
184
162
|
|
|
185
163
|
// --- Internal queue handler ---
|
|
@@ -187,39 +165,32 @@ export class Orchestrator {
|
|
|
187
165
|
async #handleRequest(request: OrchestratorRequest): Promise<void> {
|
|
188
166
|
log.debug({ type: request.type }, "Incoming request");
|
|
189
167
|
|
|
190
|
-
// Background result with matching session ID: deliver directly
|
|
191
|
-
if (request.type === "background-agent-result" &&
|
|
168
|
+
// Background result with matching session ID: deliver directly
|
|
169
|
+
if (request.type === "background-agent-result" && request.sessionId === this.#mainSessionId) {
|
|
192
170
|
log.debug({ name: request.name }, "Background result on current session, applying directly");
|
|
193
|
-
await this.#
|
|
171
|
+
await this.#deliverResponse(request.response);
|
|
194
172
|
return;
|
|
195
173
|
}
|
|
196
174
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
|
|
208
|
-
this.#adoptBackground(name, rawResponse.sessionId, startTime, rawResponse.completion.then(
|
|
209
|
-
(r) => {
|
|
210
|
-
const msg = r.structuredOutput ? String((r.structuredOutput as Record<string, unknown>).message ?? "") : (r.result ?? "");
|
|
211
|
-
return { action: "send" as const, message: msg, actionReason: "deferred-completed" };
|
|
212
|
-
},
|
|
213
|
-
(err) => ({ action: "send" as const, message: `[Error] ${err}`, actionReason: "deferred-failed" }),
|
|
214
|
-
));
|
|
215
|
-
return;
|
|
175
|
+
await writeHistoryPrompt(request);
|
|
176
|
+
|
|
177
|
+
const result = await this.#queryWithRetry(request);
|
|
178
|
+
if (!result) return;
|
|
179
|
+
|
|
180
|
+
// Update session ID (important after fork or new session)
|
|
181
|
+
if (result.sessionId !== this.#mainSessionId) {
|
|
182
|
+
log.info({ oldSessionId: this.#mainSessionId, newSessionId: result.sessionId }, "Session updated");
|
|
183
|
+
this.#mainSessionId = result.sessionId;
|
|
184
|
+
saveSessions({ mainSessionId: this.#mainSessionId }, this.#config.settingsDir);
|
|
216
185
|
}
|
|
217
186
|
|
|
218
|
-
|
|
219
|
-
await this.#
|
|
187
|
+
await writeHistoryResult(result.value);
|
|
188
|
+
await this.#deliverResponse(result.value);
|
|
220
189
|
}
|
|
221
190
|
|
|
222
|
-
|
|
191
|
+
// --- Response delivery ---
|
|
192
|
+
|
|
193
|
+
async #deliverResponse(response: AgentOutput): Promise<void> {
|
|
223
194
|
if (response.action === "send") {
|
|
224
195
|
this.#callOnResponse({
|
|
225
196
|
message: response.message || "[No output]",
|
|
@@ -245,187 +216,151 @@ export class Orchestrator {
|
|
|
245
216
|
});
|
|
246
217
|
}
|
|
247
218
|
|
|
248
|
-
// ---
|
|
219
|
+
// --- Claude calls ---
|
|
249
220
|
|
|
250
|
-
#
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
this.#active.set(sessionId, info);
|
|
254
|
-
|
|
255
|
-
log.debug({ name, sessionId }, "Starting background agent");
|
|
221
|
+
async #queryWithRetry(request: OrchestratorRequest): Promise<QueryResult<AgentOutput> | null> {
|
|
222
|
+
const timeout = request.type === "cron" ? CRON_TIMEOUT : MAIN_TIMEOUT;
|
|
223
|
+
const query = this.#query(request);
|
|
256
224
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
225
|
+
try {
|
|
226
|
+
const result = await this.#awaitOrBackground(query, request, timeout);
|
|
227
|
+
if (!result) return null;
|
|
228
|
+
return result;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// Resume failed — retry with a fresh session
|
|
231
|
+
if (err instanceof QueryProcessError && this.#mainSessionId) {
|
|
232
|
+
log.info("Resume failed, retrying with new session");
|
|
233
|
+
this.#mainSessionId = undefined;
|
|
234
|
+
const retryQuery = this.#query(request);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const retryResult = await this.#awaitOrBackground(retryQuery, request, timeout);
|
|
238
|
+
if (!retryResult) return null;
|
|
239
|
+
return retryResult;
|
|
240
|
+
} catch (retryErr) {
|
|
241
|
+
return { value: this.#errorResponse(retryErr), sessionId: retryQuery.sessionId };
|
|
269
242
|
}
|
|
270
|
-
this.#active.delete(sessionId);
|
|
271
|
-
log.debug({ name, message: response.message }, "Background agent finished");
|
|
272
|
-
this.#queue.push({ type: "background-agent-result", name, response });
|
|
273
|
-
},
|
|
274
|
-
(err) => {
|
|
275
|
-
this.#active.delete(sessionId);
|
|
276
|
-
log.error({ name, err }, "Background agent failed");
|
|
277
|
-
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
#adoptBackground(name: string, sessionId: string, startTime: Date, completion: Promise<ClaudeResponseInternal>) {
|
|
283
|
-
const info: BackgroundInfo = { name, sessionId, startTime };
|
|
284
|
-
this.#active.set(sessionId, info);
|
|
285
|
-
|
|
286
|
-
log.debug({ name, sessionId }, "Adopting backgrounded task");
|
|
287
|
-
|
|
288
|
-
// completion is pre-wrapped with .then(ok, err) at the call site, so it never rejects
|
|
289
|
-
completion.then((response) => {
|
|
290
|
-
this.#active.delete(sessionId);
|
|
291
|
-
log.debug({ name, message: response.message }, "Adopted task finished");
|
|
292
|
-
this.#queue.push({ type: "background-agent-result", name, response, sessionId });
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// --- Core Claude processing ---
|
|
297
|
-
|
|
298
|
-
async #processRequest(request: OrchestratorRequest, options?: { forkSession?: boolean }): Promise<ClaudeResponseInternal | ClaudeDeferredResult> {
|
|
299
|
-
const built = this.#buildRequest(request);
|
|
300
|
-
await logPrompt(request);
|
|
301
|
-
|
|
302
|
-
if (built.useMainSession) {
|
|
303
|
-
let result = await this.#callClaude(built, this.#resumeSession, this.#sessionId, options?.forkSession);
|
|
304
|
-
|
|
305
|
-
// Session resolution: if resume failed on first call, create new session
|
|
306
|
-
if (!isDeferred(result) && !this.#sessionResolved && this.#resumeSession && result.response.actionReason === "process-error") {
|
|
307
|
-
this.#sessionId = newSessionId();
|
|
308
|
-
log.info({ sessionId: this.#sessionId }, "Resume failed, created new session");
|
|
309
|
-
this.#resumeSession = false;
|
|
310
|
-
saveSessions({ mainSessionId: this.#sessionId }, this.#config.settingsDir);
|
|
311
|
-
result = await this.#callClaude(built, this.#resumeSession, this.#sessionId);
|
|
312
243
|
}
|
|
313
244
|
|
|
314
|
-
|
|
245
|
+
return { value: this.#errorResponse(err), sessionId: this.#mainSessionId ?? "" };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
315
248
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
saveSessions({ mainSessionId: this.#sessionId }, this.#config.settingsDir);
|
|
321
|
-
}
|
|
249
|
+
#query(request: OrchestratorRequest) {
|
|
250
|
+
const prompt = this.#formatPrompt(request);
|
|
251
|
+
const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
|
|
252
|
+
const opts = { model };
|
|
322
253
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
254
|
+
// Fork if a background agent is running on the main session
|
|
255
|
+
if (this.#mainSessionId && this.#backgroundAgents.has(this.#mainSessionId)) {
|
|
256
|
+
return this.#claude.forkSession(this.#mainSessionId, prompt, responseResultType, opts);
|
|
257
|
+
}
|
|
328
258
|
|
|
329
|
-
|
|
330
|
-
|
|
259
|
+
// Resume existing session
|
|
260
|
+
if (this.#mainSessionId) {
|
|
261
|
+
return this.#claude.resumeSession(this.#mainSessionId, prompt, responseResultType, opts);
|
|
331
262
|
}
|
|
332
263
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
const bgResult = await this.#callClaude(built, true, this.#sessionId, true);
|
|
336
|
-
if (isDeferred(bgResult)) return bgResult;
|
|
337
|
-
await logResult(bgResult.response);
|
|
338
|
-
return bgResult.response;
|
|
264
|
+
// Start fresh
|
|
265
|
+
return this.#claude.newSession(prompt, responseResultType, opts);
|
|
339
266
|
}
|
|
340
267
|
|
|
341
|
-
#
|
|
268
|
+
#formatPrompt(request: OrchestratorRequest): string {
|
|
342
269
|
switch (request.type) {
|
|
343
|
-
case "user":
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
useMainSession: true,
|
|
349
|
-
};
|
|
270
|
+
case "user": {
|
|
271
|
+
if (!request.files?.length) return request.message;
|
|
272
|
+
const prefix = request.files.map((f) => `[File: ${f}]`).join("\n");
|
|
273
|
+
return request.message ? `${prefix}\n${request.message}` : prefix;
|
|
274
|
+
}
|
|
350
275
|
case "cron":
|
|
351
|
-
return {
|
|
352
|
-
prompt: `[Context: cron/${request.name}] ${request.prompt}`,
|
|
353
|
-
model: request.model ?? this.#config.model,
|
|
354
|
-
timeout: CRON_TIMEOUT,
|
|
355
|
-
useMainSession: true,
|
|
356
|
-
};
|
|
276
|
+
return `[Context: cron/${request.name}] ${request.prompt}`;
|
|
357
277
|
case "background-agent-result":
|
|
358
|
-
return {
|
|
359
|
-
prompt: `[Context: background-result/${request.name}] ${request.response.message || "[No output]"}`,
|
|
360
|
-
model: this.#config.model,
|
|
361
|
-
timeout: MAIN_TIMEOUT,
|
|
362
|
-
useMainSession: true,
|
|
363
|
-
};
|
|
278
|
+
return `[Context: background-result/${request.name}] ${request.response.message || "[No output]"}`;
|
|
364
279
|
case "button":
|
|
365
|
-
return {
|
|
366
|
-
prompt: `[Context: button-click] User tapped "${request.label}"`,
|
|
367
|
-
model: this.#config.model,
|
|
368
|
-
timeout: MAIN_TIMEOUT,
|
|
369
|
-
useMainSession: true,
|
|
370
|
-
};
|
|
371
|
-
case "background-agent":
|
|
372
|
-
return {
|
|
373
|
-
prompt: `[Context: background-agent/${request.name}] ${request.prompt}`,
|
|
374
|
-
model: request.model ?? this.#config.model,
|
|
375
|
-
timeout: BG_TIMEOUT,
|
|
376
|
-
useMainSession: false,
|
|
377
|
-
};
|
|
280
|
+
return `[Context: button-click] User tapped "${request.label}"`;
|
|
378
281
|
}
|
|
379
282
|
}
|
|
380
283
|
|
|
381
|
-
#
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
284
|
+
async #awaitOrBackground(
|
|
285
|
+
query: RunningQuery<AgentOutput>,
|
|
286
|
+
request: OrchestratorRequest,
|
|
287
|
+
timeoutMs: number,
|
|
288
|
+
): Promise<QueryResult<AgentOutput> | null> {
|
|
289
|
+
const result = await Promise.race([
|
|
290
|
+
query.result,
|
|
291
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
if (result !== null) return result;
|
|
295
|
+
|
|
296
|
+
const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
|
|
297
|
+
: request.type === "cron" ? `cron-${request.name}`
|
|
298
|
+
: "task";
|
|
299
|
+
log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
|
|
300
|
+
this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
|
|
301
|
+
this.#adoptBackground(name, query.sessionId, query.startedAt, query.result);
|
|
302
|
+
return null;
|
|
385
303
|
}
|
|
386
304
|
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
305
|
+
#errorResponse(err: unknown): AgentOutput {
|
|
306
|
+
if (err instanceof QueryProcessError) {
|
|
307
|
+
return { action: "send", message: `[Error] Claude exited with code ${err.exitCode}:\n${err.stderr}`, actionReason: "process-error" };
|
|
308
|
+
}
|
|
309
|
+
if (err instanceof QueryParseError) {
|
|
310
|
+
return { action: "send", message: `[JSON Error] ${err.raw}`, actionReason: "json-parse-failed" };
|
|
311
|
+
}
|
|
312
|
+
if (err instanceof QueryValidationError) {
|
|
313
|
+
const rawObj = err.raw as Record<string, unknown> | null;
|
|
314
|
+
const msg = typeof rawObj?.message === "string" ? rawObj.message : JSON.stringify(err.raw);
|
|
315
|
+
return { action: "send", message: msg, actionReason: "validation-failed" };
|
|
316
|
+
}
|
|
317
|
+
throw err;
|
|
394
318
|
}
|
|
395
319
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
320
|
+
// --- Background management ---
|
|
321
|
+
|
|
322
|
+
#spawnBackground(name: string, prompt: string, model: string | undefined) {
|
|
323
|
+
const bgPrompt = `[Context: background-agent/${name}] ${prompt}`;
|
|
324
|
+
const query = this.#mainSessionId
|
|
325
|
+
? this.#claude.forkSession(this.#mainSessionId, bgPrompt, responseResultType, { model })
|
|
326
|
+
: this.#claude.newSession(bgPrompt, responseResultType, { model });
|
|
327
|
+
const sessionId = query.sessionId;
|
|
328
|
+
const info: BackgroundInfo = { name, sessionId, startTime: query.startedAt };
|
|
329
|
+
this.#backgroundAgents.set(sessionId, info);
|
|
330
|
+
|
|
331
|
+
log.debug({ name, sessionId }, "Starting background agent");
|
|
332
|
+
|
|
333
|
+
query.result.then(
|
|
334
|
+
async ({ value: response }) => {
|
|
335
|
+
this.#backgroundAgents.delete(sessionId);
|
|
336
|
+
log.debug({ name, message: response.message }, "Background agent finished");
|
|
337
|
+
this.#queue.push({ type: "background-agent-result", name, response });
|
|
338
|
+
},
|
|
339
|
+
(err) => {
|
|
340
|
+
this.#backgroundAgents.delete(sessionId);
|
|
341
|
+
log.error({ name, err }, "Background agent failed");
|
|
342
|
+
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
|
|
343
|
+
},
|
|
344
|
+
);
|
|
404
345
|
}
|
|
405
346
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
prompt: built.prompt,
|
|
410
|
-
resume,
|
|
411
|
-
sessionId: sid,
|
|
412
|
-
forkSession,
|
|
413
|
-
model: built.model,
|
|
414
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
415
|
-
timeoutMs: built.timeout,
|
|
416
|
-
});
|
|
347
|
+
#adoptBackground(name: string, sessionId: string, startTime: Date, completion: Promise<QueryResult<AgentOutput>>) {
|
|
348
|
+
const info: BackgroundInfo = { name, sessionId, startTime };
|
|
349
|
+
this.#backgroundAgents.set(sessionId, info);
|
|
417
350
|
|
|
418
|
-
|
|
351
|
+
log.debug({ name, sessionId }, "Adopting backgrounded task");
|
|
419
352
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
353
|
+
completion.then(
|
|
354
|
+
({ value: response }) => {
|
|
355
|
+
this.#backgroundAgents.delete(sessionId);
|
|
356
|
+
log.debug({ name }, "Adopted task finished");
|
|
357
|
+
this.#queue.push({ type: "background-agent-result", name, response, sessionId });
|
|
358
|
+
},
|
|
359
|
+
(err) => {
|
|
360
|
+
this.#backgroundAgents.delete(sessionId);
|
|
361
|
+
log.error({ name, err }, "Adopted task failed");
|
|
362
|
+
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" }, sessionId });
|
|
363
|
+
},
|
|
364
|
+
);
|
|
430
365
|
}
|
|
431
366
|
}
|