-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubagents.js
More file actions
111 lines (96 loc) · 3.06 KB
/
subagents.js
File metadata and controls
111 lines (96 loc) · 3.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
const fs = require('fs');
const path = require('path');
const TAIL_BYTES = 64 * 1024;
const ERROR_TIMEOUT_MS = 30000;
function readMeta(metaPath) {
try {
const raw = fs.readFileSync(metaPath, 'utf-8');
return JSON.parse(raw);
} catch {
return null;
}
}
function readLastEvent(jsonlPath) {
let fd;
try {
const stat = fs.statSync(jsonlPath);
if (stat.size === 0) return null;
fd = fs.openSync(jsonlPath, 'r');
let start = Math.max(0, stat.size - TAIL_BYTES);
while (true) {
const len = stat.size - start;
const buf = Buffer.alloc(len);
fs.readSync(fd, buf, 0, len, start);
const text = buf.toString('utf-8');
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
if (start === 0 || lines.length > 1) {
const last = lines[lines.length - 1];
if (!last) return null;
try { return JSON.parse(last); } catch { return null; }
}
// Single (possibly truncated) line — grow window
start = Math.max(0, start - TAIL_BYTES);
}
} catch {
return null;
} finally {
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
}
}
function deriveState(lastEvent, mtimeMs, nowMs = Date.now()) {
if (!lastEvent) return 'error';
const msg = lastEvent.message || {};
const stopReason = msg.stop_reason;
const ageMs = nowMs - mtimeMs;
if (stopReason === 'end_turn') return 'completed';
if (stopReason == null && ageMs > ERROR_TIMEOUT_MS) return 'error';
return 'running';
}
function scanSession(sessionDir, dispatches) {
const out = [];
const subDir = path.join(sessionDir, 'subagents');
let entries;
try { entries = fs.readdirSync(subDir); }
catch { return out; }
for (const entry of entries) {
if (!entry.startsWith('agent-') || !entry.endsWith('.jsonl')) continue;
const agentId = entry.slice('agent-'.length, -'.jsonl'.length);
const jsonlPath = path.join(subDir, entry);
const metaPath = path.join(subDir, `agent-${agentId}.meta.json`);
const meta = readMeta(metaPath);
if (!meta) continue;
let stat;
try { stat = fs.statSync(jsonlPath); } catch { continue; }
const lastEvent = readLastEvent(jsonlPath);
const state = deriveState(lastEvent, stat.mtimeMs);
const dispatch = dispatches.get(meta.toolUseId);
out.push({
agentId,
description: meta.description,
agentType: meta.agentType,
toolUseId: meta.toolUseId,
runInBackground: dispatch ? dispatch.runInBackground : undefined,
dispatchTs: dispatch ? dispatch.dispatchTs : null,
lastEventTs: stat.mtimeMs,
state,
});
}
return out;
}
// Stateless today; kept as a class so we can cache scanSession results
// (keyed by sessionDir, short TTL) if disk-scan cost becomes visible.
class SubagentTracker {
snapshotForSession(sessionDir, dispatches) {
return scanSession(sessionDir, dispatches)
.filter(sa => sa.runInBackground === true && sa.state === 'running');
}
}
module.exports = {
readMeta,
readLastEvent,
deriveState,
scanSession,
SubagentTracker,
ERROR_TIMEOUT_MS,
TAIL_BYTES,
};