@supaku/agentfactory 0.1.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/LICENSE +21 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +128 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +406 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +469 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +246 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +2525 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +73 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +301 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +145 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +131 -0
- package/dist/src/orchestrator/types.d.ts +205 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +267 -0
- package/dist/src/providers/codex-provider.d.ts +21 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +25 -0
- package/dist/src/providers/index.d.ts +42 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +77 -0
- package/dist/src/providers/types.d.ts +147 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/package.json +63 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-based Activity Emitter
|
|
3
|
+
*
|
|
4
|
+
* Emits Claude stream events to Linear via the agent app's API endpoint.
|
|
5
|
+
* This is used when the orchestrator runs remotely (as a worker) and needs
|
|
6
|
+
* to proxy activities through the Vercel app which has OAuth tokens.
|
|
7
|
+
*
|
|
8
|
+
* The agent app API endpoint (/api/sessions/[id]/activity) retrieves the
|
|
9
|
+
* OAuth token from Redis using the session's organizationId and forwards
|
|
10
|
+
* the activity to Linear's Agent API.
|
|
11
|
+
*
|
|
12
|
+
* Mapping:
|
|
13
|
+
* - assistant message → response (persisted, user-directed communication)
|
|
14
|
+
* - tool_use → action (ephemeral)
|
|
15
|
+
* - tool_result → action (ephemeral)
|
|
16
|
+
* - result → response (persisted)
|
|
17
|
+
* - error → error (persisted)
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MIN_INTERVAL = 500;
|
|
20
|
+
const DEFAULT_MAX_OUTPUT_LENGTH = 2000;
|
|
21
|
+
/**
|
|
22
|
+
* API Activity Emitter
|
|
23
|
+
*
|
|
24
|
+
* Handles rate-limited emission of Claude events to Linear via API proxy.
|
|
25
|
+
*/
|
|
26
|
+
export class ApiActivityEmitter {
|
|
27
|
+
sessionId;
|
|
28
|
+
workerId; // Mutable to allow update after worker re-registration
|
|
29
|
+
apiBaseUrl;
|
|
30
|
+
apiKey;
|
|
31
|
+
minInterval;
|
|
32
|
+
maxOutputLength;
|
|
33
|
+
includeTimestamps;
|
|
34
|
+
onActivityEmitted;
|
|
35
|
+
onActivityThrottled;
|
|
36
|
+
onActivityError;
|
|
37
|
+
onProgressPosted;
|
|
38
|
+
lastEmitTime = 0;
|
|
39
|
+
queue = [];
|
|
40
|
+
flushTimer = null;
|
|
41
|
+
isProcessing = false;
|
|
42
|
+
// Track reported tool error signatures for deduplication
|
|
43
|
+
reportedToolErrors = new Set();
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.sessionId = config.sessionId;
|
|
46
|
+
this.workerId = config.workerId;
|
|
47
|
+
this.apiBaseUrl = config.apiBaseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
48
|
+
this.apiKey = config.apiKey;
|
|
49
|
+
this.minInterval = config.minInterval ?? DEFAULT_MIN_INTERVAL;
|
|
50
|
+
this.maxOutputLength = config.maxOutputLength ?? DEFAULT_MAX_OUTPUT_LENGTH;
|
|
51
|
+
this.includeTimestamps = config.includeTimestamps ?? false;
|
|
52
|
+
this.onActivityEmitted = config.onActivityEmitted;
|
|
53
|
+
this.onActivityThrottled = config.onActivityThrottled;
|
|
54
|
+
this.onActivityError = config.onActivityError;
|
|
55
|
+
this.onProgressPosted = config.onProgressPosted;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Update the worker ID used for API requests.
|
|
59
|
+
* Called after worker re-registration to ensure activities are attributed
|
|
60
|
+
* to the new worker ID and pass ownership checks.
|
|
61
|
+
*/
|
|
62
|
+
updateWorkerId(newWorkerId) {
|
|
63
|
+
this.workerId = newWorkerId;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the current worker ID
|
|
67
|
+
*/
|
|
68
|
+
getWorkerId() {
|
|
69
|
+
return this.workerId;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Emit a thought activity (persistent by default for visibility in Linear)
|
|
73
|
+
*/
|
|
74
|
+
async emitThought(content, ephemeral = false) {
|
|
75
|
+
await this.queueActivity({
|
|
76
|
+
type: 'thought',
|
|
77
|
+
content,
|
|
78
|
+
ephemeral,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Emit a tool use activity (ephemeral by default)
|
|
83
|
+
*/
|
|
84
|
+
async emitToolUse(tool, input, ephemeral = true) {
|
|
85
|
+
const inputSummary = this.summarizeToolInput(tool, input);
|
|
86
|
+
await this.queueActivity({
|
|
87
|
+
type: 'action',
|
|
88
|
+
content: `${tool}: ${inputSummary}`,
|
|
89
|
+
ephemeral,
|
|
90
|
+
toolName: tool,
|
|
91
|
+
toolInput: input,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit a response activity (persisted)
|
|
96
|
+
*/
|
|
97
|
+
async emitResponse(content) {
|
|
98
|
+
await this.queueActivity({
|
|
99
|
+
type: 'response',
|
|
100
|
+
content,
|
|
101
|
+
ephemeral: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Emit an error activity (persisted)
|
|
106
|
+
*/
|
|
107
|
+
async emitError(error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : error;
|
|
109
|
+
await this.queueActivity({
|
|
110
|
+
type: 'error',
|
|
111
|
+
content: message,
|
|
112
|
+
ephemeral: false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Post a progress update comment to the Linear issue thread.
|
|
117
|
+
* Unlike activities which are ephemeral, progress updates are
|
|
118
|
+
* persisted as comments and visible in the issue thread.
|
|
119
|
+
*
|
|
120
|
+
* @param milestone - The type of progress milestone (e.g., 'started', 'completed')
|
|
121
|
+
* @param message - The progress message to post
|
|
122
|
+
*/
|
|
123
|
+
async postProgress(milestone, message) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(`${this.apiBaseUrl}/api/sessions/${this.sessionId}/progress`, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
workerId: this.workerId,
|
|
133
|
+
milestone,
|
|
134
|
+
message,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const errorText = await response.text();
|
|
139
|
+
throw new Error(`API error ${response.status}: ${errorText}`);
|
|
140
|
+
}
|
|
141
|
+
const result = (await response.json());
|
|
142
|
+
if (result.posted) {
|
|
143
|
+
this.onProgressPosted?.(milestone, message);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.warn(`Progress not posted: ${result.reason ?? 'unknown'}`);
|
|
147
|
+
}
|
|
148
|
+
return result.posted;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
152
|
+
console.error(`Failed to post progress (${milestone}):`, err);
|
|
153
|
+
this.onActivityError?.('progress', err);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Report a tool error as a Linear issue for tracking and improvement.
|
|
159
|
+
* Creates a bug in the Agent project backlog via API.
|
|
160
|
+
*
|
|
161
|
+
* @param toolName - Name of the tool that errored
|
|
162
|
+
* @param errorMessage - The error message
|
|
163
|
+
* @param context - Additional context about the error
|
|
164
|
+
* @returns The created issue, or null if creation failed or was deduplicated
|
|
165
|
+
*/
|
|
166
|
+
async reportToolError(toolName, errorMessage, context) {
|
|
167
|
+
// Deduplicate using tool name + first 100 chars of error
|
|
168
|
+
const signature = `${toolName}:${errorMessage.substring(0, 100)}`;
|
|
169
|
+
if (this.reportedToolErrors.has(signature)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
this.reportedToolErrors.add(signature);
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(`${this.apiBaseUrl}/api/sessions/${this.sessionId}/tool-error`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: {
|
|
177
|
+
'Content-Type': 'application/json',
|
|
178
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
workerId: this.workerId,
|
|
182
|
+
toolName,
|
|
183
|
+
errorMessage,
|
|
184
|
+
context,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const errorText = await response.text();
|
|
189
|
+
throw new Error(`API error ${response.status}: ${errorText}`);
|
|
190
|
+
}
|
|
191
|
+
const result = (await response.json());
|
|
192
|
+
if (result.created && result.issue) {
|
|
193
|
+
return result.issue;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
199
|
+
console.error('[ApiActivityEmitter] Failed to report tool error:', err);
|
|
200
|
+
this.onActivityError?.('tool-error', err);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get Claude stream handlers that emit to Linear via API
|
|
206
|
+
*/
|
|
207
|
+
getStreamHandlers() {
|
|
208
|
+
return {
|
|
209
|
+
onAssistant: async (event) => {
|
|
210
|
+
// Skip partial messages (streaming updates)
|
|
211
|
+
if (event.partial)
|
|
212
|
+
return;
|
|
213
|
+
// Assistant messages are user-directed communication, emit as response (persisted)
|
|
214
|
+
await this.queueActivity({
|
|
215
|
+
type: 'response',
|
|
216
|
+
content: event.message,
|
|
217
|
+
ephemeral: false,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
onToolUse: async (event) => {
|
|
221
|
+
const inputSummary = this.summarizeToolInput(event.tool, event.input);
|
|
222
|
+
await this.queueActivity({
|
|
223
|
+
type: 'action',
|
|
224
|
+
content: `${event.tool}: ${inputSummary}`,
|
|
225
|
+
ephemeral: true,
|
|
226
|
+
toolName: event.tool,
|
|
227
|
+
toolInput: event.input,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
onToolResult: async (event) => {
|
|
231
|
+
const output = this.truncateOutput(event.output);
|
|
232
|
+
const prefix = event.is_error ? 'Error' : 'Result';
|
|
233
|
+
await this.queueActivity({
|
|
234
|
+
type: 'action',
|
|
235
|
+
content: `${event.tool} ${prefix}: ${output}`,
|
|
236
|
+
ephemeral: true,
|
|
237
|
+
toolName: event.tool,
|
|
238
|
+
toolOutput: output,
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
onResult: async (event) => {
|
|
242
|
+
// Final result - persisted as response
|
|
243
|
+
const content = this.formatResultContent(event);
|
|
244
|
+
await this.queueActivity({
|
|
245
|
+
type: 'response',
|
|
246
|
+
content,
|
|
247
|
+
ephemeral: false,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
onError: async (event) => {
|
|
251
|
+
// Errors are persisted
|
|
252
|
+
await this.queueActivity({
|
|
253
|
+
type: 'error',
|
|
254
|
+
content: event.error.message,
|
|
255
|
+
ephemeral: false,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
// Note: Todo/plan updates are not supported via API emitter
|
|
259
|
+
// Plans would need a separate API endpoint
|
|
260
|
+
onTodo: async (_newTodos) => {
|
|
261
|
+
// No-op - plan updates not supported via API yet
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Queue an activity for emission with rate limiting
|
|
267
|
+
*/
|
|
268
|
+
async queueActivity(activity) {
|
|
269
|
+
this.queue.push(activity);
|
|
270
|
+
// Schedule flush if not already scheduled
|
|
271
|
+
if (!this.flushTimer && !this.isProcessing) {
|
|
272
|
+
const timeSinceLastEmit = Date.now() - this.lastEmitTime;
|
|
273
|
+
const delay = Math.max(0, this.minInterval - timeSinceLastEmit);
|
|
274
|
+
this.flushTimer = setTimeout(() => {
|
|
275
|
+
this.flushTimer = null;
|
|
276
|
+
this.processQueue();
|
|
277
|
+
}, delay);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Process queued activities
|
|
282
|
+
*/
|
|
283
|
+
async processQueue() {
|
|
284
|
+
if (this.isProcessing || this.queue.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
this.isProcessing = true;
|
|
287
|
+
try {
|
|
288
|
+
// Merge similar consecutive activities to reduce API calls
|
|
289
|
+
const merged = this.mergeQueuedActivities();
|
|
290
|
+
for (const activity of merged) {
|
|
291
|
+
await this.emitActivity(activity);
|
|
292
|
+
this.lastEmitTime = Date.now();
|
|
293
|
+
// Small delay between emissions to avoid rate limits
|
|
294
|
+
if (merged.length > 1) {
|
|
295
|
+
await this.delay(100);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.isProcessing = false;
|
|
301
|
+
// If more activities were queued during processing, schedule another flush
|
|
302
|
+
if (this.queue.length > 0 && !this.flushTimer) {
|
|
303
|
+
this.flushTimer = setTimeout(() => {
|
|
304
|
+
this.flushTimer = null;
|
|
305
|
+
this.processQueue();
|
|
306
|
+
}, this.minInterval);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Merge consecutive similar activities in the queue
|
|
312
|
+
*/
|
|
313
|
+
mergeQueuedActivities() {
|
|
314
|
+
const activities = [...this.queue];
|
|
315
|
+
this.queue = [];
|
|
316
|
+
if (activities.length <= 1)
|
|
317
|
+
return activities;
|
|
318
|
+
const merged = [];
|
|
319
|
+
let current = activities[0];
|
|
320
|
+
for (let i = 1; i < activities.length; i++) {
|
|
321
|
+
const next = activities[i];
|
|
322
|
+
// Merge consecutive thoughts
|
|
323
|
+
if (current.type === 'thought' &&
|
|
324
|
+
next.type === 'thought' &&
|
|
325
|
+
current.ephemeral === next.ephemeral) {
|
|
326
|
+
current = {
|
|
327
|
+
...current,
|
|
328
|
+
content: `${current.content}\n\n${next.content}`,
|
|
329
|
+
};
|
|
330
|
+
this.onActivityThrottled?.('thought', next.content);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Merge consecutive tool results for same tool
|
|
334
|
+
if (current.type === 'action' &&
|
|
335
|
+
next.type === 'action' &&
|
|
336
|
+
current.toolName === next.toolName &&
|
|
337
|
+
current.ephemeral === next.ephemeral) {
|
|
338
|
+
current = {
|
|
339
|
+
...current,
|
|
340
|
+
content: `${current.content}\n${next.content}`,
|
|
341
|
+
};
|
|
342
|
+
this.onActivityThrottled?.('action', next.content);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
merged.push(current);
|
|
346
|
+
current = next;
|
|
347
|
+
}
|
|
348
|
+
merged.push(current);
|
|
349
|
+
return merged;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Emit a single activity to Linear via API
|
|
353
|
+
*/
|
|
354
|
+
async emitActivity(activity) {
|
|
355
|
+
try {
|
|
356
|
+
const content = this.includeTimestamps
|
|
357
|
+
? `[${new Date().toISOString()}] ${activity.content}`
|
|
358
|
+
: activity.content;
|
|
359
|
+
const response = await fetch(`${this.apiBaseUrl}/api/sessions/${this.sessionId}/activity`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: {
|
|
362
|
+
'Content-Type': 'application/json',
|
|
363
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
364
|
+
},
|
|
365
|
+
body: JSON.stringify({
|
|
366
|
+
workerId: this.workerId,
|
|
367
|
+
activity: {
|
|
368
|
+
type: activity.type,
|
|
369
|
+
content,
|
|
370
|
+
toolName: activity.toolName,
|
|
371
|
+
toolInput: activity.toolInput,
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
});
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
const errorText = await response.text();
|
|
377
|
+
throw new Error(`API error ${response.status}: ${errorText}`);
|
|
378
|
+
}
|
|
379
|
+
const result = (await response.json());
|
|
380
|
+
if (!result.forwarded) {
|
|
381
|
+
console.warn(`Activity not forwarded: ${result.reason ?? 'unknown'}`);
|
|
382
|
+
}
|
|
383
|
+
this.onActivityEmitted?.(activity.type, content);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
387
|
+
console.error(`Failed to emit ${activity.type} activity via API:`, err);
|
|
388
|
+
this.onActivityError?.(activity.type, err);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Summarize tool input for display
|
|
393
|
+
*/
|
|
394
|
+
summarizeToolInput(tool, input) {
|
|
395
|
+
// Tool-specific summaries
|
|
396
|
+
switch (tool) {
|
|
397
|
+
case 'Read':
|
|
398
|
+
return String(input.file_path || input.path || 'file');
|
|
399
|
+
case 'Write':
|
|
400
|
+
return String(input.file_path || input.path || 'file');
|
|
401
|
+
case 'Edit':
|
|
402
|
+
return String(input.file_path || input.path || 'file');
|
|
403
|
+
case 'Grep':
|
|
404
|
+
return `"${input.pattern}" in ${input.path || '.'}`;
|
|
405
|
+
case 'Glob':
|
|
406
|
+
return String(input.pattern || '*');
|
|
407
|
+
case 'Bash':
|
|
408
|
+
const cmd = String(input.command || '');
|
|
409
|
+
return cmd.length > 50 ? cmd.substring(0, 47) + '...' : cmd;
|
|
410
|
+
case 'Task':
|
|
411
|
+
return String(input.description || input.prompt || 'task');
|
|
412
|
+
default:
|
|
413
|
+
// Generic: show first string value or truncated JSON
|
|
414
|
+
const firstStringValue = Object.values(input).find((v) => typeof v === 'string');
|
|
415
|
+
if (firstStringValue) {
|
|
416
|
+
return firstStringValue.length > 50
|
|
417
|
+
? firstStringValue.substring(0, 47) + '...'
|
|
418
|
+
: firstStringValue;
|
|
419
|
+
}
|
|
420
|
+
const json = JSON.stringify(input);
|
|
421
|
+
return json.length > 50 ? json.substring(0, 47) + '...' : json;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Truncate long output strings
|
|
426
|
+
*/
|
|
427
|
+
truncateOutput(output) {
|
|
428
|
+
if (output.length <= this.maxOutputLength)
|
|
429
|
+
return output;
|
|
430
|
+
return (output.substring(0, this.maxOutputLength) +
|
|
431
|
+
`\n\n... (truncated ${output.length - this.maxOutputLength} chars)`);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Format the final result content
|
|
435
|
+
*/
|
|
436
|
+
formatResultContent(event) {
|
|
437
|
+
let content = event.result;
|
|
438
|
+
if (event.cost) {
|
|
439
|
+
content += `\n\n---\n*Tokens: ${event.cost.input_tokens} in / ${event.cost.output_tokens} out*`;
|
|
440
|
+
}
|
|
441
|
+
if (event.duration_ms) {
|
|
442
|
+
const seconds = (event.duration_ms / 1000).toFixed(1);
|
|
443
|
+
content += event.cost ? ` | *Duration: ${seconds}s*` : `\n\n---\n*Duration: ${seconds}s*`;
|
|
444
|
+
}
|
|
445
|
+
return content;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Flush all pending activities immediately
|
|
449
|
+
*/
|
|
450
|
+
async flush() {
|
|
451
|
+
if (this.flushTimer) {
|
|
452
|
+
clearTimeout(this.flushTimer);
|
|
453
|
+
this.flushTimer = null;
|
|
454
|
+
}
|
|
455
|
+
await this.processQueue();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Helper delay function
|
|
459
|
+
*/
|
|
460
|
+
delay(ms) {
|
|
461
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Create an API activity emitter instance
|
|
466
|
+
*/
|
|
467
|
+
export function createApiActivityEmitter(config) {
|
|
468
|
+
return new ApiActivityEmitter(config);
|
|
469
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat Writer
|
|
3
|
+
*
|
|
4
|
+
* Periodically writes heartbeat state to the .agent/ directory.
|
|
5
|
+
* Uses atomic writes (temp file + rename) to prevent corruption.
|
|
6
|
+
*/
|
|
7
|
+
import type { HeartbeatActivityType, HeartbeatWriterConfig } from './state-types';
|
|
8
|
+
/**
|
|
9
|
+
* HeartbeatWriter periodically writes heartbeat state to enable crash detection
|
|
10
|
+
*/
|
|
11
|
+
export declare class HeartbeatWriter {
|
|
12
|
+
private readonly config;
|
|
13
|
+
private readonly heartbeatPath;
|
|
14
|
+
private intervalHandle;
|
|
15
|
+
private lastActivityType;
|
|
16
|
+
private lastActivityTimestamp;
|
|
17
|
+
private toolCallsCount;
|
|
18
|
+
private currentOperation;
|
|
19
|
+
private stopped;
|
|
20
|
+
constructor(config: HeartbeatWriterConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Start the heartbeat writer
|
|
23
|
+
* Immediately writes the first heartbeat, then starts the interval
|
|
24
|
+
*/
|
|
25
|
+
start(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Stop the heartbeat writer
|
|
28
|
+
* Should be called when the agent exits
|
|
29
|
+
*/
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Update the last activity type
|
|
33
|
+
* Call this when the agent does something
|
|
34
|
+
*/
|
|
35
|
+
updateActivity(type: HeartbeatActivityType, operation?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Record a tool call
|
|
38
|
+
*/
|
|
39
|
+
recordToolCall(toolName: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Record thinking activity
|
|
42
|
+
*/
|
|
43
|
+
recordThinking(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Write the heartbeat file atomically
|
|
46
|
+
*/
|
|
47
|
+
private writeHeartbeat;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a heartbeat writer for an agent
|
|
51
|
+
*/
|
|
52
|
+
export declare function createHeartbeatWriter(config: HeartbeatWriterConfig): HeartbeatWriter;
|
|
53
|
+
/**
|
|
54
|
+
* Parse environment variable for heartbeat interval
|
|
55
|
+
*/
|
|
56
|
+
export declare function getHeartbeatIntervalFromEnv(): number;
|
|
57
|
+
//# sourceMappingURL=heartbeat-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat-writer.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/heartbeat-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAEV,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,eAAe,CAAA;AAKtB;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,qBAAqB,CAAQ;IACrC,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,OAAO,CAAQ;gBAEX,MAAM,EAAE,qBAAqB;IASzC;;;OAGG;IACH,KAAK,IAAI,IAAI;IA2Bb;;;OAGG;IACH,IAAI,IAAI,IAAI;IAQZ;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,qBAAqB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IASrE;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAItC;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;OAEG;IACH,OAAO,CAAC,cAAc;CAyBvB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAEpF;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CASpD"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat Writer
|
|
3
|
+
*
|
|
4
|
+
* Periodically writes heartbeat state to the .agent/ directory.
|
|
5
|
+
* Uses atomic writes (temp file + rename) to prevent corruption.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync, renameSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
// Default heartbeat interval: 10 seconds
|
|
10
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 10000;
|
|
11
|
+
/**
|
|
12
|
+
* HeartbeatWriter periodically writes heartbeat state to enable crash detection
|
|
13
|
+
*/
|
|
14
|
+
export class HeartbeatWriter {
|
|
15
|
+
config;
|
|
16
|
+
heartbeatPath;
|
|
17
|
+
intervalHandle = null;
|
|
18
|
+
lastActivityType = 'idle';
|
|
19
|
+
lastActivityTimestamp;
|
|
20
|
+
toolCallsCount = 0;
|
|
21
|
+
currentOperation = null;
|
|
22
|
+
stopped = false;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = {
|
|
25
|
+
...config,
|
|
26
|
+
intervalMs: config.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
27
|
+
};
|
|
28
|
+
this.heartbeatPath = resolve(this.config.agentDir, 'heartbeat.json');
|
|
29
|
+
this.lastActivityTimestamp = Date.now();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start the heartbeat writer
|
|
33
|
+
* Immediately writes the first heartbeat, then starts the interval
|
|
34
|
+
*/
|
|
35
|
+
start() {
|
|
36
|
+
if (this.stopped) {
|
|
37
|
+
throw new Error('HeartbeatWriter has been stopped and cannot be restarted');
|
|
38
|
+
}
|
|
39
|
+
if (this.intervalHandle) {
|
|
40
|
+
return; // Already running
|
|
41
|
+
}
|
|
42
|
+
// Ensure the directory exists
|
|
43
|
+
const dir = dirname(this.heartbeatPath);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
// Write initial heartbeat
|
|
48
|
+
this.writeHeartbeat();
|
|
49
|
+
// Start interval
|
|
50
|
+
this.intervalHandle = setInterval(() => {
|
|
51
|
+
this.writeHeartbeat();
|
|
52
|
+
}, this.config.intervalMs);
|
|
53
|
+
// Don't prevent the process from exiting
|
|
54
|
+
this.intervalHandle.unref();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Stop the heartbeat writer
|
|
58
|
+
* Should be called when the agent exits
|
|
59
|
+
*/
|
|
60
|
+
stop() {
|
|
61
|
+
this.stopped = true;
|
|
62
|
+
if (this.intervalHandle) {
|
|
63
|
+
clearInterval(this.intervalHandle);
|
|
64
|
+
this.intervalHandle = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Update the last activity type
|
|
69
|
+
* Call this when the agent does something
|
|
70
|
+
*/
|
|
71
|
+
updateActivity(type, operation) {
|
|
72
|
+
this.lastActivityType = type;
|
|
73
|
+
this.lastActivityTimestamp = Date.now();
|
|
74
|
+
this.currentOperation = operation ?? null;
|
|
75
|
+
if (type === 'tool_use') {
|
|
76
|
+
this.toolCallsCount++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Record a tool call
|
|
81
|
+
*/
|
|
82
|
+
recordToolCall(toolName) {
|
|
83
|
+
this.updateActivity('tool_use', toolName);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Record thinking activity
|
|
87
|
+
*/
|
|
88
|
+
recordThinking() {
|
|
89
|
+
this.updateActivity('thinking');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write the heartbeat file atomically
|
|
93
|
+
*/
|
|
94
|
+
writeHeartbeat() {
|
|
95
|
+
const memoryUsage = process.memoryUsage();
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const state = {
|
|
98
|
+
timestamp: now,
|
|
99
|
+
pid: this.config.pid,
|
|
100
|
+
memoryUsageMB: Math.round(memoryUsage.heapUsed / 1024 / 1024),
|
|
101
|
+
uptime: Math.floor((now - this.config.startTime) / 1000),
|
|
102
|
+
lastActivityType: this.lastActivityType,
|
|
103
|
+
lastActivityTimestamp: this.lastActivityTimestamp,
|
|
104
|
+
toolCallsCount: this.toolCallsCount,
|
|
105
|
+
currentOperation: this.currentOperation,
|
|
106
|
+
};
|
|
107
|
+
// Write atomically: temp file then rename
|
|
108
|
+
const tempPath = `${this.heartbeatPath}.tmp`;
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(tempPath, JSON.stringify(state, null, 2));
|
|
111
|
+
renameSync(tempPath, this.heartbeatPath);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Silently ignore write errors - heartbeat is best-effort
|
|
115
|
+
// The file might be locked or the directory might have been removed
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a heartbeat writer for an agent
|
|
121
|
+
*/
|
|
122
|
+
export function createHeartbeatWriter(config) {
|
|
123
|
+
return new HeartbeatWriter(config);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse environment variable for heartbeat interval
|
|
127
|
+
*/
|
|
128
|
+
export function getHeartbeatIntervalFromEnv() {
|
|
129
|
+
const envValue = process.env.AGENT_HEARTBEAT_INTERVAL_MS;
|
|
130
|
+
if (envValue) {
|
|
131
|
+
const parsed = parseInt(envValue, 10);
|
|
132
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
137
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { OrchestratorConfig, OrchestratorIssue, AgentProcess, OrchestratorEvents, SpawnAgentOptions, OrchestratorResult, OrchestratorStreamConfig, StopAgentResult, ForwardPromptResult, InjectMessageResult, SpawnAgentWithResumeOptions, WorkTypeTimeoutConfig, AgentWorkResult, } from './types';
|
|
2
|
+
export type { ClaudeStreamEvent, ClaudeInitEvent, ClaudeSystemEvent, ClaudeAssistantEvent, ClaudeToolUseEvent, ClaudeToolResultEvent, ClaudeResultEvent, ClaudeErrorEvent, ClaudeTodoItem, ClaudeUserEvent, ClaudeEvent, ClaudeStreamHandlers, } from './stream-parser';
|
|
3
|
+
export type { ActivityEmitterConfig } from './activity-emitter';
|
|
4
|
+
export type { ApiActivityEmitterConfig, ProgressMilestone } from './api-activity-emitter';
|
|
5
|
+
export type { WorktreeState, WorktreeStatus, HeartbeatState, HeartbeatActivityType, TodosState, TodoItem, TodoStatus, ProgressLogEntry, ProgressEventType, RecoveryCheckResult, HeartbeatWriterConfig, ProgressLoggerConfig, } from './state-types';
|
|
6
|
+
export type { LogAnalysisConfig } from './log-config';
|
|
7
|
+
export type { SessionEventType, SessionEvent, SessionMetadata, SessionLoggerConfig, } from './session-logger';
|
|
8
|
+
export type { PatternType, PatternSeverity, AnalyzedPattern, AnalysisResult, SuggestedIssue, TrackedIssue, DeduplicationStore, } from './log-analyzer';
|
|
9
|
+
export { AgentOrchestrator, createOrchestrator, getWorktreeIdentifier } from './orchestrator';
|
|
10
|
+
export { ClaudeStreamParser, createStreamParser } from './stream-parser';
|
|
11
|
+
export { ActivityEmitter, createActivityEmitter } from './activity-emitter';
|
|
12
|
+
export { ApiActivityEmitter, createApiActivityEmitter } from './api-activity-emitter';
|
|
13
|
+
export { HeartbeatWriter, createHeartbeatWriter, getHeartbeatIntervalFromEnv, } from './heartbeat-writer';
|
|
14
|
+
export { ProgressLogger, createProgressLogger } from './progress-logger';
|
|
15
|
+
export { getAgentDir, getStatePath, getHeartbeatPath, getTodosPath, isHeartbeatFresh, readWorktreeState, readHeartbeat, readTodos, checkRecovery, initializeAgentDir, writeState, updateState, writeTodos, createInitialState, buildRecoveryPrompt, getHeartbeatTimeoutFromEnv, getMaxRecoveryAttemptsFromEnv, getTaskListId, } from './state-recovery';
|
|
16
|
+
export { getLogAnalysisConfig, isSessionLoggingEnabled, isAutoAnalyzeEnabled, } from './log-config';
|
|
17
|
+
export { SessionLogger, createSessionLogger, readSessionMetadata, readSessionEvents, } from './session-logger';
|
|
18
|
+
export { parseWorkResult } from './parse-work-result';
|
|
19
|
+
export { LogAnalyzer, createLogAnalyzer } from './log-analyzer';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,EACrB,eAAe,GAChB,MAAM,SAAS,CAAA;AAGhB,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,WAAW,EACX,oBAAoB,GACrB,MAAM,iBAAiB,CAAA;AAGxB,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG/D,YAAY,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAGzF,YAAY,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,eAAe,CAAA;AAGtB,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAGrD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AAGzB,YAAY,EACV,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAG7F,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGxE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG3E,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAGrF,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAGxE,OAAO,EACL,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,SAAS,EACT,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,aAAa,GACd,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAGrD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
|