agent-dag 1.13.0 → 1.15.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/web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agent-dag</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DZeh3MHD.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bwj0xSAo.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/server/index.mjs
CHANGED
|
@@ -73,32 +73,52 @@ async function maybeRotatePersistFile() {
|
|
|
73
73
|
// payloads for that session before broadcasting, (b) emit a synthetic
|
|
74
74
|
// `ModelObserved` event so the client backfills agents created before
|
|
75
75
|
// the model was resolved.
|
|
76
|
-
const modelBySession = new Map(); // sessionId ->
|
|
76
|
+
const modelBySession = new Map(); // sessionId -> { rootModel, subsSig }
|
|
77
77
|
const pendingTranscriptReads = new Set(); // sessionId currently being read
|
|
78
|
+
const modelLastReadAt = new Map(); // sessionId -> ms timestamp (re-read throttle)
|
|
79
|
+
const MODEL_READ_THROTTLE_MS = 2500;
|
|
78
80
|
|
|
79
81
|
async function readModelFromTranscript(path) {
|
|
82
|
+
// Returns { rootModel, subagentModels } — root is the most recent
|
|
83
|
+
// non-sidechain assistant model, subagents are attributed by the Task
|
|
84
|
+
// tool_use id that owns them. CC transcripts mark subagent messages
|
|
85
|
+
// with `isSidechain:true` and (for current schemas) a `parentToolUseID`
|
|
86
|
+
// or `parent_tool_use_id` referencing the Task invocation.
|
|
80
87
|
try {
|
|
81
88
|
const s = await stat(path);
|
|
82
89
|
if (s.size === 0) return null;
|
|
83
|
-
// Read up to last 128 KB — plenty for the most-recent model
|
|
84
|
-
// declaration. Reading from the tail handles sessions that switched
|
|
85
|
-
// model mid-conversation (we want the current one).
|
|
86
|
-
const TAIL = 128 * 1024;
|
|
87
|
-
const start = Math.max(0, s.size - TAIL);
|
|
88
90
|
const fh = await open(path, "r");
|
|
91
|
+
let text;
|
|
89
92
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const text = buf.toString("utf8");
|
|
94
|
-
// Scan all matches and return the LAST one — most recent model used.
|
|
95
|
-
const re = /"model"\s*:\s*"(claude[-_][^"]+)"/gi;
|
|
96
|
-
let last = null;
|
|
97
|
-
for (const m of text.matchAll(re)) last = m[1];
|
|
98
|
-
return last;
|
|
93
|
+
const buf = Buffer.alloc(s.size);
|
|
94
|
+
await fh.read(buf, 0, s.size, 0);
|
|
95
|
+
text = buf.toString("utf8");
|
|
99
96
|
} finally {
|
|
100
97
|
await fh.close();
|
|
101
98
|
}
|
|
99
|
+
let rootModel = null;
|
|
100
|
+
const subagentModels = {};
|
|
101
|
+
for (const line of text.split("\n")) {
|
|
102
|
+
if (!line) continue;
|
|
103
|
+
let obj;
|
|
104
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
105
|
+
const msg = obj && obj.message;
|
|
106
|
+
// Assistant model entries live on either obj.model or obj.message.model
|
|
107
|
+
// depending on CC schema version. Accept either.
|
|
108
|
+
const model = (msg && typeof msg.model === "string" && /^claude[-_]/i.test(msg.model)) ? msg.model
|
|
109
|
+
: (typeof obj.model === "string" && /^claude[-_]/i.test(obj.model)) ? obj.model
|
|
110
|
+
: null;
|
|
111
|
+
if (!model) continue;
|
|
112
|
+
const isSide = obj.isSidechain === true || obj.is_sidechain === true;
|
|
113
|
+
const ptid = obj.parentToolUseID || obj.parent_tool_use_id || obj.parentToolUseId || null;
|
|
114
|
+
if (isSide && ptid) {
|
|
115
|
+
subagentModels[ptid] = model;
|
|
116
|
+
} else if (!isSide) {
|
|
117
|
+
rootModel = model;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!rootModel && Object.keys(subagentModels).length === 0) return null;
|
|
121
|
+
return { rootModel, subagentModels };
|
|
102
122
|
} catch {
|
|
103
123
|
return null;
|
|
104
124
|
}
|
|
@@ -109,16 +129,29 @@ function maybeResolveModel(payload) {
|
|
|
109
129
|
const sid = payload.session_id;
|
|
110
130
|
const tp = payload.transcript_path;
|
|
111
131
|
if (!sid || !tp) return;
|
|
112
|
-
|
|
132
|
+
// Re-read on every event for this session — the cache was preventing us
|
|
133
|
+
// from picking up subagent models that arrive after the root is known.
|
|
134
|
+
// Throttle so we don't thrash the filesystem.
|
|
113
135
|
if (pendingTranscriptReads.has(sid)) return;
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const last = modelLastReadAt.get(sid) ?? 0;
|
|
138
|
+
if (now - last < MODEL_READ_THROTTLE_MS) return;
|
|
139
|
+
modelLastReadAt.set(sid, now);
|
|
114
140
|
pendingTranscriptReads.add(sid);
|
|
115
141
|
readModelFromTranscript(tp)
|
|
116
|
-
.then(
|
|
117
|
-
if (!
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
.then(result => {
|
|
143
|
+
if (!result) return;
|
|
144
|
+
const { rootModel, subagentModels } = result;
|
|
145
|
+
const prev = modelBySession.get(sid);
|
|
146
|
+
const subsSig = JSON.stringify(subagentModels);
|
|
147
|
+
if (prev && prev.rootModel === rootModel && prev.subsSig === subsSig) return;
|
|
148
|
+
modelBySession.set(sid, { rootModel, subsSig });
|
|
149
|
+
pushEvent({
|
|
150
|
+
hook_event_name: "ModelObserved",
|
|
151
|
+
session_id: sid,
|
|
152
|
+
model: rootModel,
|
|
153
|
+
subagentModels,
|
|
154
|
+
}, "internal");
|
|
122
155
|
})
|
|
123
156
|
.catch(() => {})
|
|
124
157
|
.finally(() => pendingTranscriptReads.delete(sid));
|
|
@@ -198,6 +231,145 @@ function maybeResolveUsage(payload) {
|
|
|
198
231
|
.finally(() => pendingUsageReads.delete(sid));
|
|
199
232
|
}
|
|
200
233
|
|
|
234
|
+
// ─── Context enrichment ──────────────────────────────────────────────────
|
|
235
|
+
// Approximation of `/context` since CC doesn't expose its breakdown via
|
|
236
|
+
// hooks. We scan the transcript JSONL for message counts (user / assistant
|
|
237
|
+
// / tool_use / tool_result / system-reminders) and walk up from cwd for
|
|
238
|
+
// any CLAUDE.md files in scope. Token totals come from UsageObserved; this
|
|
239
|
+
// scan is purely structural ("what does the context contain").
|
|
240
|
+
const lastContextReadAt = new Map();
|
|
241
|
+
const pendingContextReads = new Set();
|
|
242
|
+
const CONTEXT_READ_THROTTLE_MS = 4000;
|
|
243
|
+
|
|
244
|
+
async function readContextFromTranscript(path) {
|
|
245
|
+
try {
|
|
246
|
+
const s = await stat(path);
|
|
247
|
+
if (s.size === 0) return null;
|
|
248
|
+
const fh = await open(path, "r");
|
|
249
|
+
let buf;
|
|
250
|
+
try { buf = Buffer.alloc(s.size); await fh.read(buf, 0, s.size, 0); }
|
|
251
|
+
finally { await fh.close(); }
|
|
252
|
+
const text = buf.toString("utf8");
|
|
253
|
+
const breakdown = {
|
|
254
|
+
msgsUser: 0,
|
|
255
|
+
msgsAssistant: 0,
|
|
256
|
+
toolUses: 0,
|
|
257
|
+
toolResults: 0,
|
|
258
|
+
systemReminders: 0,
|
|
259
|
+
currentContextTokens: 0,
|
|
260
|
+
};
|
|
261
|
+
breakdown.msgsUser = (text.match(/"type"\s*:\s*"user"/g) ?? []).length;
|
|
262
|
+
breakdown.msgsAssistant = (text.match(/"type"\s*:\s*"assistant"/g) ?? []).length;
|
|
263
|
+
breakdown.toolUses = (text.match(/"type"\s*:\s*"tool_use"/g) ?? []).length;
|
|
264
|
+
breakdown.toolResults = (text.match(/"type"\s*:\s*"tool_result"/g) ?? []).length;
|
|
265
|
+
breakdown.systemReminders = (text.match(/<system-reminder>/g) ?? []).length;
|
|
266
|
+
// Current context size = input + cache_read + cache_create on the LAST
|
|
267
|
+
// usage block. Each assistant message's usage describes the context
|
|
268
|
+
// window for THAT call; summing across calls double-counts cached
|
|
269
|
+
// prefixes and explodes past the real ceiling. Take the most recent.
|
|
270
|
+
const re = /"usage"\s*:\s*\{([^}]+)\}/g;
|
|
271
|
+
const grab = (blob, key) => {
|
|
272
|
+
const km = blob.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`));
|
|
273
|
+
return km ? Number(km[1]) : 0;
|
|
274
|
+
};
|
|
275
|
+
let lastBlob = null;
|
|
276
|
+
for (const m of text.matchAll(re)) lastBlob = m[1];
|
|
277
|
+
if (lastBlob) {
|
|
278
|
+
breakdown.currentContextTokens =
|
|
279
|
+
grab(lastBlob, "input_tokens") +
|
|
280
|
+
grab(lastBlob, "cache_read_input_tokens") +
|
|
281
|
+
grab(lastBlob, "cache_creation_input_tokens");
|
|
282
|
+
}
|
|
283
|
+
return breakdown;
|
|
284
|
+
} catch { return null; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Encode an absolute path the way CC stores it under
|
|
288
|
+
* ~/.claude/projects/<slug>/. Drive letters, colons, and path separators
|
|
289
|
+
* are flattened to "-" so the slug survives as a single directory name. */
|
|
290
|
+
function ccProjectSlug(cwd) {
|
|
291
|
+
if (!cwd) return "";
|
|
292
|
+
// Replace path separators and the Windows drive colon. Match CC's own
|
|
293
|
+
// encoding: every \\ / : → - (no collapsing of adjacent dashes).
|
|
294
|
+
return resolve(cwd).replace(/[\\/:]/g, "-");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function scanClaudeMdFiles(cwd) {
|
|
298
|
+
if (!cwd || typeof cwd !== "string") return [];
|
|
299
|
+
const found = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const home = homedir();
|
|
302
|
+
const push = async (p) => {
|
|
303
|
+
if (seen.has(p)) return;
|
|
304
|
+
seen.add(p);
|
|
305
|
+
try {
|
|
306
|
+
const s = await stat(p);
|
|
307
|
+
if (s.isFile() && s.size > 0) found.push({ path: p, bytes: s.size });
|
|
308
|
+
} catch {}
|
|
309
|
+
};
|
|
310
|
+
// Walk up from cwd to filesystem root. At each dir, check for the
|
|
311
|
+
// canonical CC memory filenames plus CLAUDE.local.md (user-private).
|
|
312
|
+
let dir = resolve(cwd);
|
|
313
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
314
|
+
for (const rel of [
|
|
315
|
+
"CLAUDE.md",
|
|
316
|
+
"CLAUDE.local.md",
|
|
317
|
+
join(".claude", "CLAUDE.md"),
|
|
318
|
+
join(".claude", "CLAUDE.local.md"),
|
|
319
|
+
]) {
|
|
320
|
+
await push(join(dir, rel));
|
|
321
|
+
}
|
|
322
|
+
const parent = pdirname(dir);
|
|
323
|
+
if (parent === dir) break;
|
|
324
|
+
dir = parent;
|
|
325
|
+
}
|
|
326
|
+
// User-global memory.
|
|
327
|
+
await push(join(home, ".claude", "CLAUDE.md"));
|
|
328
|
+
await push(join(home, ".claude", "CLAUDE.local.md"));
|
|
329
|
+
// Per-project auto-memory: ~/.claude/projects/<slug>/memory/*.md
|
|
330
|
+
// (plus MEMORY.md index). CC injects these into context for sessions
|
|
331
|
+
// whose cwd matches the slug.
|
|
332
|
+
const slug = ccProjectSlug(cwd);
|
|
333
|
+
if (slug) {
|
|
334
|
+
const memDir = join(home, ".claude", "projects", slug, "memory");
|
|
335
|
+
try {
|
|
336
|
+
const entries = await readdir(memDir);
|
|
337
|
+
for (const f of entries) {
|
|
338
|
+
if (f.toLowerCase().endsWith(".md")) await push(join(memDir, f));
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
}
|
|
342
|
+
return found;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function maybeResolveContext(payload) {
|
|
346
|
+
if (!payload || typeof payload !== "object") return;
|
|
347
|
+
const sid = payload.session_id;
|
|
348
|
+
const tp = payload.transcript_path;
|
|
349
|
+
const cwd = payload.cwd;
|
|
350
|
+
if (!sid || !tp) return;
|
|
351
|
+
if (pendingContextReads.has(sid)) return;
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
const last = lastContextReadAt.get(sid) ?? 0;
|
|
354
|
+
if (now - last < CONTEXT_READ_THROTTLE_MS) return;
|
|
355
|
+
lastContextReadAt.set(sid, now);
|
|
356
|
+
pendingContextReads.add(sid);
|
|
357
|
+
Promise.all([readContextFromTranscript(tp), scanClaudeMdFiles(cwd)])
|
|
358
|
+
.then(([breakdown, claudeMdFiles]) => {
|
|
359
|
+
if (!breakdown && (!claudeMdFiles || claudeMdFiles.length === 0)) return;
|
|
360
|
+
pushEvent({
|
|
361
|
+
hook_event_name: "ContextObserved",
|
|
362
|
+
session_id: sid,
|
|
363
|
+
context: {
|
|
364
|
+
...(breakdown ?? {}),
|
|
365
|
+
claudeMdFiles: claudeMdFiles ?? [],
|
|
366
|
+
},
|
|
367
|
+
}, "internal");
|
|
368
|
+
})
|
|
369
|
+
.catch(() => {})
|
|
370
|
+
.finally(() => pendingContextReads.delete(sid));
|
|
371
|
+
}
|
|
372
|
+
|
|
201
373
|
function pushEvent(raw, source, opts = {}) {
|
|
202
374
|
// Synchronous enrichment: if we already know this session's model, stamp
|
|
203
375
|
// it on the payload so the client's recursive scanner picks it up.
|
|
@@ -235,6 +407,7 @@ function pushEvent(raw, source, opts = {}) {
|
|
|
235
407
|
if (source === "hook" && !opts.replay) {
|
|
236
408
|
maybeResolveModel(raw);
|
|
237
409
|
maybeResolveUsage(raw);
|
|
410
|
+
maybeResolveContext(raw);
|
|
238
411
|
}
|
|
239
412
|
|
|
240
413
|
return evt;
|