@rallycry/conveyor-agent 2.18.0 → 3.0.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/dist/{chunk-I7ETGLPA.js → chunk-5UYKPYFQ.js} +833 -557
- package/dist/chunk-5UYKPYFQ.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +55 -302
- package/dist/index.js +2 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-I7ETGLPA.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/connection.ts
|
|
1
|
+
// src/connection/task-connection.ts
|
|
2
2
|
import { io } from "socket.io-client";
|
|
3
3
|
var ConveyorConnection = class _ConveyorConnection {
|
|
4
4
|
socket = null;
|
|
@@ -8,8 +8,10 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
8
8
|
static EVENT_BATCH_MS = 500;
|
|
9
9
|
earlyMessages = [];
|
|
10
10
|
earlyStop = false;
|
|
11
|
+
earlyModeChanges = [];
|
|
11
12
|
chatMessageCallback = null;
|
|
12
13
|
stopCallback = null;
|
|
14
|
+
modeChangeCallback = null;
|
|
13
15
|
pendingQuestionResolvers = /* @__PURE__ */ new Map();
|
|
14
16
|
constructor(config) {
|
|
15
17
|
this.config = config;
|
|
@@ -31,16 +33,13 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
31
33
|
"ngrok-skip-browser-warning": "true"
|
|
32
34
|
}
|
|
33
35
|
});
|
|
34
|
-
this.socket.on(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else {
|
|
40
|
-
this.earlyMessages.push(msg);
|
|
41
|
-
}
|
|
36
|
+
this.socket.on("agentRunner:incomingMessage", (msg) => {
|
|
37
|
+
if (this.chatMessageCallback) {
|
|
38
|
+
this.chatMessageCallback(msg);
|
|
39
|
+
} else {
|
|
40
|
+
this.earlyMessages.push(msg);
|
|
42
41
|
}
|
|
43
|
-
);
|
|
42
|
+
});
|
|
44
43
|
this.socket.on("agentRunner:stop", () => {
|
|
45
44
|
if (this.stopCallback) {
|
|
46
45
|
this.stopCallback();
|
|
@@ -48,16 +47,20 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
48
47
|
this.earlyStop = true;
|
|
49
48
|
}
|
|
50
49
|
});
|
|
51
|
-
this.socket.on(
|
|
52
|
-
|
|
53
|
-
(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.pendingQuestionResolvers.delete(data.requestId);
|
|
57
|
-
resolver(data.answers);
|
|
58
|
-
}
|
|
50
|
+
this.socket.on("agentRunner:questionAnswer", (data) => {
|
|
51
|
+
const resolver = this.pendingQuestionResolvers.get(data.requestId);
|
|
52
|
+
if (resolver) {
|
|
53
|
+
this.pendingQuestionResolvers.delete(data.requestId);
|
|
54
|
+
resolver(data.answers);
|
|
59
55
|
}
|
|
60
|
-
);
|
|
56
|
+
});
|
|
57
|
+
this.socket.on("agentRunner:setMode", (data) => {
|
|
58
|
+
if (this.modeChangeCallback) {
|
|
59
|
+
this.modeChangeCallback(data);
|
|
60
|
+
} else {
|
|
61
|
+
this.earlyModeChanges.push(data);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
61
64
|
this.socket.on("connect", () => {
|
|
62
65
|
if (!settled) {
|
|
63
66
|
settled = true;
|
|
@@ -79,7 +82,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
79
82
|
return new Promise((resolve2, reject) => {
|
|
80
83
|
socket.emit(
|
|
81
84
|
"agentRunner:getChatMessages",
|
|
82
|
-
{
|
|
85
|
+
{ limit },
|
|
83
86
|
(response) => {
|
|
84
87
|
if (response.success && response.data) {
|
|
85
88
|
resolve2(response.data);
|
|
@@ -96,7 +99,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
96
99
|
return new Promise((resolve2, reject) => {
|
|
97
100
|
socket.emit(
|
|
98
101
|
"agentRunner:getTaskFiles",
|
|
99
|
-
{
|
|
102
|
+
{},
|
|
100
103
|
(response) => {
|
|
101
104
|
if (response.success && response.data) {
|
|
102
105
|
resolve2(response.data);
|
|
@@ -113,7 +116,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
113
116
|
return new Promise((resolve2, reject) => {
|
|
114
117
|
socket.emit(
|
|
115
118
|
"agentRunner:getTaskFile",
|
|
116
|
-
{
|
|
119
|
+
{ fileId },
|
|
117
120
|
(response) => {
|
|
118
121
|
if (response.success && response.data) {
|
|
119
122
|
resolve2(response.data);
|
|
@@ -130,7 +133,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
130
133
|
return new Promise((resolve2, reject) => {
|
|
131
134
|
socket.emit(
|
|
132
135
|
"agentRunner:getTaskContext",
|
|
133
|
-
{
|
|
136
|
+
{},
|
|
134
137
|
(response) => {
|
|
135
138
|
if (response.success && response.data) {
|
|
136
139
|
resolve2(response.data);
|
|
@@ -143,7 +146,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
143
146
|
}
|
|
144
147
|
sendEvent(event) {
|
|
145
148
|
if (!this.socket) throw new Error("Not connected");
|
|
146
|
-
this.eventBuffer.push({
|
|
149
|
+
this.eventBuffer.push({ event });
|
|
147
150
|
if (!this.flushTimer) {
|
|
148
151
|
this.flushTimer = setTimeout(() => this.flushEvents(), _ConveyorConnection.EVENT_BATCH_MS);
|
|
149
152
|
}
|
|
@@ -161,17 +164,11 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
161
164
|
}
|
|
162
165
|
updateStatus(status) {
|
|
163
166
|
if (!this.socket) throw new Error("Not connected");
|
|
164
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
165
|
-
taskId: this.config.taskId,
|
|
166
|
-
status
|
|
167
|
-
});
|
|
167
|
+
this.socket.emit("agentRunner:statusUpdate", { status });
|
|
168
168
|
}
|
|
169
169
|
postChatMessage(content) {
|
|
170
170
|
if (!this.socket) throw new Error("Not connected");
|
|
171
|
-
this.socket.emit("agentRunner:chatMessage", {
|
|
172
|
-
taskId: this.config.taskId,
|
|
173
|
-
content
|
|
174
|
-
});
|
|
171
|
+
this.socket.emit("agentRunner:chatMessage", { content });
|
|
175
172
|
}
|
|
176
173
|
createPR(params) {
|
|
177
174
|
const socket = this.socket;
|
|
@@ -179,7 +176,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
179
176
|
return new Promise((resolve2, reject) => {
|
|
180
177
|
socket.emit(
|
|
181
178
|
"agentRunner:createPR",
|
|
182
|
-
|
|
179
|
+
params,
|
|
183
180
|
(response) => {
|
|
184
181
|
if (response.success && response.data) {
|
|
185
182
|
resolve2(response.data);
|
|
@@ -192,11 +189,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
192
189
|
}
|
|
193
190
|
askUserQuestion(requestId, questions) {
|
|
194
191
|
if (!this.socket) throw new Error("Not connected");
|
|
195
|
-
this.socket.emit("agentRunner:askUserQuestion", {
|
|
196
|
-
taskId: this.config.taskId,
|
|
197
|
-
requestId,
|
|
198
|
-
questions
|
|
199
|
-
});
|
|
192
|
+
this.socket.emit("agentRunner:askUserQuestion", { requestId, questions });
|
|
200
193
|
return new Promise((resolve2) => {
|
|
201
194
|
this.pendingQuestionResolvers.set(requestId, resolve2);
|
|
202
195
|
});
|
|
@@ -206,17 +199,11 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
206
199
|
}
|
|
207
200
|
storeSessionId(sessionId) {
|
|
208
201
|
if (!this.socket) return;
|
|
209
|
-
this.socket.emit("agentRunner:storeSessionId", {
|
|
210
|
-
taskId: this.config.taskId,
|
|
211
|
-
sessionId
|
|
212
|
-
});
|
|
202
|
+
this.socket.emit("agentRunner:storeSessionId", { sessionId });
|
|
213
203
|
}
|
|
214
204
|
updateTaskFields(fields) {
|
|
215
205
|
if (!this.socket) throw new Error("Not connected");
|
|
216
|
-
this.socket.emit("agentRunner:updateTaskFields", {
|
|
217
|
-
taskId: this.config.taskId,
|
|
218
|
-
fields
|
|
219
|
-
});
|
|
206
|
+
this.socket.emit("agentRunner:updateTaskFields", { fields });
|
|
220
207
|
}
|
|
221
208
|
onChatMessage(callback) {
|
|
222
209
|
this.chatMessageCallback = callback;
|
|
@@ -232,25 +219,28 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
232
219
|
this.earlyStop = false;
|
|
233
220
|
}
|
|
234
221
|
}
|
|
222
|
+
onModeChange(callback) {
|
|
223
|
+
this.modeChangeCallback = callback;
|
|
224
|
+
for (const data of this.earlyModeChanges) {
|
|
225
|
+
callback(data);
|
|
226
|
+
}
|
|
227
|
+
this.earlyModeChanges = [];
|
|
228
|
+
}
|
|
229
|
+
emitModeChanged(mode) {
|
|
230
|
+
if (!this.socket) return;
|
|
231
|
+
this.socket.emit("agentRunner:modeChanged", { mode });
|
|
232
|
+
}
|
|
235
233
|
trackSpending(params) {
|
|
236
234
|
if (!this.socket) throw new Error("Not connected");
|
|
237
|
-
this.socket.emit("agentRunner:trackSpending",
|
|
238
|
-
taskId: this.config.taskId,
|
|
239
|
-
...params
|
|
240
|
-
});
|
|
235
|
+
this.socket.emit("agentRunner:trackSpending", params);
|
|
241
236
|
}
|
|
242
237
|
emitStatus(status) {
|
|
243
238
|
if (!this.socket) return;
|
|
244
|
-
this.socket.emit("agentRunner:statusUpdate", {
|
|
245
|
-
taskId: this.config.taskId,
|
|
246
|
-
status
|
|
247
|
-
});
|
|
239
|
+
this.socket.emit("agentRunner:statusUpdate", { status });
|
|
248
240
|
}
|
|
249
241
|
sendHeartbeat() {
|
|
250
242
|
if (!this.socket) return;
|
|
251
|
-
this.socket.emit("agentRunner:heartbeat", {
|
|
252
|
-
taskId: this.config.taskId
|
|
253
|
-
});
|
|
243
|
+
this.socket.emit("agentRunner:heartbeat", {});
|
|
254
244
|
}
|
|
255
245
|
sendTypingStart() {
|
|
256
246
|
this.sendEvent({ type: "agent_typing_start" });
|
|
@@ -262,22 +252,15 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
262
252
|
const socket = this.socket;
|
|
263
253
|
if (!socket) throw new Error("Not connected");
|
|
264
254
|
return new Promise((resolve2, reject) => {
|
|
265
|
-
socket.emit(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (response.success && response.data) resolve2(response.data);
|
|
270
|
-
else reject(new Error(response.error ?? "Failed to create subtask"));
|
|
271
|
-
}
|
|
272
|
-
);
|
|
255
|
+
socket.emit("agentRunner:createSubtask", data, (response) => {
|
|
256
|
+
if (response.success && response.data) resolve2(response.data);
|
|
257
|
+
else reject(new Error(response.error ?? "Failed to create subtask"));
|
|
258
|
+
});
|
|
273
259
|
});
|
|
274
260
|
}
|
|
275
261
|
updateSubtask(subtaskId, fields) {
|
|
276
262
|
if (!this.socket) throw new Error("Not connected");
|
|
277
|
-
this.socket.emit("agentRunner:updateSubtask", {
|
|
278
|
-
subtaskId,
|
|
279
|
-
fields
|
|
280
|
-
});
|
|
263
|
+
this.socket.emit("agentRunner:updateSubtask", { subtaskId, fields });
|
|
281
264
|
}
|
|
282
265
|
deleteSubtask(subtaskId) {
|
|
283
266
|
if (!this.socket) throw new Error("Not connected");
|
|
@@ -287,14 +270,10 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
287
270
|
const socket = this.socket;
|
|
288
271
|
if (!socket) throw new Error("Not connected");
|
|
289
272
|
return new Promise((resolve2, reject) => {
|
|
290
|
-
socket.emit(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (response.success && response.data) resolve2(response.data);
|
|
295
|
-
else reject(new Error(response.error ?? "Failed to list subtasks"));
|
|
296
|
-
}
|
|
297
|
-
);
|
|
273
|
+
socket.emit("agentRunner:listSubtasks", {}, (response) => {
|
|
274
|
+
if (response.success && response.data) resolve2(response.data);
|
|
275
|
+
else reject(new Error(response.error ?? "Failed to list subtasks"));
|
|
276
|
+
});
|
|
298
277
|
});
|
|
299
278
|
}
|
|
300
279
|
fetchTask(slugOrId) {
|
|
@@ -303,7 +282,7 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
303
282
|
return new Promise((resolve2, reject) => {
|
|
304
283
|
socket.emit(
|
|
305
284
|
"agentRunner:getTask",
|
|
306
|
-
{
|
|
285
|
+
{ slugOrId },
|
|
307
286
|
(response) => {
|
|
308
287
|
if (response.success && response.data) {
|
|
309
288
|
resolve2(response.data);
|
|
@@ -321,9 +300,157 @@ var ConveyorConnection = class _ConveyorConnection {
|
|
|
321
300
|
}
|
|
322
301
|
};
|
|
323
302
|
|
|
324
|
-
// src/
|
|
325
|
-
import {
|
|
326
|
-
|
|
303
|
+
// src/connection/project-connection.ts
|
|
304
|
+
import { io as io2 } from "socket.io-client";
|
|
305
|
+
var ProjectConnection = class {
|
|
306
|
+
socket = null;
|
|
307
|
+
config;
|
|
308
|
+
taskAssignmentCallback = null;
|
|
309
|
+
stopTaskCallback = null;
|
|
310
|
+
shutdownCallback = null;
|
|
311
|
+
chatMessageCallback = null;
|
|
312
|
+
earlyChatMessages = [];
|
|
313
|
+
constructor(config) {
|
|
314
|
+
this.config = config;
|
|
315
|
+
}
|
|
316
|
+
connect() {
|
|
317
|
+
return new Promise((resolve2, reject) => {
|
|
318
|
+
let settled = false;
|
|
319
|
+
let attempts = 0;
|
|
320
|
+
const maxInitialAttempts = 30;
|
|
321
|
+
this.socket = io2(this.config.apiUrl, {
|
|
322
|
+
auth: { projectToken: this.config.projectToken },
|
|
323
|
+
transports: ["websocket"],
|
|
324
|
+
reconnection: true,
|
|
325
|
+
reconnectionAttempts: Infinity,
|
|
326
|
+
reconnectionDelay: 2e3,
|
|
327
|
+
reconnectionDelayMax: 3e4,
|
|
328
|
+
randomizationFactor: 0.3,
|
|
329
|
+
extraHeaders: {
|
|
330
|
+
"ngrok-skip-browser-warning": "true"
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
this.socket.on("projectRunner:assignTask", (data) => {
|
|
334
|
+
if (this.taskAssignmentCallback) {
|
|
335
|
+
this.taskAssignmentCallback(data);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
this.socket.on("projectRunner:stopTask", (data) => {
|
|
339
|
+
if (this.stopTaskCallback) {
|
|
340
|
+
this.stopTaskCallback(data);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
this.socket.on("projectRunner:shutdown", () => {
|
|
344
|
+
if (this.shutdownCallback) {
|
|
345
|
+
this.shutdownCallback();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
this.socket.on("projectRunner:incomingChatMessage", (msg) => {
|
|
349
|
+
if (this.chatMessageCallback) {
|
|
350
|
+
this.chatMessageCallback(msg);
|
|
351
|
+
} else {
|
|
352
|
+
this.earlyChatMessages.push(msg);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.socket.on("connect", () => {
|
|
356
|
+
if (!settled) {
|
|
357
|
+
settled = true;
|
|
358
|
+
resolve2();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
this.socket.io.on("reconnect_attempt", () => {
|
|
362
|
+
attempts++;
|
|
363
|
+
if (!settled && attempts >= maxInitialAttempts) {
|
|
364
|
+
settled = true;
|
|
365
|
+
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
onTaskAssignment(callback) {
|
|
371
|
+
this.taskAssignmentCallback = callback;
|
|
372
|
+
}
|
|
373
|
+
onStopTask(callback) {
|
|
374
|
+
this.stopTaskCallback = callback;
|
|
375
|
+
}
|
|
376
|
+
onShutdown(callback) {
|
|
377
|
+
this.shutdownCallback = callback;
|
|
378
|
+
}
|
|
379
|
+
onChatMessage(callback) {
|
|
380
|
+
this.chatMessageCallback = callback;
|
|
381
|
+
for (const msg of this.earlyChatMessages) {
|
|
382
|
+
callback(msg);
|
|
383
|
+
}
|
|
384
|
+
this.earlyChatMessages = [];
|
|
385
|
+
}
|
|
386
|
+
sendHeartbeat() {
|
|
387
|
+
if (!this.socket) return;
|
|
388
|
+
this.socket.emit("projectRunner:heartbeat", {});
|
|
389
|
+
}
|
|
390
|
+
emitTaskStarted(taskId) {
|
|
391
|
+
if (!this.socket) return;
|
|
392
|
+
this.socket.emit("projectRunner:taskStarted", { taskId });
|
|
393
|
+
}
|
|
394
|
+
emitTaskStopped(taskId, reason) {
|
|
395
|
+
if (!this.socket) return;
|
|
396
|
+
this.socket.emit("projectRunner:taskStopped", { taskId, reason });
|
|
397
|
+
}
|
|
398
|
+
emitEvent(event) {
|
|
399
|
+
if (!this.socket) return;
|
|
400
|
+
this.socket.emit("conveyor:projectAgentEvent", event);
|
|
401
|
+
}
|
|
402
|
+
emitChatMessage(content) {
|
|
403
|
+
const socket = this.socket;
|
|
404
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
405
|
+
return new Promise((resolve2, reject) => {
|
|
406
|
+
socket.emit(
|
|
407
|
+
"conveyor:projectAgentChatMessage",
|
|
408
|
+
{ content },
|
|
409
|
+
(response) => {
|
|
410
|
+
if (response.success) resolve2();
|
|
411
|
+
else reject(new Error(response.error ?? "Failed to send chat message"));
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
emitAgentStatus(status) {
|
|
417
|
+
if (!this.socket) return;
|
|
418
|
+
this.socket.emit("conveyor:projectAgentStatus", { status });
|
|
419
|
+
}
|
|
420
|
+
fetchAgentContext() {
|
|
421
|
+
const socket = this.socket;
|
|
422
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
423
|
+
return new Promise((resolve2, reject) => {
|
|
424
|
+
socket.emit(
|
|
425
|
+
"projectRunner:getAgentContext",
|
|
426
|
+
(response) => {
|
|
427
|
+
if (response.success) resolve2(response.data ?? null);
|
|
428
|
+
else reject(new Error(response.error ?? "Failed to fetch agent context"));
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
fetchChatHistory(limit) {
|
|
434
|
+
const socket = this.socket;
|
|
435
|
+
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
436
|
+
return new Promise((resolve2, reject) => {
|
|
437
|
+
socket.emit(
|
|
438
|
+
"projectRunner:getChatHistory",
|
|
439
|
+
{ limit },
|
|
440
|
+
(response) => {
|
|
441
|
+
if (response.success && response.data) resolve2(response.data);
|
|
442
|
+
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
disconnect() {
|
|
448
|
+
this.socket?.disconnect();
|
|
449
|
+
this.socket = null;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/setup/config.ts
|
|
327
454
|
import { readFile } from "fs/promises";
|
|
328
455
|
import { join } from "path";
|
|
329
456
|
var CONVEYOR_CONFIG_PATH = ".conveyor/config.json";
|
|
@@ -354,6 +481,9 @@ async function loadConveyorConfig(workspaceDir) {
|
|
|
354
481
|
}
|
|
355
482
|
return null;
|
|
356
483
|
}
|
|
484
|
+
|
|
485
|
+
// src/setup/commands.ts
|
|
486
|
+
import { spawn } from "child_process";
|
|
357
487
|
function runSetupCommand(cmd, cwd, onOutput) {
|
|
358
488
|
return new Promise((resolve2, reject) => {
|
|
359
489
|
const child = spawn("sh", ["-c", cmd], {
|
|
@@ -395,6 +525,10 @@ function runStartCommand(cmd, cwd, onOutput) {
|
|
|
395
525
|
child.unref();
|
|
396
526
|
return child;
|
|
397
527
|
}
|
|
528
|
+
|
|
529
|
+
// src/setup/codespace.ts
|
|
530
|
+
import { execSync } from "child_process";
|
|
531
|
+
var DEVCONTAINER_PATH2 = ".devcontainer/conveyor/devcontainer.json";
|
|
398
532
|
function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
399
533
|
const git = (cmd) => execSync(cmd, { cwd: workspaceDir, encoding: "utf-8", timeout: 3e4 }).trim();
|
|
400
534
|
try {
|
|
@@ -403,7 +537,7 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
|
403
537
|
return { cleaned: false, message: `Failed to fetch origin/${baseBranch}` };
|
|
404
538
|
}
|
|
405
539
|
try {
|
|
406
|
-
git(`git diff --quiet origin/${baseBranch} -- ${
|
|
540
|
+
git(`git diff --quiet origin/${baseBranch} -- ${DEVCONTAINER_PATH2}`);
|
|
407
541
|
return { cleaned: false, message: "devcontainer.json already matches base" };
|
|
408
542
|
} catch {
|
|
409
543
|
}
|
|
@@ -412,10 +546,10 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
|
412
546
|
if (ahead <= 1) {
|
|
413
547
|
git(`git reset --hard origin/${baseBranch}`);
|
|
414
548
|
} else {
|
|
415
|
-
git(`git checkout origin/${baseBranch} -- ${
|
|
416
|
-
git(`git add ${
|
|
549
|
+
git(`git checkout origin/${baseBranch} -- ${DEVCONTAINER_PATH2}`);
|
|
550
|
+
git(`git add ${DEVCONTAINER_PATH2}`);
|
|
417
551
|
try {
|
|
418
|
-
git(`git diff --cached --quiet -- ${
|
|
552
|
+
git(`git diff --cached --quiet -- ${DEVCONTAINER_PATH2}`);
|
|
419
553
|
return { cleaned: false, message: "devcontainer.json already clean in working tree" };
|
|
420
554
|
} catch {
|
|
421
555
|
git(`git commit -m "chore: reset devcontainer config"`);
|
|
@@ -436,8 +570,25 @@ function cleanDevcontainerFromGit(workspaceDir, taskBranch, baseBranch) {
|
|
|
436
570
|
return { cleaned: false, message: `Git cleanup failed: ${msg}` };
|
|
437
571
|
}
|
|
438
572
|
}
|
|
573
|
+
function initRtk() {
|
|
574
|
+
try {
|
|
575
|
+
execSync("rtk --version", { stdio: "ignore" });
|
|
576
|
+
execSync("rtk init --global --auto-patch", { stdio: "ignore" });
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function unshallowRepo(workspaceDir) {
|
|
581
|
+
try {
|
|
582
|
+
execSync("git fetch --unshallow", {
|
|
583
|
+
cwd: workspaceDir,
|
|
584
|
+
stdio: "ignore",
|
|
585
|
+
timeout: 6e4
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
439
590
|
|
|
440
|
-
// src/worktree.ts
|
|
591
|
+
// src/runner/worktree.ts
|
|
441
592
|
import { execSync as execSync2 } from "child_process";
|
|
442
593
|
import { existsSync } from "fs";
|
|
443
594
|
import { join as join2 } from "path";
|
|
@@ -454,39 +605,194 @@ function ensureWorktree(projectDir, taskId, branch) {
|
|
|
454
605
|
} catch {
|
|
455
606
|
}
|
|
456
607
|
}
|
|
457
|
-
return worktreePath;
|
|
608
|
+
return worktreePath;
|
|
609
|
+
}
|
|
610
|
+
const ref = branch ? `origin/${branch}` : "HEAD";
|
|
611
|
+
execSync2(`git worktree add --detach "${worktreePath}" ${ref}`, {
|
|
612
|
+
cwd: projectDir,
|
|
613
|
+
stdio: "ignore"
|
|
614
|
+
});
|
|
615
|
+
return worktreePath;
|
|
616
|
+
}
|
|
617
|
+
function removeWorktree(projectDir, taskId) {
|
|
618
|
+
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
619
|
+
if (!existsSync(worktreePath)) return;
|
|
620
|
+
try {
|
|
621
|
+
execSync2(`git worktree remove "${worktreePath}" --force`, {
|
|
622
|
+
cwd: projectDir,
|
|
623
|
+
stdio: "ignore"
|
|
624
|
+
});
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/runner/agent-runner.ts
|
|
630
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
631
|
+
import { execSync as execSync3 } from "child_process";
|
|
632
|
+
|
|
633
|
+
// src/execution/event-processor.ts
|
|
634
|
+
async function processAssistantEvent(event, host, turnToolCalls) {
|
|
635
|
+
const { content } = event.message;
|
|
636
|
+
const turnTextParts = [];
|
|
637
|
+
for (const block of content) {
|
|
638
|
+
if (block.type === "text") {
|
|
639
|
+
turnTextParts.push(block.text);
|
|
640
|
+
host.connection.sendEvent({ type: "message", content: block.text });
|
|
641
|
+
await host.callbacks.onEvent({ type: "message", content: block.text });
|
|
642
|
+
} else if (block.type === "tool_use") {
|
|
643
|
+
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
644
|
+
const isContentTool = ["edit", "write"].includes(block.name.toLowerCase());
|
|
645
|
+
const inputLimit = isContentTool ? 1e4 : 500;
|
|
646
|
+
const summary = {
|
|
647
|
+
tool: block.name,
|
|
648
|
+
input: inputStr.slice(0, inputLimit),
|
|
649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
650
|
+
};
|
|
651
|
+
turnToolCalls.push(summary);
|
|
652
|
+
host.connection.sendEvent({ type: "tool_use", tool: block.name, input: inputStr });
|
|
653
|
+
await host.callbacks.onEvent({ type: "tool_use", tool: block.name, input: inputStr });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (turnTextParts.length > 0) {
|
|
657
|
+
host.connection.postChatMessage(turnTextParts.join("\n\n"));
|
|
658
|
+
}
|
|
659
|
+
if (turnToolCalls.length > 0) {
|
|
660
|
+
host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
|
|
661
|
+
turnToolCalls.length = 0;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
var API_ERROR_PATTERN = /API Error: [45]\d\d/;
|
|
665
|
+
function handleResultEvent(event, host, context, startTime) {
|
|
666
|
+
let totalCostUsd = 0;
|
|
667
|
+
let retriable = false;
|
|
668
|
+
if (event.subtype === "success") {
|
|
669
|
+
const successEvent = event;
|
|
670
|
+
const queryCostUsd = successEvent.total_cost_usd;
|
|
671
|
+
const durationMs = Date.now() - startTime;
|
|
672
|
+
const summary = successEvent.result || "Task completed.";
|
|
673
|
+
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
674
|
+
retriable = true;
|
|
675
|
+
}
|
|
676
|
+
const cumulativeTotal = host.costTracker.addQueryCost(queryCostUsd);
|
|
677
|
+
totalCostUsd = cumulativeTotal;
|
|
678
|
+
const { modelUsage } = successEvent;
|
|
679
|
+
if (modelUsage && typeof modelUsage === "object") {
|
|
680
|
+
host.costTracker.addModelUsage(modelUsage);
|
|
681
|
+
}
|
|
682
|
+
host.connection.sendEvent({ type: "completed", summary, costUsd: cumulativeTotal, durationMs });
|
|
683
|
+
if (cumulativeTotal > 0 && context.agentId && context._runnerSessionId) {
|
|
684
|
+
const breakdown = host.costTracker.modelBreakdown;
|
|
685
|
+
host.connection.trackSpending({
|
|
686
|
+
agentId: context.agentId,
|
|
687
|
+
sessionId: context._runnerSessionId,
|
|
688
|
+
totalCostUsd: cumulativeTotal,
|
|
689
|
+
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
690
|
+
modelUsage: breakdown.length > 0 ? breakdown : void 0
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
const errorEvent = event;
|
|
695
|
+
const errorMsg = errorEvent.errors.length > 0 ? errorEvent.errors.join(", ") : `Agent stopped: ${errorEvent.subtype}`;
|
|
696
|
+
if (API_ERROR_PATTERN.test(errorMsg)) {
|
|
697
|
+
retriable = true;
|
|
698
|
+
}
|
|
699
|
+
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
700
|
+
}
|
|
701
|
+
return { totalCostUsd, retriable };
|
|
702
|
+
}
|
|
703
|
+
async function emitResultEvent(event, host, context, startTime) {
|
|
704
|
+
const result = handleResultEvent(event, host, context, startTime);
|
|
705
|
+
const durationMs = Date.now() - startTime;
|
|
706
|
+
if (event.subtype === "success") {
|
|
707
|
+
const successEvent = event;
|
|
708
|
+
const summary = successEvent.result || "Task completed.";
|
|
709
|
+
await host.callbacks.onEvent({
|
|
710
|
+
type: "completed",
|
|
711
|
+
summary,
|
|
712
|
+
costUsd: result.totalCostUsd,
|
|
713
|
+
durationMs
|
|
714
|
+
});
|
|
715
|
+
} else {
|
|
716
|
+
const errorEvent = event;
|
|
717
|
+
const errorMsg = errorEvent.errors.length > 0 ? errorEvent.errors.join(", ") : `Agent stopped: ${errorEvent.subtype}`;
|
|
718
|
+
await host.callbacks.onEvent({ type: "error", message: errorMsg });
|
|
719
|
+
}
|
|
720
|
+
return result.retriable;
|
|
721
|
+
}
|
|
722
|
+
function handleRateLimitEvent(event, host) {
|
|
723
|
+
const { rate_limit_info } = event;
|
|
724
|
+
const status = rate_limit_info.status;
|
|
725
|
+
if (status === "rejected") {
|
|
726
|
+
const resetsAt = rate_limit_info.resetsAt ? new Date(rate_limit_info.resetsAt).toISOString() : "unknown";
|
|
727
|
+
const message = `Rate limit rejected (type: ${rate_limit_info.rateLimitType ?? "unknown"}, resets at: ${resetsAt})`;
|
|
728
|
+
host.connection.sendEvent({ type: "error", message });
|
|
729
|
+
void host.callbacks.onEvent({ type: "error", message });
|
|
730
|
+
} else if (status === "allowed_warning") {
|
|
731
|
+
const utilization = rate_limit_info.utilization ? `${Math.round(rate_limit_info.utilization * 100)}%` : "high";
|
|
732
|
+
const message = `Rate limit warning: ${utilization} utilization (type: ${rate_limit_info.rateLimitType ?? "unknown"})`;
|
|
733
|
+
host.connection.sendEvent({ type: "thinking", message });
|
|
734
|
+
void host.callbacks.onEvent({ type: "thinking", message });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async function processEvents(events, context, host) {
|
|
738
|
+
const startTime = Date.now();
|
|
739
|
+
let sessionIdStored = false;
|
|
740
|
+
let isTyping = false;
|
|
741
|
+
let retriable = false;
|
|
742
|
+
const turnToolCalls = [];
|
|
743
|
+
for await (const event of events) {
|
|
744
|
+
if (host.isStopped()) break;
|
|
745
|
+
switch (event.type) {
|
|
746
|
+
case "system": {
|
|
747
|
+
const systemEvent = event;
|
|
748
|
+
if (systemEvent.subtype === "init") {
|
|
749
|
+
if (systemEvent.session_id && !sessionIdStored) {
|
|
750
|
+
sessionIdStored = true;
|
|
751
|
+
host.connection.storeSessionId(systemEvent.session_id);
|
|
752
|
+
context.claudeSessionId = systemEvent.session_id;
|
|
753
|
+
}
|
|
754
|
+
await host.callbacks.onEvent({
|
|
755
|
+
type: "thinking",
|
|
756
|
+
message: `Agent initialized (model: ${systemEvent.model})`
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
case "assistant": {
|
|
762
|
+
if (!isTyping) {
|
|
763
|
+
setTimeout(() => host.connection.sendTypingStart(), 200);
|
|
764
|
+
isTyping = true;
|
|
765
|
+
}
|
|
766
|
+
await processAssistantEvent(event, host, turnToolCalls);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "result": {
|
|
770
|
+
if (isTyping) {
|
|
771
|
+
host.connection.sendTypingStop();
|
|
772
|
+
isTyping = false;
|
|
773
|
+
}
|
|
774
|
+
retriable = await emitResultEvent(event, host, context, startTime);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
case "rate_limit_event": {
|
|
778
|
+
handleRateLimitEvent(event, host);
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
458
782
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
cwd: projectDir,
|
|
462
|
-
stdio: "ignore"
|
|
463
|
-
});
|
|
464
|
-
return worktreePath;
|
|
465
|
-
}
|
|
466
|
-
function removeWorktree(projectDir, taskId) {
|
|
467
|
-
const worktreePath = join2(projectDir, WORKTREE_DIR, taskId);
|
|
468
|
-
if (!existsSync(worktreePath)) return;
|
|
469
|
-
try {
|
|
470
|
-
execSync2(`git worktree remove "${worktreePath}" --force`, {
|
|
471
|
-
cwd: projectDir,
|
|
472
|
-
stdio: "ignore"
|
|
473
|
-
});
|
|
474
|
-
} catch {
|
|
783
|
+
if (isTyping) {
|
|
784
|
+
host.connection.sendTypingStop();
|
|
475
785
|
}
|
|
786
|
+
return { retriable };
|
|
476
787
|
}
|
|
477
788
|
|
|
478
|
-
// src/
|
|
479
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
480
|
-
import { execSync as execSync3 } from "child_process";
|
|
481
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
482
|
-
import { homedir } from "os";
|
|
483
|
-
import { join as join3 } from "path";
|
|
484
|
-
|
|
485
|
-
// src/query-executor.ts
|
|
789
|
+
// src/execution/query-executor.ts
|
|
486
790
|
import { randomUUID } from "crypto";
|
|
487
|
-
import {
|
|
791
|
+
import {
|
|
792
|
+
query
|
|
793
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
488
794
|
|
|
489
|
-
// src/prompt-builder.ts
|
|
795
|
+
// src/execution/prompt-builder.ts
|
|
490
796
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["InProgress", "ReviewPR", "ReviewDev", "ReviewLive"]);
|
|
491
797
|
function formatFileSize(bytes) {
|
|
492
798
|
if (bytes === void 0) return "";
|
|
@@ -556,7 +862,7 @@ Address the requested changes. Do NOT re-investigate the codebase from scratch o
|
|
|
556
862
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
557
863
|
`Run \`git log --oneline -10\` to review what you already committed.`,
|
|
558
864
|
`Review the current state of the codebase and verify everything is working correctly.`,
|
|
559
|
-
`
|
|
865
|
+
`Reply with a brief status update (visible in chat), then wait for further instructions.`
|
|
560
866
|
);
|
|
561
867
|
if (context.githubPRUrl) {
|
|
562
868
|
parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
|
|
@@ -638,20 +944,20 @@ function buildInstructions(mode, context, scenario) {
|
|
|
638
944
|
`You are the project manager for this task and its subtasks.`,
|
|
639
945
|
`Use list_subtasks to review the current state of child tasks.`,
|
|
640
946
|
`The task details are provided above. Wait for the team to provide instructions before taking action.`,
|
|
641
|
-
`When you finish planning, save the plan with update_task
|
|
947
|
+
`When you finish planning, save the plan with update_task and end your turn. Your reply will be visible to the team in chat.`
|
|
642
948
|
);
|
|
643
949
|
} else if (isPm) {
|
|
644
950
|
parts.push(
|
|
645
951
|
`You are the project manager for this task.`,
|
|
646
952
|
`The task details are provided above. Wait for the team to ask questions or provide additional requirements before starting to plan.`,
|
|
647
|
-
`When you finish planning, save the plan with update_task
|
|
953
|
+
`When you finish planning, save the plan with update_task and end your turn. Your reply summarizing the plan will be visible in chat. A separate task agent will execute the plan after review.`
|
|
648
954
|
);
|
|
649
955
|
} else {
|
|
650
956
|
parts.push(
|
|
651
957
|
`Begin executing the task plan above immediately.`,
|
|
652
958
|
`Your FIRST action should be reading the relevant source files mentioned in the plan, then writing code. Do NOT run install, build, lint, test, or dev server commands first \u2014 the environment is already set up.`,
|
|
653
959
|
`Work on the git branch "${context.githubBranch}". Stay on this branch for the entire task. Do not checkout or create other branches.`,
|
|
654
|
-
`
|
|
960
|
+
`Your replies are visible to the team in chat \u2014 briefly describe what you're doing when you begin meaningful implementation, and again when the PR is ready.`,
|
|
655
961
|
`When finished, commit your changes, then run \`git fetch origin ${context.githubBranch}\` and \`git push origin ${context.githubBranch}\` (use --force-with-lease if push fails). Then use the create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
|
|
656
962
|
);
|
|
657
963
|
}
|
|
@@ -667,7 +973,7 @@ function buildInstructions(mode, context, scenario) {
|
|
|
667
973
|
`You were relaunched but no new instructions have been given since your last run.`,
|
|
668
974
|
`Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
|
|
669
975
|
`Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`,
|
|
670
|
-
`
|
|
976
|
+
`Reply with a brief status update summarizing where things stand (visible in chat).`,
|
|
671
977
|
`Then wait for further instructions \u2014 do NOT redo work that was already completed.`
|
|
672
978
|
);
|
|
673
979
|
if (context.githubPRUrl) {
|
|
@@ -720,8 +1026,9 @@ function buildInitialPrompt(mode, context) {
|
|
|
720
1026
|
const instructions = buildInstructions(mode, context, scenario);
|
|
721
1027
|
return [...body, ...instructions].join("\n");
|
|
722
1028
|
}
|
|
723
|
-
function buildSystemPrompt(mode, context, config, setupLog) {
|
|
1029
|
+
function buildSystemPrompt(mode, context, config, setupLog, pmSubMode = "planning") {
|
|
724
1030
|
const isPm = mode === "pm";
|
|
1031
|
+
const isPmActive = isPm && pmSubMode === "active";
|
|
725
1032
|
const pmParts = [
|
|
726
1033
|
`You are an AI project manager helping to plan tasks for the "${context.title}" project.`,
|
|
727
1034
|
`You are running locally with full access to the repository.`,
|
|
@@ -735,7 +1042,7 @@ Environment (ready, no setup required):`,
|
|
|
735
1042
|
Workflow:`,
|
|
736
1043
|
`- You can draft and iterate on plans in .claude/plans/*.md \u2014 these files are automatically synced to the task.`,
|
|
737
1044
|
`- You can also use update_task directly to save the plan to the task.`,
|
|
738
|
-
`- After saving the plan,
|
|
1045
|
+
`- After saving the plan, end your turn with a summary reply (the team sees your responses in chat automatically). Do NOT attempt to execute the plan yourself.`,
|
|
739
1046
|
`- A separate task agent will handle execution after the team reviews and approves your plan.`
|
|
740
1047
|
];
|
|
741
1048
|
if (isPm && context.isParentTask) {
|
|
@@ -764,7 +1071,30 @@ Project Agents:`);
|
|
|
764
1071
|
pmParts.push(`- ${pa.agent.name} (${role}${sp})`);
|
|
765
1072
|
}
|
|
766
1073
|
}
|
|
767
|
-
const
|
|
1074
|
+
const activeParts = [
|
|
1075
|
+
`You are an AI project manager in ACTIVE mode for the "${context.title}" project.`,
|
|
1076
|
+
`You have direct coding access to the repository at ${config.workspaceDir}.`,
|
|
1077
|
+
`You can edit files, run tests, and make commits.`,
|
|
1078
|
+
`You still have access to all PM tools (subtasks, update_task, chat).`,
|
|
1079
|
+
`
|
|
1080
|
+
Environment (ready, no setup required):`,
|
|
1081
|
+
`- Repository is cloned at your current working directory.`,
|
|
1082
|
+
`- You can read, write, and edit files directly.`,
|
|
1083
|
+
`- You can run shell commands including git, build tools, and test runners.`,
|
|
1084
|
+
context.githubBranch ? `- You are working on branch: \`${context.githubBranch}\`` : "",
|
|
1085
|
+
`
|
|
1086
|
+
Safety rules:`,
|
|
1087
|
+
`- Stay within the project directory (${config.workspaceDir}).`,
|
|
1088
|
+
`- Do NOT run \`git push --force\` or \`git reset --hard\`. Use \`--force-with-lease\` if needed.`,
|
|
1089
|
+
`- Do NOT delete \`.env\` files or modify \`node_modules\`.`,
|
|
1090
|
+
`- Do NOT run destructive commands like \`rm -rf /\`.`,
|
|
1091
|
+
`
|
|
1092
|
+
Workflow:`,
|
|
1093
|
+
`- You can make code changes, fix bugs, run tests, and commit directly.`,
|
|
1094
|
+
`- When done with changes, summarize what you did in your reply.`,
|
|
1095
|
+
`- If you toggled into active mode temporarily, mention when you're done so the team can switch you back to planning mode.`
|
|
1096
|
+
].filter(Boolean);
|
|
1097
|
+
const parts = isPmActive ? activeParts : isPm ? pmParts : [
|
|
768
1098
|
`You are an AI agent working on a task for the "${context.title}" project.`,
|
|
769
1099
|
`You are running inside a GitHub Codespace with full access to the repository.`,
|
|
770
1100
|
`
|
|
@@ -810,11 +1140,12 @@ ${config.instructions}`);
|
|
|
810
1140
|
}
|
|
811
1141
|
parts.push(
|
|
812
1142
|
`
|
|
813
|
-
|
|
814
|
-
`
|
|
815
|
-
`
|
|
1143
|
+
Your responses are sent directly to the task chat \u2014 the team sees everything you say.`,
|
|
1144
|
+
`Do NOT call the post_to_chat tool for your own task; your replies already appear in chat automatically.`,
|
|
1145
|
+
`Only use post_to_chat if you need to message a different task's chat (e.g. a parent task via get_task).`,
|
|
1146
|
+
`Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`
|
|
816
1147
|
);
|
|
817
|
-
if (!isPm) {
|
|
1148
|
+
if (!isPm || isPmActive) {
|
|
818
1149
|
parts.push(
|
|
819
1150
|
`Use the create_pull_request tool to open PRs \u2014 do NOT use gh CLI or shell commands for PR creation.`
|
|
820
1151
|
);
|
|
@@ -822,8 +1153,11 @@ You have access to Conveyor MCP tools to interact with the task management syste
|
|
|
822
1153
|
return parts.join("\n");
|
|
823
1154
|
}
|
|
824
1155
|
|
|
825
|
-
// src/
|
|
826
|
-
import {
|
|
1156
|
+
// src/tools/index.ts
|
|
1157
|
+
import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
1158
|
+
|
|
1159
|
+
// src/tools/common-tools.ts
|
|
1160
|
+
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
827
1161
|
import { z } from "zod";
|
|
828
1162
|
function buildCommonTools(connection, config) {
|
|
829
1163
|
return [
|
|
@@ -849,7 +1183,7 @@ function buildCommonTools(connection, config) {
|
|
|
849
1183
|
),
|
|
850
1184
|
tool(
|
|
851
1185
|
"post_to_chat",
|
|
852
|
-
"Post a message to the task chat
|
|
1186
|
+
"Post a message to the task chat. Your normal replies already appear in chat \u2014 only use this for explicit out-of-band updates or posting to a different task's chat",
|
|
853
1187
|
{ message: z.string().describe("The message to post to the team") },
|
|
854
1188
|
({ message }) => {
|
|
855
1189
|
connection.postChatMessage(message);
|
|
@@ -931,6 +1265,10 @@ function buildCommonTools(connection, config) {
|
|
|
931
1265
|
)
|
|
932
1266
|
];
|
|
933
1267
|
}
|
|
1268
|
+
|
|
1269
|
+
// src/tools/pm-tools.ts
|
|
1270
|
+
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1271
|
+
import { z as z2 } from "zod";
|
|
934
1272
|
function buildStoryPointDescription(storyPoints) {
|
|
935
1273
|
if (storyPoints && storyPoints.length > 0) {
|
|
936
1274
|
const tiers = storyPoints.map((sp) => `${sp.value}=${sp.name}`).join(", ");
|
|
@@ -941,12 +1279,12 @@ function buildStoryPointDescription(storyPoints) {
|
|
|
941
1279
|
function buildPmTools(connection, storyPoints) {
|
|
942
1280
|
const spDescription = buildStoryPointDescription(storyPoints);
|
|
943
1281
|
return [
|
|
944
|
-
|
|
1282
|
+
tool2(
|
|
945
1283
|
"update_task",
|
|
946
1284
|
"Save the finalized task plan and/or description",
|
|
947
1285
|
{
|
|
948
|
-
plan:
|
|
949
|
-
description:
|
|
1286
|
+
plan: z2.string().optional().describe("The task plan in markdown"),
|
|
1287
|
+
description: z2.string().optional().describe("Updated task description")
|
|
950
1288
|
},
|
|
951
1289
|
async ({ plan, description }) => {
|
|
952
1290
|
try {
|
|
@@ -957,15 +1295,15 @@ function buildPmTools(connection, storyPoints) {
|
|
|
957
1295
|
}
|
|
958
1296
|
}
|
|
959
1297
|
),
|
|
960
|
-
|
|
1298
|
+
tool2(
|
|
961
1299
|
"create_subtask",
|
|
962
1300
|
"Create a subtask under the current parent task. Use for breaking complex tasks into smaller pieces.",
|
|
963
1301
|
{
|
|
964
|
-
title:
|
|
965
|
-
description:
|
|
966
|
-
plan:
|
|
967
|
-
ordinal:
|
|
968
|
-
storyPointValue:
|
|
1302
|
+
title: z2.string().describe("Subtask title"),
|
|
1303
|
+
description: z2.string().optional().describe("Brief description"),
|
|
1304
|
+
plan: z2.string().optional().describe("Implementation plan in markdown"),
|
|
1305
|
+
ordinal: z2.number().optional().describe("Step/order number (0-based)"),
|
|
1306
|
+
storyPointValue: z2.number().optional().describe(spDescription)
|
|
969
1307
|
},
|
|
970
1308
|
async (params) => {
|
|
971
1309
|
try {
|
|
@@ -978,16 +1316,16 @@ function buildPmTools(connection, storyPoints) {
|
|
|
978
1316
|
}
|
|
979
1317
|
}
|
|
980
1318
|
),
|
|
981
|
-
|
|
1319
|
+
tool2(
|
|
982
1320
|
"update_subtask",
|
|
983
1321
|
"Update an existing subtask's fields",
|
|
984
1322
|
{
|
|
985
|
-
subtaskId:
|
|
986
|
-
title:
|
|
987
|
-
description:
|
|
988
|
-
plan:
|
|
989
|
-
ordinal:
|
|
990
|
-
storyPointValue:
|
|
1323
|
+
subtaskId: z2.string().describe("The subtask ID to update"),
|
|
1324
|
+
title: z2.string().optional(),
|
|
1325
|
+
description: z2.string().optional(),
|
|
1326
|
+
plan: z2.string().optional(),
|
|
1327
|
+
ordinal: z2.number().optional(),
|
|
1328
|
+
storyPointValue: z2.number().optional().describe(spDescription)
|
|
991
1329
|
},
|
|
992
1330
|
async ({ subtaskId, ...fields }) => {
|
|
993
1331
|
try {
|
|
@@ -998,10 +1336,10 @@ function buildPmTools(connection, storyPoints) {
|
|
|
998
1336
|
}
|
|
999
1337
|
}
|
|
1000
1338
|
),
|
|
1001
|
-
|
|
1339
|
+
tool2(
|
|
1002
1340
|
"delete_subtask",
|
|
1003
1341
|
"Delete a subtask",
|
|
1004
|
-
{ subtaskId:
|
|
1342
|
+
{ subtaskId: z2.string().describe("The subtask ID to delete") },
|
|
1005
1343
|
async ({ subtaskId }) => {
|
|
1006
1344
|
try {
|
|
1007
1345
|
await Promise.resolve(connection.deleteSubtask(subtaskId));
|
|
@@ -1011,7 +1349,7 @@ function buildPmTools(connection, storyPoints) {
|
|
|
1011
1349
|
}
|
|
1012
1350
|
}
|
|
1013
1351
|
),
|
|
1014
|
-
|
|
1352
|
+
tool2(
|
|
1015
1353
|
"list_subtasks",
|
|
1016
1354
|
"List all subtasks under the current parent task",
|
|
1017
1355
|
{},
|
|
@@ -1027,14 +1365,18 @@ function buildPmTools(connection, storyPoints) {
|
|
|
1027
1365
|
)
|
|
1028
1366
|
];
|
|
1029
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
// src/tools/task-tools.ts
|
|
1370
|
+
import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
1371
|
+
import { z as z3 } from "zod";
|
|
1030
1372
|
function buildTaskTools(connection) {
|
|
1031
1373
|
return [
|
|
1032
|
-
|
|
1374
|
+
tool3(
|
|
1033
1375
|
"create_pull_request",
|
|
1034
1376
|
"Create a GitHub pull request for this task. Use this instead of gh CLI or git commands to create PRs.",
|
|
1035
1377
|
{
|
|
1036
|
-
title:
|
|
1037
|
-
body:
|
|
1378
|
+
title: z3.string().describe("The PR title"),
|
|
1379
|
+
body: z3.string().describe("The PR description/body in markdown")
|
|
1038
1380
|
},
|
|
1039
1381
|
async ({ title, body }) => {
|
|
1040
1382
|
try {
|
|
@@ -1053,6 +1395,8 @@ function buildTaskTools(connection) {
|
|
|
1053
1395
|
)
|
|
1054
1396
|
];
|
|
1055
1397
|
}
|
|
1398
|
+
|
|
1399
|
+
// src/tools/index.ts
|
|
1056
1400
|
function textResult(text) {
|
|
1057
1401
|
return { content: [{ type: "text", text }] };
|
|
1058
1402
|
}
|
|
@@ -1065,144 +1409,17 @@ function createConveyorMcpServer(connection, config, context) {
|
|
|
1065
1409
|
});
|
|
1066
1410
|
}
|
|
1067
1411
|
|
|
1068
|
-
// src/query-executor.ts
|
|
1069
|
-
var
|
|
1412
|
+
// src/execution/query-executor.ts
|
|
1413
|
+
var API_ERROR_PATTERN2 = /API Error: [45]\d\d/;
|
|
1070
1414
|
var RETRY_DELAYS_MS = [6e4, 12e4, 18e4, 3e5];
|
|
1071
1415
|
var PM_PLAN_FILE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit"]);
|
|
1072
|
-
|
|
1073
|
-
const msg = event.message;
|
|
1074
|
-
const content = msg.content;
|
|
1075
|
-
const turnTextParts = [];
|
|
1076
|
-
for (const block of content) {
|
|
1077
|
-
const blockType = block.type;
|
|
1078
|
-
if (blockType === "text") {
|
|
1079
|
-
const text = block.text;
|
|
1080
|
-
turnTextParts.push(text);
|
|
1081
|
-
host.connection.sendEvent({ type: "message", content: text });
|
|
1082
|
-
await host.callbacks.onEvent({ type: "message", content: text });
|
|
1083
|
-
} else if (blockType === "tool_use") {
|
|
1084
|
-
const name = block.name;
|
|
1085
|
-
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
1086
|
-
const isContentTool = ["edit", "write"].includes(name.toLowerCase());
|
|
1087
|
-
const inputLimit = isContentTool ? 1e4 : 500;
|
|
1088
|
-
const summary = {
|
|
1089
|
-
tool: name,
|
|
1090
|
-
input: inputStr.slice(0, inputLimit),
|
|
1091
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1092
|
-
};
|
|
1093
|
-
turnToolCalls.push(summary);
|
|
1094
|
-
host.connection.sendEvent({ type: "tool_use", tool: name, input: inputStr });
|
|
1095
|
-
await host.callbacks.onEvent({ type: "tool_use", tool: name, input: inputStr });
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (turnTextParts.length > 0) {
|
|
1099
|
-
host.connection.postChatMessage(turnTextParts.join("\n\n"));
|
|
1100
|
-
}
|
|
1101
|
-
if (turnToolCalls.length > 0) {
|
|
1102
|
-
host.connection.sendEvent({ type: "turn_end", toolCalls: [...turnToolCalls] });
|
|
1103
|
-
turnToolCalls.length = 0;
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
function handleResultEvent(event, host, context, startTime) {
|
|
1107
|
-
const resultEvent = event;
|
|
1108
|
-
let totalCostUsd = 0;
|
|
1109
|
-
let retriable = false;
|
|
1110
|
-
if (resultEvent.subtype === "success") {
|
|
1111
|
-
totalCostUsd = "total_cost_usd" in resultEvent ? resultEvent.total_cost_usd : 0;
|
|
1112
|
-
const durationMs = Date.now() - startTime;
|
|
1113
|
-
const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
|
|
1114
|
-
if (API_ERROR_PATTERN.test(summary) && durationMs < 3e4) {
|
|
1115
|
-
retriable = true;
|
|
1116
|
-
}
|
|
1117
|
-
host.connection.sendEvent({ type: "completed", summary, costUsd: totalCostUsd, durationMs });
|
|
1118
|
-
if (totalCostUsd > 0 && context.agentId && context._runnerSessionId) {
|
|
1119
|
-
host.connection.trackSpending({
|
|
1120
|
-
agentId: context.agentId,
|
|
1121
|
-
sessionId: context._runnerSessionId,
|
|
1122
|
-
totalCostUsd,
|
|
1123
|
-
onSubscription: host.config.mode === "pm" || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
} else {
|
|
1127
|
-
const errors = "errors" in resultEvent ? resultEvent.errors : [];
|
|
1128
|
-
const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
|
|
1129
|
-
if (API_ERROR_PATTERN.test(errorMsg)) {
|
|
1130
|
-
retriable = true;
|
|
1131
|
-
}
|
|
1132
|
-
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
1133
|
-
}
|
|
1134
|
-
return { totalCostUsd, retriable };
|
|
1135
|
-
}
|
|
1136
|
-
async function emitResultEvent(event, host, context, startTime) {
|
|
1137
|
-
const result = handleResultEvent(event, host, context, startTime);
|
|
1138
|
-
const durationMs = Date.now() - startTime;
|
|
1139
|
-
const resultEvent = event;
|
|
1140
|
-
if (resultEvent.subtype === "success") {
|
|
1141
|
-
const summary = "result" in resultEvent ? String(resultEvent.result) : "Task completed.";
|
|
1142
|
-
await host.callbacks.onEvent({
|
|
1143
|
-
type: "completed",
|
|
1144
|
-
summary,
|
|
1145
|
-
costUsd: result.totalCostUsd,
|
|
1146
|
-
durationMs
|
|
1147
|
-
});
|
|
1148
|
-
} else {
|
|
1149
|
-
const errors = "errors" in resultEvent ? resultEvent.errors : [];
|
|
1150
|
-
const errorMsg = errors.length > 0 ? errors.join(", ") : `Agent stopped: ${resultEvent.subtype}`;
|
|
1151
|
-
await host.callbacks.onEvent({ type: "error", message: errorMsg });
|
|
1152
|
-
}
|
|
1153
|
-
return result.retriable;
|
|
1154
|
-
}
|
|
1155
|
-
async function processEvents(events, context, host) {
|
|
1156
|
-
const startTime = Date.now();
|
|
1157
|
-
let sessionIdStored = false;
|
|
1158
|
-
let isTyping = false;
|
|
1159
|
-
let retriable = false;
|
|
1160
|
-
const turnToolCalls = [];
|
|
1161
|
-
for await (const event of events) {
|
|
1162
|
-
if (host.isStopped()) break;
|
|
1163
|
-
switch (event.type) {
|
|
1164
|
-
case "system": {
|
|
1165
|
-
if (event.subtype === "init") {
|
|
1166
|
-
const sessionId = event.session_id;
|
|
1167
|
-
if (sessionId && !sessionIdStored) {
|
|
1168
|
-
sessionIdStored = true;
|
|
1169
|
-
host.connection.storeSessionId(sessionId);
|
|
1170
|
-
context.claudeSessionId = sessionId;
|
|
1171
|
-
}
|
|
1172
|
-
await host.callbacks.onEvent({
|
|
1173
|
-
type: "thinking",
|
|
1174
|
-
message: `Agent initialized (model: ${event.model})`
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
break;
|
|
1178
|
-
}
|
|
1179
|
-
case "assistant": {
|
|
1180
|
-
if (!isTyping) {
|
|
1181
|
-
setTimeout(() => host.connection.sendTypingStart(), 200);
|
|
1182
|
-
isTyping = true;
|
|
1183
|
-
}
|
|
1184
|
-
await processAssistantEvent(event, host, turnToolCalls);
|
|
1185
|
-
break;
|
|
1186
|
-
}
|
|
1187
|
-
case "result": {
|
|
1188
|
-
if (isTyping) {
|
|
1189
|
-
host.connection.sendTypingStop();
|
|
1190
|
-
isTyping = false;
|
|
1191
|
-
}
|
|
1192
|
-
retriable = await emitResultEvent(event, host, context, startTime);
|
|
1193
|
-
break;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
if (isTyping) {
|
|
1198
|
-
host.connection.sendTypingStop();
|
|
1199
|
-
}
|
|
1200
|
-
return { retriable };
|
|
1201
|
-
}
|
|
1416
|
+
var DESTRUCTIVE_CMD_PATTERN = /git\s+push\s+--force(?!\s*-with-lease)|git\s+reset\s+--hard|rm\s+-rf\s+\//;
|
|
1202
1417
|
function buildCanUseTool(host) {
|
|
1203
1418
|
const QUESTION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1204
1419
|
return async (toolName, input) => {
|
|
1205
|
-
|
|
1420
|
+
const isPmPlanning = host.config.mode === "pm" && host.activeMode === "planning";
|
|
1421
|
+
const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
|
|
1422
|
+
if (isPmPlanning && PM_PLAN_FILE_TOOLS.has(toolName)) {
|
|
1206
1423
|
const filePath = String(input.file_path ?? input.path ?? "");
|
|
1207
1424
|
if (filePath.includes(".claude/plans/")) {
|
|
1208
1425
|
return { behavior: "allow", updatedInput: input };
|
|
@@ -1212,6 +1429,15 @@ function buildCanUseTool(host) {
|
|
|
1212
1429
|
message: "File write tools are only available for plan files in PM mode."
|
|
1213
1430
|
};
|
|
1214
1431
|
}
|
|
1432
|
+
if (isPmActive && toolName === "Bash") {
|
|
1433
|
+
const cmd = String(input.command ?? "");
|
|
1434
|
+
if (DESTRUCTIVE_CMD_PATTERN.test(cmd)) {
|
|
1435
|
+
return {
|
|
1436
|
+
behavior: "deny",
|
|
1437
|
+
message: "Destructive operation blocked in active mode. Use safer alternatives."
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1215
1441
|
if (toolName !== "AskUserQuestion") {
|
|
1216
1442
|
return { behavior: "allow", updatedInput: input };
|
|
1217
1443
|
}
|
|
@@ -1240,13 +1466,41 @@ function buildCanUseTool(host) {
|
|
|
1240
1466
|
}
|
|
1241
1467
|
function buildQueryOptions(host, context) {
|
|
1242
1468
|
const settings = context.agentSettings ?? host.config.agentSettings ?? {};
|
|
1243
|
-
const
|
|
1469
|
+
const isPmActive = host.config.mode === "pm" && host.activeMode === "active";
|
|
1470
|
+
const systemPromptText = buildSystemPrompt(
|
|
1471
|
+
host.config.mode,
|
|
1472
|
+
context,
|
|
1473
|
+
host.config,
|
|
1474
|
+
host.setupLog,
|
|
1475
|
+
isPmActive ? "active" : "planning"
|
|
1476
|
+
);
|
|
1244
1477
|
const conveyorMcp = createConveyorMcpServer(host.connection, host.config, context);
|
|
1245
1478
|
const isPm = host.config.mode === "pm";
|
|
1246
|
-
const pmDisallowedTools = isPm ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
|
|
1479
|
+
const pmDisallowedTools = isPm && !isPmActive ? ["TodoWrite", "TodoRead", "NotebookEdit"] : [];
|
|
1247
1480
|
const disallowedTools = [...settings.disallowedTools ?? [], ...pmDisallowedTools];
|
|
1248
1481
|
const settingSources = settings.settingSources ?? ["user", "project"];
|
|
1249
|
-
|
|
1482
|
+
const hooks = {
|
|
1483
|
+
PostToolUse: [
|
|
1484
|
+
{
|
|
1485
|
+
hooks: [
|
|
1486
|
+
async (input) => {
|
|
1487
|
+
if (input.hook_event_name === "PostToolUse") {
|
|
1488
|
+
const output = typeof input.tool_response === "string" ? input.tool_response.slice(0, 500) : JSON.stringify(input.tool_response).slice(0, 500);
|
|
1489
|
+
host.connection.sendEvent({
|
|
1490
|
+
type: "tool_result",
|
|
1491
|
+
tool: input.tool_name,
|
|
1492
|
+
output,
|
|
1493
|
+
isError: false
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
return { continue: true };
|
|
1497
|
+
}
|
|
1498
|
+
],
|
|
1499
|
+
timeout: 5
|
|
1500
|
+
}
|
|
1501
|
+
]
|
|
1502
|
+
};
|
|
1503
|
+
const baseOptions = {
|
|
1250
1504
|
model: context.model || host.config.model,
|
|
1251
1505
|
systemPrompt: {
|
|
1252
1506
|
type: "preset",
|
|
@@ -1255,11 +1509,12 @@ function buildQueryOptions(host, context) {
|
|
|
1255
1509
|
},
|
|
1256
1510
|
settingSources,
|
|
1257
1511
|
cwd: host.config.workspaceDir,
|
|
1258
|
-
permissionMode: "bypassPermissions",
|
|
1259
|
-
allowDangerouslySkipPermissions:
|
|
1512
|
+
permissionMode: isPmActive ? "acceptEdits" : "bypassPermissions",
|
|
1513
|
+
allowDangerouslySkipPermissions: !isPmActive,
|
|
1260
1514
|
canUseTool: buildCanUseTool(host),
|
|
1261
1515
|
tools: { type: "preset", preset: "claude_code" },
|
|
1262
1516
|
mcpServers: { conveyor: conveyorMcp },
|
|
1517
|
+
hooks,
|
|
1263
1518
|
maxTurns: settings.maxTurns,
|
|
1264
1519
|
effort: settings.effort,
|
|
1265
1520
|
thinking: settings.thinking,
|
|
@@ -1268,6 +1523,25 @@ function buildQueryOptions(host, context) {
|
|
|
1268
1523
|
disallowedTools: disallowedTools.length > 0 ? disallowedTools : void 0,
|
|
1269
1524
|
enableFileCheckpointing: settings.enableFileCheckpointing
|
|
1270
1525
|
};
|
|
1526
|
+
if (isPmActive) {
|
|
1527
|
+
const apiHostname = new URL(host.config.conveyorApiUrl).hostname;
|
|
1528
|
+
baseOptions.sandbox = {
|
|
1529
|
+
enabled: true,
|
|
1530
|
+
autoAllowBashIfSandboxed: true,
|
|
1531
|
+
allowUnsandboxedCommands: false,
|
|
1532
|
+
filesystem: {
|
|
1533
|
+
allowWrite: [`${host.config.workspaceDir}/**`],
|
|
1534
|
+
denyRead: ["/etc/shadow", "/etc/passwd", "**/.env", "**/.env.*"],
|
|
1535
|
+
denyWrite: ["**/.env", "**/.env.*", "**/node_modules/**"]
|
|
1536
|
+
},
|
|
1537
|
+
network: {
|
|
1538
|
+
allowedDomains: [apiHostname, "api.anthropic.com"],
|
|
1539
|
+
allowManagedDomainsOnly: true,
|
|
1540
|
+
allowLocalBinding: true
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return baseOptions;
|
|
1271
1545
|
}
|
|
1272
1546
|
function buildMultimodalPrompt(textPrompt, context) {
|
|
1273
1547
|
const taskImages = (context.files ?? []).filter(
|
|
@@ -1316,25 +1590,40 @@ function buildMultimodalPrompt(textPrompt, context) {
|
|
|
1316
1590
|
async function runSdkQuery(host, context, followUpContent) {
|
|
1317
1591
|
if (host.isStopped()) return;
|
|
1318
1592
|
const isPm = host.config.mode === "pm";
|
|
1319
|
-
|
|
1593
|
+
const isPmPlanning = isPm && host.activeMode === "planning";
|
|
1594
|
+
if (isPmPlanning) {
|
|
1320
1595
|
host.snapshotPlanFiles();
|
|
1321
1596
|
}
|
|
1322
1597
|
const options = buildQueryOptions(host, context);
|
|
1323
1598
|
const resume = context.claudeSessionId ?? void 0;
|
|
1324
1599
|
if (followUpContent) {
|
|
1600
|
+
const followUpText = typeof followUpContent === "string" ? followUpContent : followUpContent.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1601
|
+
const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
|
|
1602
|
+
(b) => b.type === "image"
|
|
1603
|
+
);
|
|
1325
1604
|
const textPrompt = isPm ? `${buildInitialPrompt(host.config.mode, context)}
|
|
1326
1605
|
|
|
1327
1606
|
---
|
|
1328
1607
|
|
|
1329
1608
|
The team says:
|
|
1330
|
-
${
|
|
1331
|
-
|
|
1609
|
+
${followUpText}` : followUpText;
|
|
1610
|
+
let prompt;
|
|
1611
|
+
if (isPm) {
|
|
1612
|
+
prompt = buildMultimodalPrompt(textPrompt, context);
|
|
1613
|
+
if (followUpImages.length > 0 && Array.isArray(prompt)) {
|
|
1614
|
+
prompt.push(...followUpImages);
|
|
1615
|
+
}
|
|
1616
|
+
} else if (followUpImages.length > 0) {
|
|
1617
|
+
prompt = [{ type: "text", text: textPrompt }, ...followUpImages];
|
|
1618
|
+
} else {
|
|
1619
|
+
prompt = textPrompt;
|
|
1620
|
+
}
|
|
1332
1621
|
const agentQuery = query({
|
|
1333
1622
|
prompt: typeof prompt === "string" ? prompt : host.createInputStream(prompt),
|
|
1334
1623
|
options: { ...options, resume }
|
|
1335
1624
|
});
|
|
1336
1625
|
await runWithRetry(agentQuery, context, host, options);
|
|
1337
|
-
} else if (
|
|
1626
|
+
} else if (isPmPlanning) {
|
|
1338
1627
|
return;
|
|
1339
1628
|
} else {
|
|
1340
1629
|
const initialPrompt = buildInitialPrompt(host.config.mode, context);
|
|
@@ -1345,7 +1634,7 @@ ${followUpContent}` : followUpContent;
|
|
|
1345
1634
|
});
|
|
1346
1635
|
await runWithRetry(agentQuery, context, host, options);
|
|
1347
1636
|
}
|
|
1348
|
-
if (
|
|
1637
|
+
if (isPmPlanning) {
|
|
1349
1638
|
host.syncPlanFile();
|
|
1350
1639
|
}
|
|
1351
1640
|
}
|
|
@@ -1380,7 +1669,7 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
1380
1669
|
});
|
|
1381
1670
|
return runWithRetry(freshQuery, context, host, options);
|
|
1382
1671
|
}
|
|
1383
|
-
const isApiError = error instanceof Error &&
|
|
1672
|
+
const isApiError = error instanceof Error && API_ERROR_PATTERN2.test(error.message);
|
|
1384
1673
|
if (!isApiError) throw error;
|
|
1385
1674
|
}
|
|
1386
1675
|
if (attempt >= RETRY_DELAYS_MS.length) {
|
|
@@ -1414,9 +1703,131 @@ async function runWithRetry(initialQuery, context, host, options) {
|
|
|
1414
1703
|
host.connection.emitStatus("running");
|
|
1415
1704
|
await host.callbacks.onStatusChange("running");
|
|
1416
1705
|
}
|
|
1417
|
-
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/execution/cost-tracker.ts
|
|
1709
|
+
var CostTracker = class {
|
|
1710
|
+
cumulativeCostUsd = 0;
|
|
1711
|
+
modelUsage = /* @__PURE__ */ new Map();
|
|
1712
|
+
/** Add cost from a completed query and return the running total */
|
|
1713
|
+
addQueryCost(queryCostUsd) {
|
|
1714
|
+
this.cumulativeCostUsd += queryCostUsd;
|
|
1715
|
+
return this.cumulativeCostUsd;
|
|
1716
|
+
}
|
|
1717
|
+
/** Merge per-model usage from a completed query */
|
|
1718
|
+
addModelUsage(usage) {
|
|
1719
|
+
for (const [model, data] of Object.entries(usage)) {
|
|
1720
|
+
const existing = this.modelUsage.get(model) ?? {
|
|
1721
|
+
model,
|
|
1722
|
+
inputTokens: 0,
|
|
1723
|
+
outputTokens: 0,
|
|
1724
|
+
cacheReadInputTokens: 0,
|
|
1725
|
+
cacheCreationInputTokens: 0,
|
|
1726
|
+
costUSD: 0
|
|
1727
|
+
};
|
|
1728
|
+
existing.inputTokens += data.inputTokens ?? 0;
|
|
1729
|
+
existing.outputTokens += data.outputTokens ?? 0;
|
|
1730
|
+
existing.cacheReadInputTokens += data.cacheReadInputTokens ?? 0;
|
|
1731
|
+
existing.cacheCreationInputTokens += data.cacheCreationInputTokens ?? 0;
|
|
1732
|
+
existing.costUSD += data.costUSD ?? 0;
|
|
1733
|
+
this.modelUsage.set(model, existing);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
get totalCostUsd() {
|
|
1737
|
+
return this.cumulativeCostUsd;
|
|
1738
|
+
}
|
|
1739
|
+
get modelBreakdown() {
|
|
1740
|
+
return [...this.modelUsage.values()];
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// src/runner/plan-sync.ts
|
|
1745
|
+
import { readdirSync, statSync, readFileSync } from "fs";
|
|
1746
|
+
import { homedir } from "os";
|
|
1747
|
+
import { join as join3 } from "path";
|
|
1748
|
+
var PlanSync = class {
|
|
1749
|
+
planFileSnapshot = /* @__PURE__ */ new Map();
|
|
1750
|
+
lockedPlanFile = null;
|
|
1751
|
+
workspaceDir;
|
|
1752
|
+
connection;
|
|
1753
|
+
constructor(workspaceDir, connection) {
|
|
1754
|
+
this.workspaceDir = workspaceDir;
|
|
1755
|
+
this.connection = connection;
|
|
1756
|
+
}
|
|
1757
|
+
updateWorkspaceDir(workspaceDir) {
|
|
1758
|
+
this.workspaceDir = workspaceDir;
|
|
1759
|
+
}
|
|
1760
|
+
getPlanDirs() {
|
|
1761
|
+
return [join3(homedir(), ".claude", "plans"), join3(this.workspaceDir, ".claude", "plans")];
|
|
1762
|
+
}
|
|
1763
|
+
snapshotPlanFiles() {
|
|
1764
|
+
this.planFileSnapshot.clear();
|
|
1765
|
+
this.lockedPlanFile = null;
|
|
1766
|
+
for (const plansDir of this.getPlanDirs()) {
|
|
1767
|
+
try {
|
|
1768
|
+
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1769
|
+
try {
|
|
1770
|
+
const fullPath = join3(plansDir, file);
|
|
1771
|
+
const stat = statSync(fullPath);
|
|
1772
|
+
this.planFileSnapshot.set(fullPath, stat.mtimeMs);
|
|
1773
|
+
} catch {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
syncPlanFile() {
|
|
1782
|
+
if (this.lockedPlanFile) {
|
|
1783
|
+
try {
|
|
1784
|
+
const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
|
|
1785
|
+
if (content) {
|
|
1786
|
+
this.connection.updateTaskFields({ plan: content });
|
|
1787
|
+
const fileName = this.lockedPlanFile.split("/").pop();
|
|
1788
|
+
this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
|
|
1789
|
+
}
|
|
1790
|
+
} catch {
|
|
1791
|
+
}
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
let newest = null;
|
|
1795
|
+
for (const plansDir of this.getPlanDirs()) {
|
|
1796
|
+
let files;
|
|
1797
|
+
try {
|
|
1798
|
+
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
1799
|
+
} catch {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
for (const file of files) {
|
|
1803
|
+
const fullPath = join3(plansDir, file);
|
|
1804
|
+
try {
|
|
1805
|
+
const stat = statSync(fullPath);
|
|
1806
|
+
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1807
|
+
const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
|
|
1808
|
+
if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
|
|
1809
|
+
newest = { path: fullPath, mtime: stat.mtimeMs };
|
|
1810
|
+
}
|
|
1811
|
+
} catch {
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (newest) {
|
|
1817
|
+
this.lockedPlanFile = newest.path;
|
|
1818
|
+
const content = readFileSync(newest.path, "utf-8").trim();
|
|
1819
|
+
if (content) {
|
|
1820
|
+
this.connection.updateTaskFields({ plan: content });
|
|
1821
|
+
const fileName = newest.path.split("/").pop();
|
|
1822
|
+
this.connection.postChatMessage(
|
|
1823
|
+
`Detected local plan file (${fileName}) and synced it to the task plan.`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1418
1829
|
|
|
1419
|
-
// src/runner.ts
|
|
1830
|
+
// src/runner/agent-runner.ts
|
|
1420
1831
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1421
1832
|
var AgentRunner = class _AgentRunner {
|
|
1422
1833
|
config;
|
|
@@ -1429,14 +1840,16 @@ var AgentRunner = class _AgentRunner {
|
|
|
1429
1840
|
setupLog = [];
|
|
1430
1841
|
heartbeatTimer = null;
|
|
1431
1842
|
taskContext = null;
|
|
1432
|
-
|
|
1433
|
-
|
|
1843
|
+
planSync;
|
|
1844
|
+
costTracker = new CostTracker();
|
|
1434
1845
|
worktreeActive = false;
|
|
1846
|
+
activeMode = "planning";
|
|
1435
1847
|
static MAX_SETUP_LOG_LINES = 50;
|
|
1436
1848
|
constructor(config, callbacks) {
|
|
1437
1849
|
this.config = config;
|
|
1438
1850
|
this.connection = new ConveyorConnection(config);
|
|
1439
1851
|
this.callbacks = callbacks;
|
|
1852
|
+
this.planSync = new PlanSync(config.workspaceDir, this.connection);
|
|
1440
1853
|
}
|
|
1441
1854
|
get state() {
|
|
1442
1855
|
return this._state;
|
|
@@ -1466,6 +1879,7 @@ var AgentRunner = class _AgentRunner {
|
|
|
1466
1879
|
this.connection.onChatMessage(
|
|
1467
1880
|
(message) => this.injectHumanMessage(message.content, message.files)
|
|
1468
1881
|
);
|
|
1882
|
+
this.connection.onModeChange((data) => this.handleModeChange(data.mode));
|
|
1469
1883
|
await this.setState("connected");
|
|
1470
1884
|
this.connection.sendEvent({ type: "connected", taskId: this.config.taskId });
|
|
1471
1885
|
this.startHeartbeat();
|
|
@@ -1478,11 +1892,12 @@ var AgentRunner = class _AgentRunner {
|
|
|
1478
1892
|
return;
|
|
1479
1893
|
}
|
|
1480
1894
|
}
|
|
1481
|
-
|
|
1895
|
+
initRtk();
|
|
1482
1896
|
if (this.config.mode === "pm" || process.env.CONVEYOR_USE_WORKTREE === "true") {
|
|
1483
1897
|
try {
|
|
1484
1898
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
1485
1899
|
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
1900
|
+
this.planSync.updateWorkspaceDir(worktreePath);
|
|
1486
1901
|
this.worktreeActive = true;
|
|
1487
1902
|
this.setupLog.push(`[conveyor] Using worktree: ${worktreePath}`);
|
|
1488
1903
|
} catch (error) {
|
|
@@ -1503,15 +1918,9 @@ var AgentRunner = class _AgentRunner {
|
|
|
1503
1918
|
return;
|
|
1504
1919
|
}
|
|
1505
1920
|
this.taskContext._runnerSessionId = randomUUID2();
|
|
1921
|
+
this.logEffectiveSettings();
|
|
1506
1922
|
if (process.env.CODESPACES === "true") {
|
|
1507
|
-
|
|
1508
|
-
execSync3("git fetch --unshallow", {
|
|
1509
|
-
cwd: this.config.workspaceDir,
|
|
1510
|
-
stdio: "ignore",
|
|
1511
|
-
timeout: 6e4
|
|
1512
|
-
});
|
|
1513
|
-
} catch {
|
|
1514
|
-
}
|
|
1923
|
+
unshallowRepo(this.config.workspaceDir);
|
|
1515
1924
|
}
|
|
1516
1925
|
if (process.env.CODESPACES === "true" && this.taskContext.baseBranch) {
|
|
1517
1926
|
const result = cleanDevcontainerFromGit(
|
|
@@ -1527,6 +1936,7 @@ var AgentRunner = class _AgentRunner {
|
|
|
1527
1936
|
try {
|
|
1528
1937
|
const worktreePath = ensureWorktree(this.config.workspaceDir, this.config.taskId);
|
|
1529
1938
|
this.config = { ...this.config, workspaceDir: worktreePath };
|
|
1939
|
+
this.planSync.updateWorkspaceDir(worktreePath);
|
|
1530
1940
|
this.worktreeActive = true;
|
|
1531
1941
|
this.setupLog.push(`[conveyor] Using worktree (from task config): ${worktreePath}`);
|
|
1532
1942
|
} catch (error) {
|
|
@@ -1610,6 +2020,24 @@ The agent cannot start until this is resolved.`
|
|
|
1610
2020
|
return false;
|
|
1611
2021
|
}
|
|
1612
2022
|
}
|
|
2023
|
+
logEffectiveSettings() {
|
|
2024
|
+
if (!this.taskContext) return;
|
|
2025
|
+
const s = this.taskContext.agentSettings ?? this.config.agentSettings ?? {};
|
|
2026
|
+
const model = this.taskContext.model || this.config.model;
|
|
2027
|
+
const thinking = s.thinking?.type === "enabled" ? `enabled(${s.thinking.budgetTokens ?? "?"})` : s.thinking?.type ?? "default";
|
|
2028
|
+
const parts = [
|
|
2029
|
+
`model=${model}`,
|
|
2030
|
+
`mode=${this.config.mode ?? "task"}`,
|
|
2031
|
+
`effort=${s.effort ?? "default"}`,
|
|
2032
|
+
`thinking=${thinking}`,
|
|
2033
|
+
`maxBudget=$${s.maxBudgetUsd ?? 50}`,
|
|
2034
|
+
`maxTurns=${s.maxTurns ?? "unlimited"}`
|
|
2035
|
+
];
|
|
2036
|
+
if (s.betas?.length) parts.push(`betas=${s.betas.join(",")}`);
|
|
2037
|
+
if (s.disallowedTools?.length) parts.push(`disallowed=[${s.disallowedTools.join(",")}]`);
|
|
2038
|
+
if (s.enableFileCheckpointing) parts.push(`checkpointing=on`);
|
|
2039
|
+
console.log(`[conveyor-agent] ${parts.join(", ")}`);
|
|
2040
|
+
}
|
|
1613
2041
|
pushSetupLog(line) {
|
|
1614
2042
|
this.setupLog.push(line);
|
|
1615
2043
|
if (this.setupLog.length > _AgentRunner.MAX_SETUP_LOG_LINES) {
|
|
@@ -1634,13 +2062,6 @@ The agent cannot start until this is resolved.`
|
|
|
1634
2062
|
});
|
|
1635
2063
|
}
|
|
1636
2064
|
}
|
|
1637
|
-
initRtk() {
|
|
1638
|
-
try {
|
|
1639
|
-
execSync3("rtk --version", { stdio: "ignore" });
|
|
1640
|
-
execSync3("rtk init --global --auto-patch", { stdio: "ignore" });
|
|
1641
|
-
} catch {
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
2065
|
injectHumanMessage(content, files) {
|
|
1645
2066
|
let messageContent;
|
|
1646
2067
|
if (files?.length) {
|
|
@@ -1706,11 +2127,15 @@ ${f.content}
|
|
|
1706
2127
|
async waitForUserContent() {
|
|
1707
2128
|
if (this.pendingMessages.length > 0) {
|
|
1708
2129
|
const next = this.pendingMessages.shift();
|
|
1709
|
-
|
|
2130
|
+
const content2 = next?.message.content;
|
|
2131
|
+
if (!content2) return null;
|
|
2132
|
+
return content2;
|
|
1710
2133
|
}
|
|
1711
2134
|
const msg = await this.waitForMessage();
|
|
1712
2135
|
if (!msg) return null;
|
|
1713
|
-
|
|
2136
|
+
const content = msg.message.content;
|
|
2137
|
+
if (!content) return null;
|
|
2138
|
+
return content;
|
|
1714
2139
|
}
|
|
1715
2140
|
async *createInputStream(initialPrompt) {
|
|
1716
2141
|
const makeUserMessage = (content) => ({
|
|
@@ -1735,93 +2160,31 @@ ${f.content}
|
|
|
1735
2160
|
yield msg;
|
|
1736
2161
|
}
|
|
1737
2162
|
}
|
|
1738
|
-
getPlanDirs() {
|
|
1739
|
-
return [
|
|
1740
|
-
join3(homedir(), ".claude", "plans"),
|
|
1741
|
-
join3(this.config.workspaceDir, ".claude", "plans")
|
|
1742
|
-
];
|
|
1743
|
-
}
|
|
1744
|
-
/**
|
|
1745
|
-
* Snapshot current plan files so syncPlanFile can distinguish files created
|
|
1746
|
-
* by THIS session from ones created by a concurrent agent.
|
|
1747
|
-
*/
|
|
1748
|
-
snapshotPlanFiles() {
|
|
1749
|
-
this.planFileSnapshot.clear();
|
|
1750
|
-
this.lockedPlanFile = null;
|
|
1751
|
-
for (const plansDir of this.getPlanDirs()) {
|
|
1752
|
-
try {
|
|
1753
|
-
for (const file of readdirSync(plansDir).filter((f) => f.endsWith(".md"))) {
|
|
1754
|
-
try {
|
|
1755
|
-
const fullPath = join3(plansDir, file);
|
|
1756
|
-
const stat = statSync(fullPath);
|
|
1757
|
-
this.planFileSnapshot.set(fullPath, stat.mtimeMs);
|
|
1758
|
-
} catch {
|
|
1759
|
-
continue;
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
} catch {
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
syncPlanFile() {
|
|
1767
|
-
if (this.lockedPlanFile) {
|
|
1768
|
-
try {
|
|
1769
|
-
const content = readFileSync(this.lockedPlanFile, "utf-8").trim();
|
|
1770
|
-
if (content) {
|
|
1771
|
-
this.connection.updateTaskFields({ plan: content });
|
|
1772
|
-
const fileName = this.lockedPlanFile.split("/").pop();
|
|
1773
|
-
this.connection.postChatMessage(`Synced local plan file (${fileName}) to the task plan.`);
|
|
1774
|
-
}
|
|
1775
|
-
} catch {
|
|
1776
|
-
}
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
let newest = null;
|
|
1780
|
-
for (const plansDir of this.getPlanDirs()) {
|
|
1781
|
-
let files;
|
|
1782
|
-
try {
|
|
1783
|
-
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
1784
|
-
} catch {
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
for (const file of files) {
|
|
1788
|
-
const fullPath = join3(plansDir, file);
|
|
1789
|
-
try {
|
|
1790
|
-
const stat = statSync(fullPath);
|
|
1791
|
-
const prevMtime = this.planFileSnapshot.get(fullPath);
|
|
1792
|
-
const isNew = prevMtime === void 0 || stat.mtimeMs > prevMtime;
|
|
1793
|
-
if (isNew && (!newest || stat.mtimeMs > newest.mtime)) {
|
|
1794
|
-
newest = { path: fullPath, mtime: stat.mtimeMs };
|
|
1795
|
-
}
|
|
1796
|
-
} catch {
|
|
1797
|
-
continue;
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
if (newest) {
|
|
1802
|
-
this.lockedPlanFile = newest.path;
|
|
1803
|
-
const content = readFileSync(newest.path, "utf-8").trim();
|
|
1804
|
-
if (content) {
|
|
1805
|
-
this.connection.updateTaskFields({ plan: content });
|
|
1806
|
-
const fileName = newest.path.split("/").pop();
|
|
1807
|
-
this.connection.postChatMessage(
|
|
1808
|
-
`Detected local plan file (${fileName}) and synced it to the task plan.`
|
|
1809
|
-
);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
2163
|
asQueryHost() {
|
|
2164
|
+
const getActiveMode = () => this.activeMode;
|
|
1814
2165
|
return {
|
|
1815
2166
|
config: this.config,
|
|
1816
2167
|
connection: this.connection,
|
|
1817
2168
|
callbacks: this.callbacks,
|
|
1818
2169
|
setupLog: this.setupLog,
|
|
2170
|
+
costTracker: this.costTracker,
|
|
2171
|
+
get activeMode() {
|
|
2172
|
+
return getActiveMode();
|
|
2173
|
+
},
|
|
1819
2174
|
isStopped: () => this.stopped,
|
|
1820
2175
|
createInputStream: (prompt) => this.createInputStream(prompt),
|
|
1821
|
-
snapshotPlanFiles: () => this.snapshotPlanFiles(),
|
|
1822
|
-
syncPlanFile: () => this.syncPlanFile()
|
|
2176
|
+
snapshotPlanFiles: () => this.planSync.snapshotPlanFiles(),
|
|
2177
|
+
syncPlanFile: () => this.planSync.syncPlanFile()
|
|
1823
2178
|
};
|
|
1824
2179
|
}
|
|
2180
|
+
handleModeChange(mode) {
|
|
2181
|
+
if (this.config.mode !== "pm") return;
|
|
2182
|
+
this.activeMode = mode;
|
|
2183
|
+
this.connection.emitModeChanged(mode);
|
|
2184
|
+
this.connection.postChatMessage(
|
|
2185
|
+
`Mode switched to **${mode}**${mode === "active" ? " \u2014 I now have direct coding access." : " \u2014 back to planning only."}`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
1825
2188
|
stop() {
|
|
1826
2189
|
this.stopped = true;
|
|
1827
2190
|
if (this.inputResolver) {
|
|
@@ -1831,163 +2194,13 @@ ${f.content}
|
|
|
1831
2194
|
}
|
|
1832
2195
|
};
|
|
1833
2196
|
|
|
1834
|
-
// src/project-
|
|
1835
|
-
import { io as io2 } from "socket.io-client";
|
|
1836
|
-
var ProjectConnection = class {
|
|
1837
|
-
socket = null;
|
|
1838
|
-
config;
|
|
1839
|
-
taskAssignmentCallback = null;
|
|
1840
|
-
stopTaskCallback = null;
|
|
1841
|
-
shutdownCallback = null;
|
|
1842
|
-
chatMessageCallback = null;
|
|
1843
|
-
earlyChatMessages = [];
|
|
1844
|
-
constructor(config) {
|
|
1845
|
-
this.config = config;
|
|
1846
|
-
}
|
|
1847
|
-
connect() {
|
|
1848
|
-
return new Promise((resolve2, reject) => {
|
|
1849
|
-
let settled = false;
|
|
1850
|
-
let attempts = 0;
|
|
1851
|
-
const maxInitialAttempts = 30;
|
|
1852
|
-
this.socket = io2(this.config.apiUrl, {
|
|
1853
|
-
auth: { projectToken: this.config.projectToken },
|
|
1854
|
-
transports: ["websocket"],
|
|
1855
|
-
reconnection: true,
|
|
1856
|
-
reconnectionAttempts: Infinity,
|
|
1857
|
-
reconnectionDelay: 2e3,
|
|
1858
|
-
reconnectionDelayMax: 3e4,
|
|
1859
|
-
randomizationFactor: 0.3,
|
|
1860
|
-
extraHeaders: {
|
|
1861
|
-
"ngrok-skip-browser-warning": "true"
|
|
1862
|
-
}
|
|
1863
|
-
});
|
|
1864
|
-
this.socket.on("projectRunner:assignTask", (data) => {
|
|
1865
|
-
if (this.taskAssignmentCallback) {
|
|
1866
|
-
this.taskAssignmentCallback(data);
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
1869
|
-
this.socket.on("projectRunner:stopTask", (data) => {
|
|
1870
|
-
if (this.stopTaskCallback) {
|
|
1871
|
-
this.stopTaskCallback(data);
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
this.socket.on("projectRunner:shutdown", () => {
|
|
1875
|
-
if (this.shutdownCallback) {
|
|
1876
|
-
this.shutdownCallback();
|
|
1877
|
-
}
|
|
1878
|
-
});
|
|
1879
|
-
this.socket.on("projectRunner:incomingChatMessage", (msg) => {
|
|
1880
|
-
if (this.chatMessageCallback) {
|
|
1881
|
-
this.chatMessageCallback(msg);
|
|
1882
|
-
} else {
|
|
1883
|
-
this.earlyChatMessages.push(msg);
|
|
1884
|
-
}
|
|
1885
|
-
});
|
|
1886
|
-
this.socket.on("connect", () => {
|
|
1887
|
-
if (!settled) {
|
|
1888
|
-
settled = true;
|
|
1889
|
-
resolve2();
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
this.socket.io.on("reconnect_attempt", () => {
|
|
1893
|
-
attempts++;
|
|
1894
|
-
if (!settled && attempts >= maxInitialAttempts) {
|
|
1895
|
-
settled = true;
|
|
1896
|
-
reject(new Error(`Failed to connect after ${maxInitialAttempts} attempts`));
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
onTaskAssignment(callback) {
|
|
1902
|
-
this.taskAssignmentCallback = callback;
|
|
1903
|
-
}
|
|
1904
|
-
onStopTask(callback) {
|
|
1905
|
-
this.stopTaskCallback = callback;
|
|
1906
|
-
}
|
|
1907
|
-
onShutdown(callback) {
|
|
1908
|
-
this.shutdownCallback = callback;
|
|
1909
|
-
}
|
|
1910
|
-
onChatMessage(callback) {
|
|
1911
|
-
this.chatMessageCallback = callback;
|
|
1912
|
-
for (const msg of this.earlyChatMessages) {
|
|
1913
|
-
callback(msg);
|
|
1914
|
-
}
|
|
1915
|
-
this.earlyChatMessages = [];
|
|
1916
|
-
}
|
|
1917
|
-
sendHeartbeat() {
|
|
1918
|
-
if (!this.socket) return;
|
|
1919
|
-
this.socket.emit("projectRunner:heartbeat", {});
|
|
1920
|
-
}
|
|
1921
|
-
emitTaskStarted(taskId) {
|
|
1922
|
-
if (!this.socket) return;
|
|
1923
|
-
this.socket.emit("projectRunner:taskStarted", { taskId });
|
|
1924
|
-
}
|
|
1925
|
-
emitTaskStopped(taskId, reason) {
|
|
1926
|
-
if (!this.socket) return;
|
|
1927
|
-
this.socket.emit("projectRunner:taskStopped", { taskId, reason });
|
|
1928
|
-
}
|
|
1929
|
-
emitEvent(event) {
|
|
1930
|
-
if (!this.socket) return;
|
|
1931
|
-
this.socket.emit("conveyor:projectAgentEvent", event);
|
|
1932
|
-
}
|
|
1933
|
-
emitChatMessage(content) {
|
|
1934
|
-
const socket = this.socket;
|
|
1935
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1936
|
-
return new Promise((resolve2, reject) => {
|
|
1937
|
-
socket.emit(
|
|
1938
|
-
"conveyor:projectAgentChatMessage",
|
|
1939
|
-
{ content },
|
|
1940
|
-
(response) => {
|
|
1941
|
-
if (response.success) resolve2();
|
|
1942
|
-
else reject(new Error(response.error ?? "Failed to send chat message"));
|
|
1943
|
-
}
|
|
1944
|
-
);
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
emitAgentStatus(status) {
|
|
1948
|
-
if (!this.socket) return;
|
|
1949
|
-
this.socket.emit("conveyor:projectAgentStatus", { status });
|
|
1950
|
-
}
|
|
1951
|
-
fetchAgentContext() {
|
|
1952
|
-
const socket = this.socket;
|
|
1953
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1954
|
-
return new Promise((resolve2, reject) => {
|
|
1955
|
-
socket.emit(
|
|
1956
|
-
"projectRunner:getAgentContext",
|
|
1957
|
-
(response) => {
|
|
1958
|
-
if (response.success) resolve2(response.data ?? null);
|
|
1959
|
-
else reject(new Error(response.error ?? "Failed to fetch agent context"));
|
|
1960
|
-
}
|
|
1961
|
-
);
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
fetchChatHistory(limit) {
|
|
1965
|
-
const socket = this.socket;
|
|
1966
|
-
if (!socket) return Promise.reject(new Error("Not connected"));
|
|
1967
|
-
return new Promise((resolve2, reject) => {
|
|
1968
|
-
socket.emit(
|
|
1969
|
-
"projectRunner:getChatHistory",
|
|
1970
|
-
{ limit },
|
|
1971
|
-
(response) => {
|
|
1972
|
-
if (response.success && response.data) resolve2(response.data);
|
|
1973
|
-
else reject(new Error(response.error ?? "Failed to fetch chat history"));
|
|
1974
|
-
}
|
|
1975
|
-
);
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
disconnect() {
|
|
1979
|
-
this.socket?.disconnect();
|
|
1980
|
-
this.socket = null;
|
|
1981
|
-
}
|
|
1982
|
-
};
|
|
1983
|
-
|
|
1984
|
-
// src/project-runner.ts
|
|
2197
|
+
// src/runner/project-runner.ts
|
|
1985
2198
|
import { fork } from "child_process";
|
|
1986
2199
|
import { execSync as execSync4 } from "child_process";
|
|
1987
2200
|
import * as path from "path";
|
|
1988
2201
|
import { fileURLToPath } from "url";
|
|
1989
2202
|
|
|
1990
|
-
// src/project-chat-handler.ts
|
|
2203
|
+
// src/runner/project-chat-handler.ts
|
|
1991
2204
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1992
2205
|
var FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
1993
2206
|
function buildSystemPrompt2(projectDir, agentCtx) {
|
|
@@ -2073,33 +2286,31 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
2073
2286
|
const turnToolCalls = [];
|
|
2074
2287
|
let isTyping = false;
|
|
2075
2288
|
for await (const event of events) {
|
|
2076
|
-
|
|
2077
|
-
if (eventType === "assistant") {
|
|
2289
|
+
if (event.type === "assistant") {
|
|
2078
2290
|
if (!isTyping) {
|
|
2079
2291
|
setTimeout(() => connection.emitEvent({ type: "agent_typing_start" }), 200);
|
|
2080
2292
|
isTyping = true;
|
|
2081
2293
|
}
|
|
2082
|
-
const
|
|
2083
|
-
const content =
|
|
2294
|
+
const assistantEvent = event;
|
|
2295
|
+
const { content } = assistantEvent.message;
|
|
2084
2296
|
for (const block of content) {
|
|
2085
2297
|
if (block.type === "text") {
|
|
2086
2298
|
responseParts.push(block.text);
|
|
2087
2299
|
} else if (block.type === "tool_use") {
|
|
2088
|
-
const name = block.name;
|
|
2089
2300
|
const inputStr = typeof block.input === "string" ? block.input : JSON.stringify(block.input);
|
|
2090
2301
|
turnToolCalls.push({
|
|
2091
|
-
tool: name,
|
|
2302
|
+
tool: block.name,
|
|
2092
2303
|
input: inputStr.slice(0, 1e4),
|
|
2093
2304
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2094
2305
|
});
|
|
2095
|
-
console.log(`[project-chat] [tool_use] ${name}`);
|
|
2306
|
+
console.log(`[project-chat] [tool_use] ${block.name}`);
|
|
2096
2307
|
}
|
|
2097
2308
|
}
|
|
2098
2309
|
if (turnToolCalls.length > 0) {
|
|
2099
2310
|
connection.emitEvent({ type: "activity_block", events: [...turnToolCalls] });
|
|
2100
2311
|
turnToolCalls.length = 0;
|
|
2101
2312
|
}
|
|
2102
|
-
} else if (
|
|
2313
|
+
} else if (event.type === "result") {
|
|
2103
2314
|
if (isTyping) {
|
|
2104
2315
|
connection.emitEvent({ type: "agent_typing_stop" });
|
|
2105
2316
|
isTyping = false;
|
|
@@ -2130,7 +2341,7 @@ async function handleProjectChatMessage(message, connection, projectDir) {
|
|
|
2130
2341
|
}
|
|
2131
2342
|
}
|
|
2132
2343
|
|
|
2133
|
-
// src/project-runner.ts
|
|
2344
|
+
// src/runner/project-runner.ts
|
|
2134
2345
|
var __filename = fileURLToPath(import.meta.url);
|
|
2135
2346
|
var __dirname = path.dirname(__filename);
|
|
2136
2347
|
var HEARTBEAT_INTERVAL_MS2 = 3e4;
|
|
@@ -2317,15 +2528,80 @@ var ProjectRunner = class {
|
|
|
2317
2528
|
}
|
|
2318
2529
|
};
|
|
2319
2530
|
|
|
2531
|
+
// src/runner/file-cache.ts
|
|
2532
|
+
var DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
|
|
2533
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
2534
|
+
var FileCache = class {
|
|
2535
|
+
cache = /* @__PURE__ */ new Map();
|
|
2536
|
+
currentSize = 0;
|
|
2537
|
+
maxSizeBytes;
|
|
2538
|
+
ttlMs;
|
|
2539
|
+
constructor(maxSizeBytes = DEFAULT_MAX_SIZE_BYTES, ttlMs = DEFAULT_TTL_MS) {
|
|
2540
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
2541
|
+
this.ttlMs = ttlMs;
|
|
2542
|
+
}
|
|
2543
|
+
get(fileId) {
|
|
2544
|
+
const entry = this.cache.get(fileId);
|
|
2545
|
+
if (!entry) return null;
|
|
2546
|
+
if (Date.now() - entry.createdAt > this.ttlMs) {
|
|
2547
|
+
this.delete(fileId);
|
|
2548
|
+
return null;
|
|
2549
|
+
}
|
|
2550
|
+
this.cache.delete(fileId);
|
|
2551
|
+
this.cache.set(fileId, entry);
|
|
2552
|
+
return entry;
|
|
2553
|
+
}
|
|
2554
|
+
set(fileId, content, mimeType, fileName) {
|
|
2555
|
+
if (this.cache.has(fileId)) {
|
|
2556
|
+
this.delete(fileId);
|
|
2557
|
+
}
|
|
2558
|
+
const size = content.byteLength;
|
|
2559
|
+
if (size > this.maxSizeBytes) return;
|
|
2560
|
+
while (this.currentSize + size > this.maxSizeBytes && this.cache.size > 0) {
|
|
2561
|
+
const oldestKey = this.cache.keys().next().value;
|
|
2562
|
+
if (oldestKey !== void 0) {
|
|
2563
|
+
this.delete(oldestKey);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
this.cache.set(fileId, {
|
|
2567
|
+
content,
|
|
2568
|
+
mimeType,
|
|
2569
|
+
fileName,
|
|
2570
|
+
createdAt: Date.now(),
|
|
2571
|
+
size
|
|
2572
|
+
});
|
|
2573
|
+
this.currentSize += size;
|
|
2574
|
+
}
|
|
2575
|
+
delete(fileId) {
|
|
2576
|
+
const entry = this.cache.get(fileId);
|
|
2577
|
+
if (entry) {
|
|
2578
|
+
this.currentSize -= entry.size;
|
|
2579
|
+
this.cache.delete(fileId);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
clear() {
|
|
2583
|
+
this.cache.clear();
|
|
2584
|
+
this.currentSize = 0;
|
|
2585
|
+
}
|
|
2586
|
+
get stats() {
|
|
2587
|
+
return {
|
|
2588
|
+
entries: this.cache.size,
|
|
2589
|
+
sizeBytes: this.currentSize,
|
|
2590
|
+
maxSizeBytes: this.maxSizeBytes
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2320
2595
|
export {
|
|
2321
2596
|
ConveyorConnection,
|
|
2597
|
+
ProjectConnection,
|
|
2322
2598
|
loadConveyorConfig,
|
|
2323
2599
|
runSetupCommand,
|
|
2324
2600
|
runStartCommand,
|
|
2325
2601
|
ensureWorktree,
|
|
2326
2602
|
removeWorktree,
|
|
2327
2603
|
AgentRunner,
|
|
2328
|
-
|
|
2329
|
-
|
|
2604
|
+
ProjectRunner,
|
|
2605
|
+
FileCache
|
|
2330
2606
|
};
|
|
2331
|
-
//# sourceMappingURL=chunk-
|
|
2607
|
+
//# sourceMappingURL=chunk-5UYKPYFQ.js.map
|