@swestash/agent-lens 0.5.4 → 0.6.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/agent-lens.js +135 -11
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{CopyButton-CQ5SHT0Q.js → CopyButton-XWWtxa5T.js} +1 -1
- package/web/assets/{Dashboard-BZeT30Fc.js → Dashboard-BQX66iZp.js} +34 -34
- package/web/assets/{SecurityView-CCSq2PMa.js → SecurityView-C_CfNLoD.js} +1 -1
- package/web/assets/{SessionView-DTYrghRK.js → SessionView-qPYTooYU.js} +8 -8
- package/web/assets/{SkillView-BPXOcH3K.js → SkillView-BuWdX4As.js} +1 -1
- package/web/assets/{SkillsView-ClZpsQqe.js → SkillsView-D8Wtsxi6.js} +1 -1
- package/web/assets/{WorkflowView-DwxhHR6A.js → WorkflowView-Afsv_IWs.js} +1 -1
- package/web/assets/index-BkDuOrzs.js +68 -0
- package/web/assets/index-CPyBa1Te.css +1 -0
- package/web/assets/{index-CM-5HLoq.js → index-T6SPgI6b.js} +1 -1
- package/web/assets/{severity-TiQaFcLQ.js → severity-DkldRG9Z.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-B_beFRjy.js +0 -68
- package/web/assets/index-CGKywTVG.css +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -591,7 +591,7 @@ var CAC = class extends EventEmitter {
|
|
|
591
591
|
var cac = (name = "") => new CAC(name);
|
|
592
592
|
|
|
593
593
|
// ../core/dist/schema.js
|
|
594
|
-
var SCHEMA_VERSION =
|
|
594
|
+
var SCHEMA_VERSION = 13;
|
|
595
595
|
var SCHEMA_SQL = (
|
|
596
596
|
/* sql */
|
|
597
597
|
`
|
|
@@ -712,7 +712,8 @@ CREATE TABLE IF NOT EXISTS tool_calls (
|
|
|
712
712
|
workflow_run_id TEXT, -- for Workflow: the run id (toolUseResult.runId, e.g. wf_<id>) \u2014 ties the run to its subagents
|
|
713
713
|
workflow_name TEXT, -- for Workflow: the workflow name (toolUseResult.workflowName)
|
|
714
714
|
resolved_model TEXT,
|
|
715
|
-
status TEXT, -- success | error | ...
|
|
715
|
+
status TEXT, -- success | error | ... ('error' when the tool_result had is_error)
|
|
716
|
+
error_type TEXT, -- heuristic bucket for status='error' rows (see errors.ts); NULL otherwise
|
|
716
717
|
total_duration_ms INTEGER,
|
|
717
718
|
total_tokens INTEGER,
|
|
718
719
|
total_tool_use_count INTEGER,
|
|
@@ -725,6 +726,7 @@ CREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name);
|
|
|
725
726
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_skill_id ON tool_calls(skill_id);
|
|
726
727
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);
|
|
727
728
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);
|
|
729
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_error_type ON tool_calls(error_type);
|
|
728
730
|
|
|
729
731
|
-- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the
|
|
730
732
|
-- transcript as an isMeta user event (begins "Base directory for this skill: \u2026", then the body,
|
|
@@ -945,6 +947,35 @@ var SECURITY_CATEGORIES = [
|
|
|
945
947
|
}
|
|
946
948
|
];
|
|
947
949
|
|
|
950
|
+
// ../core/dist/errors.js
|
|
951
|
+
var ERROR_CLASSIFIER_VERSION = 1;
|
|
952
|
+
var REJECTION_TYPES = /* @__PURE__ */ new Set(["user-rejected", "guardrail-blocked"]);
|
|
953
|
+
function errorKind(type) {
|
|
954
|
+
return REJECTION_TYPES.has(type) ? "rejection" : "failure";
|
|
955
|
+
}
|
|
956
|
+
var PATTERNS = [
|
|
957
|
+
// — Rejections / blocks: is_error, but the agent's tool didn't fail — a human or guardrail stopped it.
|
|
958
|
+
{ re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\bCancelled:/i, type: "user-rejected", kind: "rejection" },
|
|
959
|
+
{ re: /\bBlocked:/i, type: "guardrail-blocked", kind: "rejection" },
|
|
960
|
+
// — Real tool failures.
|
|
961
|
+
{ re: /String to replace not found/i, type: "string-not-found", kind: "failure" },
|
|
962
|
+
{ re: /exceeds maximum allowed tokens/i, type: "token-limit", kind: "failure" },
|
|
963
|
+
// A non-zero Bash exit code is a command failure regardless of what its stderr text mentions — check
|
|
964
|
+
// it before the file-state fs-error patterns, since ENOENT/EISDIR also appear inside command stderr
|
|
965
|
+
// (e.g. `Exit code 2 npm error ENOENT`). The file-state patterns then catch the Read/Write/Edit
|
|
966
|
+
// tool_use_errors, which carry no exit code.
|
|
967
|
+
{ re: /\bExit code\b/i, type: "command-failed", kind: "failure" },
|
|
968
|
+
{ re: /has not been read yet|modified since (?:you |it was )?read|File (?:content )?does not exist|\bEISDIR\b|\bENOENT\b/i, type: "file-state", kind: "failure" }
|
|
969
|
+
];
|
|
970
|
+
function classifyToolError(resultSummary) {
|
|
971
|
+
const s = resultSummary ?? "";
|
|
972
|
+
for (const p of PATTERNS) {
|
|
973
|
+
if (p.re.test(s))
|
|
974
|
+
return { type: p.type, kind: p.kind };
|
|
975
|
+
}
|
|
976
|
+
return { type: "other", kind: "failure" };
|
|
977
|
+
}
|
|
978
|
+
|
|
948
979
|
// ../core/dist/pricing.js
|
|
949
980
|
var PRICE_TABLE = {
|
|
950
981
|
// Opus 4.0 / 4.1 launched at $15/$75; Opus 4.5 cut the Opus tier to $5/$25 and 4.6/4.7/4.8 kept it.
|
|
@@ -2041,8 +2072,13 @@ var ClaudeCodeAdapter = class {
|
|
|
2041
2072
|
toolResults.push({
|
|
2042
2073
|
tool_use_id: b.tool_use_id,
|
|
2043
2074
|
result_summary: isAskUser ? JSON.stringify({ answers: tur.answers, annotations: tur.annotations ?? {} }) : summarizeResult(b.content),
|
|
2075
|
+
// Tool failure is signalled by `is_error` on the tool_result block itself — NOT by
|
|
2076
|
+
// toolUseResult.status (whose values are task/lifecycle states like completed/pending/
|
|
2077
|
+
// async_launched and never "error"). Map is_error → status="error" so the error roll-ups,
|
|
2078
|
+
// the transcript `tool-err` styling, and detect.ts's failed-attempt de-escalation all fire.
|
|
2079
|
+
// Non-error results fall back to the toolUseResult lifecycle status when present.
|
|
2080
|
+
status: b.is_error === true ? "error" : tur ? asString(tur.status) : null,
|
|
2044
2081
|
...tur ? {
|
|
2045
|
-
status: asString(tur.status),
|
|
2046
2082
|
agent_type: asString(tur.agentType),
|
|
2047
2083
|
// Deterministic link: a Task/Agent subagent's transcript is keyed off its agentId
|
|
2048
2084
|
// ('agent-'||agentId is the session id assigned by discover()'s filename stem).
|
|
@@ -2832,6 +2868,33 @@ function detect(db, dirty) {
|
|
|
2832
2868
|
return { count, version: DETECTOR_VERSION };
|
|
2833
2869
|
}
|
|
2834
2870
|
|
|
2871
|
+
// ../ingest/dist/errors.js
|
|
2872
|
+
function classifyErrors(db, dirty) {
|
|
2873
|
+
const incremental = dirty != null;
|
|
2874
|
+
const scope = incremental ? " AND session_id IN (SELECT id FROM _dirty_err)" : "";
|
|
2875
|
+
if (incremental) {
|
|
2876
|
+
db.exec("DROP TABLE IF EXISTS _dirty_err");
|
|
2877
|
+
db.exec("CREATE TEMP TABLE _dirty_err (id TEXT PRIMARY KEY)");
|
|
2878
|
+
const ins = db.prepare("INSERT OR IGNORE INTO _dirty_err (id) VALUES (?)");
|
|
2879
|
+
db.transaction((ids) => {
|
|
2880
|
+
for (const id of ids)
|
|
2881
|
+
ins.run(id);
|
|
2882
|
+
})(dirty);
|
|
2883
|
+
}
|
|
2884
|
+
const rows = db.prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`).all();
|
|
2885
|
+
const update = db.prepare("UPDATE tool_calls SET error_type = ? WHERE id = ?");
|
|
2886
|
+
const tx = db.transaction(() => {
|
|
2887
|
+
db.exec(`UPDATE tool_calls SET error_type = NULL WHERE error_type IS NOT NULL${scope}`);
|
|
2888
|
+
for (const r of rows)
|
|
2889
|
+
update.run(classifyToolError(r.result_summary).type, r.id);
|
|
2890
|
+
});
|
|
2891
|
+
tx();
|
|
2892
|
+
if (incremental)
|
|
2893
|
+
db.exec("DROP TABLE IF EXISTS _dirty_err");
|
|
2894
|
+
const count = db.prepare("SELECT COUNT(*) n FROM tool_calls WHERE error_type IS NOT NULL").get().n;
|
|
2895
|
+
return { count, version: ERROR_CLASSIFIER_VERSION };
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2835
2898
|
// ../ingest/dist/pipeline.js
|
|
2836
2899
|
import { createHash as createHash2 } from "crypto";
|
|
2837
2900
|
function newStats() {
|
|
@@ -3767,6 +3830,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
3767
3830
|
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
3768
3831
|
const classified = classify(db, args.full ? null : expanded);
|
|
3769
3832
|
const detected = detect(db, args.full ? null : expanded);
|
|
3833
|
+
classifyErrors(db, args.full ? null : expanded);
|
|
3770
3834
|
const count = (sql) => db.prepare(sql).get().n;
|
|
3771
3835
|
const sessions = count("SELECT COUNT(*) n FROM sessions");
|
|
3772
3836
|
const turns = count("SELECT COUNT(*) n FROM turns");
|
|
@@ -3808,8 +3872,9 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
3808
3872
|
const db = openDb(dbPath);
|
|
3809
3873
|
const r = classify(db);
|
|
3810
3874
|
const d = detect(db);
|
|
3875
|
+
const e = classifyErrors(db);
|
|
3811
3876
|
db.close();
|
|
3812
|
-
console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} db=${dbPath}`);
|
|
3877
|
+
console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} db=${dbPath}`);
|
|
3813
3878
|
}
|
|
3814
3879
|
|
|
3815
3880
|
// ../server/dist/run.js
|
|
@@ -3889,6 +3954,14 @@ function listSessions(db, f) {
|
|
|
3889
3954
|
where.push("s.is_sidechain = 0");
|
|
3890
3955
|
if (f.kind === "subagent")
|
|
3891
3956
|
where.push("s.is_sidechain = 1");
|
|
3957
|
+
if (f.severity?.length) {
|
|
3958
|
+
where.push(`EXISTS (SELECT 1 FROM findings fd WHERE fd.session_id = s.id AND fd.severity IN (${f.severity.map(() => "?").join(",")}))`);
|
|
3959
|
+
params.push(...f.severity);
|
|
3960
|
+
}
|
|
3961
|
+
if (f.errorType?.length) {
|
|
3962
|
+
where.push(`EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.session_id = s.id AND tc.error_type IN (${f.errorType.map(() => "?").join(",")}))`);
|
|
3963
|
+
params.push(...f.errorType);
|
|
3964
|
+
}
|
|
3892
3965
|
if (f.model) {
|
|
3893
3966
|
where.push("EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)");
|
|
3894
3967
|
params.push(f.model);
|
|
@@ -3904,7 +3977,13 @@ function listSessions(db, f) {
|
|
|
3904
3977
|
const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
|
|
3905
3978
|
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at, s.ended_at,
|
|
3906
3979
|
s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
|
|
3907
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3980
|
+
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
3981
|
+
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
|
|
3982
|
+
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,
|
|
3983
|
+
(SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,
|
|
3984
|
+
(SELECT fd.severity FROM findings fd WHERE fd.session_id = s.id
|
|
3985
|
+
ORDER BY CASE fd.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 WHEN 'info' THEN 0 ELSE -1 END DESC
|
|
3986
|
+
LIMIT 1) AS worst_severity
|
|
3908
3987
|
FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
|
|
3909
3988
|
${whereSql}`;
|
|
3910
3989
|
const NATIVE_ORDER = {
|
|
@@ -3915,13 +3994,29 @@ function listSessions(db, f) {
|
|
|
3915
3994
|
};
|
|
3916
3995
|
const sortKey = f.sort ?? "started";
|
|
3917
3996
|
const sqlDir = f.dir === "asc" ? "ASC" : "DESC";
|
|
3997
|
+
const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
|
|
3918
3998
|
let rows;
|
|
3919
|
-
if (sortKey
|
|
3999
|
+
if (JS_SORT.has(sortKey)) {
|
|
3920
4000
|
const all = db.prepare(baseSelect).all(...params);
|
|
3921
4001
|
attachSessionCost(db, all);
|
|
3922
4002
|
const sign = f.dir === "asc" ? 1 : -1;
|
|
4003
|
+
const metric = (r) => {
|
|
4004
|
+
switch (sortKey) {
|
|
4005
|
+
case "tokens":
|
|
4006
|
+
return r.tokens;
|
|
4007
|
+
case "cost":
|
|
4008
|
+
return r.cost;
|
|
4009
|
+
case "errors":
|
|
4010
|
+
return r.tool_error_count ?? 0;
|
|
4011
|
+
// Security: rank by worst severity (info=0 … critical=4; none = -1), finding count as tiebreak.
|
|
4012
|
+
case "security":
|
|
4013
|
+
return (severityRank(r.worst_severity ?? "") + 1) * 1e6 + (r.finding_count ?? 0);
|
|
4014
|
+
default:
|
|
4015
|
+
return 0;
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
3923
4018
|
all.sort((a, b) => {
|
|
3924
|
-
const c = (
|
|
4019
|
+
const c = (metric(a) - metric(b)) * sign;
|
|
3925
4020
|
return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
|
|
3926
4021
|
});
|
|
3927
4022
|
rows = all.slice(f.offset, f.offset + f.limit);
|
|
@@ -3976,7 +4071,7 @@ function getSession(db, id) {
|
|
|
3976
4071
|
const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
|
|
3977
4072
|
FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
|
|
3978
4073
|
const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
|
|
3979
|
-
workflow_run_id, workflow_name, status,
|
|
4074
|
+
workflow_run_id, workflow_name, status, error_type,
|
|
3980
4075
|
total_duration_ms, total_tokens, input_json, result_summary,
|
|
3981
4076
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
|
|
3982
4077
|
FROM tool_calls WHERE session_id = ?`).all(id);
|
|
@@ -4106,6 +4201,10 @@ function getSession(db, id) {
|
|
|
4106
4201
|
FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
|
|
4107
4202
|
WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
|
|
4108
4203
|
ORDER BY t.seq`).all(id);
|
|
4204
|
+
session.tool_call_count = toolRows.length;
|
|
4205
|
+
session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
|
|
4206
|
+
session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
|
|
4207
|
+
session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
|
|
4109
4208
|
return { session, turns, events, classification, parent, children, workflow_runs, findings: sessionFindings };
|
|
4110
4209
|
}
|
|
4111
4210
|
function xmlTag(text, tag) {
|
|
@@ -4456,7 +4555,7 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
4456
4555
|
const get = (b) => {
|
|
4457
4556
|
let r = byBucket.get(b);
|
|
4458
4557
|
if (!r) {
|
|
4459
|
-
r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0 };
|
|
4558
|
+
r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0, failures: 0, rejections: 0 };
|
|
4460
4559
|
byBucket.set(b, r);
|
|
4461
4560
|
}
|
|
4462
4561
|
return r;
|
|
@@ -4479,6 +4578,16 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
4479
4578
|
if (t.b)
|
|
4480
4579
|
get(t.b).turns = t.n;
|
|
4481
4580
|
}
|
|
4581
|
+
for (const e of db.prepare(`SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
4582
|
+
${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`).all(...w.params)) {
|
|
4583
|
+
if (!e.b)
|
|
4584
|
+
continue;
|
|
4585
|
+
const r = get(e.b);
|
|
4586
|
+
if (e.et && errorKind(e.et) === "rejection")
|
|
4587
|
+
r.rejections += e.n;
|
|
4588
|
+
else
|
|
4589
|
+
r.failures += e.n;
|
|
4590
|
+
}
|
|
4482
4591
|
const series = [...byBucket.values()].sort((a, b) => a.bucket.localeCompare(b.bucket));
|
|
4483
4592
|
for (const r of series)
|
|
4484
4593
|
r.cost = Number(r.cost.toFixed(4));
|
|
@@ -4529,7 +4638,20 @@ function dashboardBreakdowns(db, f) {
|
|
|
4529
4638
|
max_per_session: counts.length ? Math.max(...counts) : 0,
|
|
4530
4639
|
avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0
|
|
4531
4640
|
};
|
|
4532
|
-
|
|
4641
|
+
const errorRows = db.prepare(`SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
4642
|
+
${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
|
|
4643
|
+
let failures = 0;
|
|
4644
|
+
let rejections = 0;
|
|
4645
|
+
const byType = errorRows.map((r) => {
|
|
4646
|
+
const kind = errorKind(r.type);
|
|
4647
|
+
if (kind === "rejection")
|
|
4648
|
+
rejections += r.n;
|
|
4649
|
+
else
|
|
4650
|
+
failures += r.n;
|
|
4651
|
+
return { type: r.type, kind, n: r.n };
|
|
4652
|
+
});
|
|
4653
|
+
const errorTypes = { by_type: byType, failures, rejections };
|
|
4654
|
+
return { by_model: byModel, by_source: bySource, by_category: byCategory, by_complexity: byComplexity, tools, skills, skill_versions: skillVersions, subagent_fanout: subagentFanout, error_types: errorTypes };
|
|
4533
4655
|
}
|
|
4534
4656
|
|
|
4535
4657
|
// ../server/dist/refresh.js
|
|
@@ -4691,7 +4813,9 @@ async function createApp(db, opts = {}) {
|
|
|
4691
4813
|
from: q.from,
|
|
4692
4814
|
to: q.to,
|
|
4693
4815
|
kind: q.kind === "main" || q.kind === "subagent" ? q.kind : void 0,
|
|
4694
|
-
|
|
4816
|
+
severity: q.severity ? q.severity.split(",").filter(Boolean) : void 0,
|
|
4817
|
+
errorType: q.error_type ? q.error_type.split(",").filter(Boolean) : void 0,
|
|
4818
|
+
sort: ["started", "title", "turns", "tokens", "cost", "duration", "errors", "security"].find((s) => s === q.sort),
|
|
4695
4819
|
dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
|
|
4696
4820
|
limit: Math.min(Number(q.limit) || 50, 200),
|
|
4697
4821
|
offset: Number(q.offset) || 0
|