agent-dag 1.9.0 → 1.9.1
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,7 +5,7 @@
|
|
|
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-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-g_sZY5sg.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-q0qkmkVM.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
package/src/server/index.mjs
CHANGED
|
@@ -92,6 +92,80 @@ function maybeResolveModel(payload) {
|
|
|
92
92
|
.finally(() => pendingTranscriptReads.delete(sid));
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// ─── Usage enrichment ────────────────────────────────────────────────────
|
|
96
|
+
// Same story as the model: token counts (input/output/cache) are missing
|
|
97
|
+
// from every CC hook payload but present on every assistant message in
|
|
98
|
+
// the transcript JSONL as a `"usage":{…}` block. We sum them across the
|
|
99
|
+
// whole transcript and ship a synthetic UsageObserved event so the
|
|
100
|
+
// session's root agent gets accurate cumulative usage (and therefore the
|
|
101
|
+
// cost columns actually have something to multiply by).
|
|
102
|
+
const lastUsageReadAt = new Map(); // sid -> ms timestamp
|
|
103
|
+
const pendingUsageReads = new Set(); // sid currently being read
|
|
104
|
+
const USAGE_READ_THROTTLE_MS = 2500;
|
|
105
|
+
|
|
106
|
+
async function readUsageFromTranscript(path) {
|
|
107
|
+
try {
|
|
108
|
+
const s = await stat(path);
|
|
109
|
+
if (s.size === 0) return null;
|
|
110
|
+
// Transcripts can grow large (thinking blocks, tool inputs) — read the
|
|
111
|
+
// whole file. Each entry has its own usage object and we sum every
|
|
112
|
+
// occurrence, so missing earlier bytes would undercount. Files are
|
|
113
|
+
// usually < 1MB; tens-of-MB sessions cost a few ms to scan.
|
|
114
|
+
const fh = await open(path, "r");
|
|
115
|
+
let buf;
|
|
116
|
+
try {
|
|
117
|
+
buf = Buffer.alloc(s.size);
|
|
118
|
+
await fh.read(buf, 0, s.size, 0);
|
|
119
|
+
} finally {
|
|
120
|
+
await fh.close();
|
|
121
|
+
}
|
|
122
|
+
const text = buf.toString("utf8");
|
|
123
|
+
const totals = {
|
|
124
|
+
input_tokens: 0, output_tokens: 0,
|
|
125
|
+
cache_read_input_tokens: 0, cache_creation_input_tokens: 0,
|
|
126
|
+
};
|
|
127
|
+
// Match each `"usage":{...}` block and sum the four numeric fields.
|
|
128
|
+
// Regex is good enough — these blocks are flat single-level JSON.
|
|
129
|
+
const re = /"usage"\s*:\s*\{([^}]+)\}/g;
|
|
130
|
+
const grab = (blob, key) => {
|
|
131
|
+
const km = blob.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`));
|
|
132
|
+
return km ? Number(km[1]) : 0;
|
|
133
|
+
};
|
|
134
|
+
for (const m of text.matchAll(re)) {
|
|
135
|
+
const blob = m[1];
|
|
136
|
+
totals.input_tokens += grab(blob, "input_tokens");
|
|
137
|
+
totals.output_tokens += grab(blob, "output_tokens");
|
|
138
|
+
totals.cache_read_input_tokens += grab(blob, "cache_read_input_tokens");
|
|
139
|
+
totals.cache_creation_input_tokens += grab(blob, "cache_creation_input_tokens");
|
|
140
|
+
}
|
|
141
|
+
if (totals.input_tokens === 0 && totals.output_tokens === 0
|
|
142
|
+
&& totals.cache_read_input_tokens === 0 && totals.cache_creation_input_tokens === 0) return null;
|
|
143
|
+
return totals;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function maybeResolveUsage(payload) {
|
|
150
|
+
if (!payload || typeof payload !== "object") return;
|
|
151
|
+
const sid = payload.session_id;
|
|
152
|
+
const tp = payload.transcript_path;
|
|
153
|
+
if (!sid || !tp) return;
|
|
154
|
+
if (pendingUsageReads.has(sid)) return;
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const last = lastUsageReadAt.get(sid) ?? 0;
|
|
157
|
+
if (now - last < USAGE_READ_THROTTLE_MS) return;
|
|
158
|
+
lastUsageReadAt.set(sid, now);
|
|
159
|
+
pendingUsageReads.add(sid);
|
|
160
|
+
readUsageFromTranscript(tp)
|
|
161
|
+
.then(usage => {
|
|
162
|
+
if (!usage) return;
|
|
163
|
+
pushEvent({ hook_event_name: "UsageObserved", session_id: sid, usage }, "internal");
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {})
|
|
166
|
+
.finally(() => pendingUsageReads.delete(sid));
|
|
167
|
+
}
|
|
168
|
+
|
|
95
169
|
function pushEvent(raw, source, opts = {}) {
|
|
96
170
|
// Synchronous enrichment: if we already know this session's model, stamp
|
|
97
171
|
// it on the payload so the client's recursive scanner picks it up.
|
|
@@ -120,10 +194,14 @@ function pushEvent(raw, source, opts = {}) {
|
|
|
120
194
|
appendFile(persistPath, JSON.stringify(evt) + "\n", "utf8").catch(() => {});
|
|
121
195
|
}
|
|
122
196
|
|
|
123
|
-
// Kick off async transcript
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
197
|
+
// Kick off async transcript scans. Model arrives as a one-shot
|
|
198
|
+
// ModelObserved; usage is re-read periodically (throttled to 2.5s per
|
|
199
|
+
// session) so the cost columns track running totals as the session
|
|
200
|
+
// progresses. Both result in synthetic events.
|
|
201
|
+
if (source === "hook" && !opts.replay) {
|
|
202
|
+
maybeResolveModel(raw);
|
|
203
|
+
maybeResolveUsage(raw);
|
|
204
|
+
}
|
|
127
205
|
|
|
128
206
|
return evt;
|
|
129
207
|
}
|