@agentguard-run/burn 0.2.5 → 0.2.6

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +48 -17
  3. package/dist/src/calibrate.js +2 -3
  4. package/dist/src/cli.js +14 -3
  5. package/dist/src/conformance.d.ts +5 -2
  6. package/dist/src/conformance.js +30 -17
  7. package/dist/src/defaults.d.ts +5 -4
  8. package/dist/src/defaults.js +7 -6
  9. package/dist/src/detectors/evaluate.d.ts +4 -5
  10. package/dist/src/detectors/evaluate.js +13 -11
  11. package/dist/src/gateway.js +2 -8
  12. package/dist/src/history/claude-transcript.d.ts +20 -2
  13. package/dist/src/history/claude-transcript.js +56 -15
  14. package/dist/src/hook/pre-tool-use.d.ts +13 -9
  15. package/dist/src/hook/pre-tool-use.js +55 -31
  16. package/dist/src/insights/blocks.d.ts +61 -0
  17. package/dist/src/insights/blocks.js +243 -0
  18. package/dist/src/insights/transcript.d.ts +3 -1
  19. package/dist/src/insights/transcript.js +14 -1
  20. package/dist/src/insights/types.d.ts +7 -0
  21. package/dist/src/policy.d.ts +4 -0
  22. package/dist/src/policy.js +57 -0
  23. package/dist/src/replay/render.js +4 -2
  24. package/dist/src/replay/simulate.d.ts +5 -0
  25. package/dist/src/replay/simulate.js +23 -8
  26. package/dist/src/state/reservations.d.ts +2 -0
  27. package/dist/src/state/reservations.js +5 -1
  28. package/dist/src/state/spawn-window.d.ts +10 -0
  29. package/dist/src/state/spawn-window.js +25 -0
  30. package/dist/src/types.d.ts +2 -1
  31. package/fixtures/usage-dedup-session/subagents/agent-synthetic-first.jsonl +5 -0
  32. package/fixtures/usage-dedup-session/subagents/agent-synthetic-second.jsonl +4 -0
  33. package/fixtures/usage-dedup-session.jsonl +4 -0
  34. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.providerResponseIdentity = void 0;
3
4
  exports.parseInsightTranscript = parseInsightTranscript;
4
5
  exports.readInsightTranscript = readInsightTranscript;
5
6
  exports.readInsightSession = readInsightSession;
@@ -11,6 +12,9 @@ const object = (value) => value !== null && typeof value === 'object' && !Array.
11
12
  const count = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
12
13
  const hasCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
13
14
  const digest = (value) => (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(value)).digest('hex');
15
+ /** Provider response identity is shared by reporting and enforcement readers. */
16
+ const providerResponseIdentity = (host, id) => digest(['provider-response', host, id]);
17
+ exports.providerResponseIdentity = providerResponseIdentity;
14
18
  const identifier = (value) => typeof value === 'string' && /^[A-Za-z0-9_.:/-]{1,128}$/.test(value) ? value : undefined;
15
19
  const modelName = identifier;
16
20
  function millis(value) {
@@ -260,9 +264,18 @@ function parseInsightTranscript(text, options = {}) {
260
264
  const message = object(raw.message), payload = object(raw.payload), response = object(raw.response);
261
265
  const extracted = usageOf(raw), usage = extracted.usage;
262
266
  const at = millis(raw.timestamp ?? raw.created_at ?? payload.timestamp);
267
+ if (raw.type === 'fork-context-ref') {
268
+ state.lineage = { ...state.lineage, forkContextRef: true, forked: true,
269
+ ...(identifier(raw.parentSessionId) ? { parentSessionId: identifier(raw.parentSessionId) } : {}) };
270
+ }
263
271
  if (raw.type === 'session_meta' && !state.sessionMetadataSeen) {
264
272
  state.sessionMetadataSeen = true;
265
273
  state.host = 'codex';
274
+ const spawned = object(object(object(payload.source).subagent).thread_spawn);
275
+ state.lineage = { ...state.lineage,
276
+ ...(identifier(payload.id ?? payload.session_id) ? { sessionId: identifier(payload.id ?? payload.session_id) } : {}),
277
+ ...(identifier(payload.parent_thread_id ?? spawned.parent_thread_id) ? { parentSessionId: identifier(payload.parent_thread_id ?? spawned.parent_thread_id) } : {}),
278
+ ...(identifier(payload.forked_from_id) ? { forked: true } : {}) };
266
279
  if ('subagent' in object(payload.source) || identifier(payload.parent_thread_id) || identifier(payload.agent_path))
267
280
  state.spawnedSession = true;
268
281
  if (hasCount(payload.subagent_history_start_ordinal))
@@ -371,7 +384,7 @@ function parseInsightTranscript(text, options = {}) {
371
384
  // Provider response ids remain the same when a host copies history into a
372
385
  // forked session. Preserve global identity instead of charging it twice.
373
386
  const providerId = message.id !== undefined || response.id !== undefined;
374
- const id = digest([providerId ? 'provider-response' : extracted.codex && state.codexTurnIdDigest ? 'codex-turn' : state.sessionId, host, nativeId]);
387
+ const id = providerId ? (0, exports.providerResponseIdentity)(host, nativeId) : digest([extracted.codex && state.codexTurnIdDigest ? 'codex-turn' : state.sessionId, host, nativeId]);
375
388
  const existing = state.seen[id];
376
389
  const creation = object(usage.cache_creation);
377
390
  const split5m = creation.ephemeral_5m_input_tokens, split1h = creation.ephemeral_1h_input_tokens;
@@ -77,6 +77,13 @@ export interface InsightParserState {
77
77
  fileKey?: string;
78
78
  }>;
79
79
  sessionMetadataSeen?: boolean;
80
+ /** Host-recorded lineage identifiers only, never inherited prompt content. */
81
+ lineage?: {
82
+ sessionId?: string;
83
+ parentSessionId?: string;
84
+ forkContextRef?: boolean;
85
+ forked?: boolean;
86
+ };
80
87
  inheritedBeforeOrdinal?: number;
81
88
  forkHistoryBoundaryUnknown?: boolean;
82
89
  inheritedTotalDigest?: string;
@@ -0,0 +1,4 @@
1
+ import type { Policy } from './types';
2
+ /** Normalize before evaluating or hashing so receipts bind the actual policy. */
3
+ export declare function normalizePolicy(policy: Policy): Policy;
4
+ export declare function loadPolicy(home: string): Policy;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePolicy = normalizePolicy;
4
+ exports.loadPolicy = loadPolicy;
5
+ /** Load additive policy defaults without rewriting the operator's policy. */
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const defaults_1 = require("./defaults");
9
+ const noticed = new Set();
10
+ /** Normalize before evaluating or hashing so receipts bind the actual policy. */
11
+ function normalizePolicy(policy) {
12
+ const thresholds = policy.thresholds;
13
+ const normalized = {
14
+ fanout: { ...defaults_1.DEFAULT_THRESHOLDS.fanout, ...thresholds.fanout },
15
+ sustained: { ...defaults_1.DEFAULT_THRESHOLDS.sustained, ...thresholds.sustained },
16
+ burnDebt: { ...defaults_1.DEFAULT_THRESHOLDS.burnDebt, ...thresholds.burnDebt },
17
+ spawnRate: { ...defaults_1.DEFAULT_THRESHOLDS.spawnRate, ...thresholds.spawnRate },
18
+ duplicate: { ...defaults_1.DEFAULT_THRESHOLDS.duplicate, ...thresholds.duplicate },
19
+ account: { ...defaults_1.DEFAULT_THRESHOLDS.account, ...thresholds.account },
20
+ localCompute: { ...defaults_1.DEFAULT_THRESHOLDS.localCompute, ...thresholds.localCompute },
21
+ };
22
+ return { ...policy, thresholds: normalized };
23
+ }
24
+ function noticeOnce(home, fields) {
25
+ if (noticed.has(home))
26
+ return;
27
+ noticed.add(home);
28
+ try {
29
+ (0, node_fs_1.mkdirSync)(home, { recursive: true, mode: 0o700 });
30
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(home, '.burn-policy-0.2.6-notice'), fields.join(', ') + '\n', { flag: 'wx', mode: 0o600 });
31
+ }
32
+ catch (error) {
33
+ if (error.code === 'EEXIST')
34
+ return;
35
+ // A read-only home still gets the notice once in this process.
36
+ }
37
+ process.stderr.write(`AgentGuard loaded missing policy fields from defaults: ${fields.join(', ')}; existing overrides are unchanged.\n`);
38
+ }
39
+ function loadPolicy(home) {
40
+ try {
41
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, 'burn-policy.json'), 'utf8'));
42
+ if (parsed && (parsed.mode === 'shadow' || parsed.mode === 'enforce') && parsed.thresholds) {
43
+ const missing = [];
44
+ if (parsed.thresholds.fanout?.windowActiveMinutes === undefined)
45
+ missing.push('fanout.windowActiveMinutes=120');
46
+ if (parsed.thresholds.spawnRate?.enforce === undefined)
47
+ missing.push('spawnRate.enforce=true');
48
+ if (missing.length)
49
+ noticeOnce(home, missing);
50
+ return normalizePolicy(parsed);
51
+ }
52
+ }
53
+ catch {
54
+ /* no policy yet: shadow defaults */
55
+ }
56
+ return normalizePolicy(defaults_1.DEFAULT_POLICY);
57
+ }
@@ -150,13 +150,15 @@ function renderReplay(summary, opts = {}) {
150
150
  paint(on, C.dim, ` across ${summary.sessions.length} sessions, ${summary.totalSpawns} spawns`)));
151
151
  out.push(MID);
152
152
  for (const s of summary.sessions.slice(0, top)) {
153
- const tag = s.fanoutStop ? paint(on, C.red, 'FAN-OUT STOP') : s.sustainedStop ? paint(on, C.red, 'SUSTAINED STOP') : s.firstWarn ? paint(on, C.yellow, 'WARN') : paint(on, C.green, 'clean');
153
+ const tag = s.fanoutStop ? paint(on, C.red, 'FAN-OUT STOP') : s.sustainedStop ? paint(on, C.red, 'SUSTAINED STOP') : s.spawnRateStop ? paint(on, C.red, 'SPAWN-RATE STOP') : s.firstWarn ? paint(on, C.yellow, 'WARN') : paint(on, C.green, 'clean');
154
154
  out.push(row(`${sparkline(s.curve, s.stopAtIndex, on)} ${tag}`));
155
155
  const detail = s.fanoutStop
156
156
  ? `before spawn ${s.fanoutStop.atSpawn}, tail ${(0, evaluate_1.fmt)(s.catchableTail)}`
157
157
  : s.sustainedStop
158
158
  ? `near ${(0, evaluate_1.fmt)(s.sustainedStop.tokensAtStop)}, tail ${(0, evaluate_1.fmt)(s.catchableTail)}`
159
- : `${(0, evaluate_1.fmt)(s.totalTokens)} · ${s.spawns} spawns`;
159
+ : s.spawnRateStop
160
+ ? `before spawn ${s.spawnRateStop.atSpawn}, tail ${(0, evaluate_1.fmt)(s.catchableTail)}`
161
+ : `${(0, evaluate_1.fmt)(s.totalTokens)} · ${s.spawns} spawns`;
160
162
  out.push(row(paint(on, C.dim, `${s.sessionId.slice(0, 8)} ${(0, evaluate_1.fmt)(s.totalTokens).padStart(6)} · ${String(s.spawns).padStart(3)} spawns ${detail}`)));
161
163
  }
162
164
  if (summary.sessions.length > top) {
@@ -30,6 +30,11 @@ export interface SessionReplay {
30
30
  atSpawn: number;
31
31
  tokensAtStop: number;
32
32
  } | null;
33
+ /** Additive boundary for policies that enforce short-window spawn rate. */
34
+ spawnRateStop?: {
35
+ atSpawn: number;
36
+ tokensAtStop: number;
37
+ } | null;
33
38
  /** Tokens observed after the earliest STOP boundary. */
34
39
  catchableTail: number;
35
40
  firstWarn: {
@@ -58,6 +58,7 @@ function replayEvents(sessionId, path, events, thresholds) {
58
58
  const state = (0, session_1.newSessionState)(sessionId, first.at);
59
59
  let fanoutStop = null;
60
60
  let sustainedStop = null;
61
+ let spawnRateStop = null;
61
62
  let firstWarn = null;
62
63
  let tokensAtEarliestStop = null;
63
64
  // Cumulative tokens after every event, for the sparkline.
@@ -66,25 +67,38 @@ function replayEvents(sessionId, path, events, thresholds) {
66
67
  for (const event of events) {
67
68
  // A hook boundary exists only where a spawn was attempted. Evaluate the
68
69
  // *proposal* before applying the event, then apply it.
70
+ const candidateState = event.spawns.length > 0
71
+ ? { ...state, spawnsByActiveMinute: new Map(state.spawnsByActiveMinute) }
72
+ : state;
73
+ if (event.spawns.length > 0) {
74
+ // One assistant row can propose several agents. Advance its active-time
75
+ // clock once, then account for each earlier proposal in this row. Usage
76
+ // and the durable state are still folded only once below.
77
+ (0, session_1.applyEvent)(candidateState, { at: event.at, tokens: 0, cacheRead: 0, spawns: [], surfaces: [], sidechain: event.sidechain });
78
+ }
69
79
  for (const spawn of event.spawns) {
70
- const report = (0, evaluate_1.evaluate)(state, thresholds, spawn.issuerDepth + 1);
80
+ const report = (0, evaluate_1.evaluate)(candidateState, thresholds, spawn.issuerDepth + 1);
71
81
  if (!firstWarn && report.verdict !== 'OK') {
72
- firstWarn = { detector: report.findings[0]?.detector ?? 'unknown', tokensAt: state.totalTokens, spawnsAt: state.spawnCount + 1 };
82
+ firstWarn = { detector: report.findings[0]?.detector ?? 'unknown', tokensAt: state.totalTokens, spawnsAt: candidateState.spawnCount + 1 };
73
83
  }
74
84
  for (const finding of report.findings) {
75
85
  if (finding.verdict !== 'STOP')
76
86
  continue;
77
87
  if (finding.detector === 'fanout' && !fanoutStop) {
78
- fanoutStop = { atSpawn: state.spawnCount + 1, tokensAtStop: state.totalTokens };
88
+ fanoutStop = { atSpawn: candidateState.spawnCount + 1, tokensAtStop: state.totalTokens };
89
+ }
90
+ if (finding.detector === 'spawn_rate' && !spawnRateStop) {
91
+ spawnRateStop = { atSpawn: candidateState.spawnCount + 1, tokensAtStop: state.totalTokens };
79
92
  }
80
93
  if ((finding.detector === 'sustained_burn' || finding.detector === 'burn_debt') && !sustainedStop) {
81
- sustainedStop = { atSpawn: state.spawnCount + 1, tokensAtStop: state.totalTokens };
94
+ sustainedStop = { atSpawn: candidateState.spawnCount + 1, tokensAtStop: state.totalTokens };
82
95
  }
83
96
  }
84
- if ((fanoutStop || sustainedStop) && tokensAtEarliestStop === null) {
97
+ if ((fanoutStop || sustainedStop || spawnRateStop) && tokensAtEarliestStop === null) {
85
98
  tokensAtEarliestStop = state.totalTokens;
86
99
  stopEventIndex = timeline.length;
87
100
  }
101
+ (0, session_1.applyEvent)(candidateState, { at: event.at, tokens: 0, cacheRead: 0, spawns: [spawn], surfaces: [], sidechain: event.sidechain });
88
102
  }
89
103
  // The sustained plane also has a boundary at every tool call, not just
90
104
  // spawns. Approximate: any event with surfaces is a tool boundary.
@@ -118,6 +132,7 @@ function replayEvents(sessionId, path, events, thresholds) {
118
132
  finalVerdict: final.verdict,
119
133
  fanoutStop,
120
134
  sustainedStop,
135
+ spawnRateStop,
121
136
  catchableTail: tokensAtEarliestStop === null ? 0 : Math.max(0, state.totalTokens - tokensAtEarliestStop),
122
137
  firstWarn,
123
138
  curve,
@@ -160,8 +175,8 @@ function replayAll(paths, thresholds, minTokens = 0) {
160
175
  totalSpawns: sessions.reduce((s, r) => s + r.spawns, 0),
161
176
  catchableTail,
162
177
  catchableShare: totalTokens > 0 ? catchableTail / totalTokens : 0,
163
- stops: sessions.filter((r) => r.fanoutStop || r.sustainedStop).length,
164
- warns: sessions.filter((r) => !r.fanoutStop && !r.sustainedStop && r.firstWarn).length,
165
- clean: sessions.filter((r) => !r.fanoutStop && !r.sustainedStop && !r.firstWarn).length,
178
+ stops: sessions.filter((r) => r.fanoutStop || r.sustainedStop || r.spawnRateStop).length,
179
+ warns: sessions.filter((r) => !r.fanoutStop && !r.sustainedStop && !r.spawnRateStop && r.firstWarn).length,
180
+ clean: sessions.filter((r) => !r.fanoutStop && !r.sustainedStop && !r.spawnRateStop && !r.firstWarn).length,
166
181
  };
167
182
  }
@@ -152,6 +152,8 @@ export declare class Transaction {
152
152
  private readonly data;
153
153
  dirty: boolean;
154
154
  constructor(data: ReservationFile);
155
+ /** Pending proposals participate in every spawn window under the same lock. */
156
+ pendingSpawns(sessionId: string, toolUseId: string, now: number): number;
155
157
  reserve(args: ReserveArgs): ReserveResult;
156
158
  reconcile(sessionId: string, observedSpawns: number, previouslyObserved: number): void;
157
159
  /**
@@ -310,6 +310,10 @@ class Transaction {
310
310
  if (!this.data.calls)
311
311
  this.data.calls = [];
312
312
  }
313
+ /** Pending proposals participate in every spawn window under the same lock. */
314
+ pendingSpawns(sessionId, toolUseId, now) {
315
+ return this.data.reservations.filter(r => r.sessionId === sessionId && r.toolUseId !== toolUseId && r.expiresAt > now).length;
316
+ }
313
317
  reserve(args) {
314
318
  const now = args.now ?? Date.now();
315
319
  const data = this.data;
@@ -318,7 +322,7 @@ class Transaction {
318
322
  if (data.reservations.length !== before)
319
323
  this.dirty = true;
320
324
  // Idempotent: the same tool_use_id evaluated twice must not double-count.
321
- const existing = data.reservations.find((r) => r.toolUseId === args.toolUseId);
325
+ const existing = data.reservations.find((r) => r.sessionId === args.sessionId && r.toolUseId === args.toolUseId);
322
326
  const pendingForSession = data.reservations.filter((r) => r.sessionId === args.sessionId && r.toolUseId !== args.toolUseId).length;
323
327
  const effective = args.observedSpawns + pendingForSession + 1;
324
328
  if (existing) {
@@ -0,0 +1,10 @@
1
+ import type { BurnReport, SessionState, Thresholds } from '../types';
2
+ import type { ReserveResult, Transaction } from './reservations';
3
+ /** Evaluate and reserve together. Pending forks must not bypass either window. */
4
+ export declare function evaluateSpawnReservation(tx: Transaction, state: SessionState, thresholds: Thresholds, proposedDepth: number, toolUseId: string, now: number, account?: {
5
+ sessions: SessionState[];
6
+ now: number;
7
+ }): {
8
+ report: BurnReport;
9
+ reservation: ReserveResult;
10
+ };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateSpawnReservation = evaluateSpawnReservation;
4
+ const evaluate_1 = require("../detectors/evaluate");
5
+ const defaults_1 = require("../defaults");
6
+ const session_1 = require("./session");
7
+ /** Evaluate and reserve together. Pending forks must not bypass either window. */
8
+ function evaluateSpawnReservation(tx, state, thresholds, proposedDepth, toolUseId, now, account) {
9
+ const pending = tx.pendingSpawns(state.sessionId, toolUseId, now);
10
+ const minute = Math.floor(state.activeMinutes);
11
+ const spawns = new Map(state.spawnsByActiveMinute);
12
+ if (pending)
13
+ spawns.set(minute, (spawns.get(minute) ?? 0) + pending);
14
+ const candidateState = { ...state, spawnCount: state.spawnCount + pending, spawnsByActiveMinute: spawns };
15
+ const report = (0, evaluate_1.evaluate)(candidateState, thresholds, proposedDepth, account);
16
+ // Receipts retain the lifetime ordinal, while admission uses the active window.
17
+ const effectiveSpawns = state.spawnCount + pending + 1;
18
+ if (report.verdict === 'STOP') {
19
+ // A refused proposal never consumes a reservation. Its detector explains why.
20
+ return { report, reservation: { allowed: true, effectiveSpawns, pending } };
21
+ }
22
+ const recent = (0, session_1.windowSum)(state.spawnsByActiveMinute, state.activeMinutes, thresholds.fanout.windowActiveMinutes ?? defaults_1.DEFAULT_THRESHOLDS.fanout.windowActiveMinutes);
23
+ const reservation = tx.reserve({ sessionId: state.sessionId, toolUseId, observedSpawns: recent, ceiling: thresholds.fanout.stop, now });
24
+ return { report, reservation: { ...reservation, effectiveSpawns } };
25
+ }
@@ -83,7 +83,8 @@ export interface Thresholds {
83
83
  fanout: {
84
84
  warn: number;
85
85
  stop: number;
86
- maxDepth: number;
86
+ maxDepth: number; /** Older policies use 120 active minutes. */
87
+ windowActiveMinutes?: number;
87
88
  };
88
89
  sustained: {
89
90
  warnTokens: number;
@@ -0,0 +1,5 @@
1
+ {"type":"fork-context-ref"}
2
+ {"type":"assistant","timestamp":"2026-09-01T00:00:02Z","uuid":"synthetic-root-block-3","message":{"id":"synthetic-response-a","model":"claude-sonnet-5","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":100,"output_tokens":9},"content":[]}}
3
+ {"type":"assistant","timestamp":"2026-09-01T00:00:03Z","uuid":"synthetic-root-spawn","message":{"id":"synthetic-response-b","model":"claude-sonnet-5","usage":{"input_tokens":11,"cache_read_input_tokens":130,"output_tokens":2},"content":[{"type":"tool_use","id":"synthetic-spawn-1","name":"Agent","input":{"description":"synthetic task"}}]}}
4
+ {"type":"assistant","timestamp":"2026-09-01T00:00:04Z","uuid":"synthetic-child-block-1","message":{"id":"synthetic-response-c","model":"claude-sonnet-5","usage":{"input_tokens":5,"cache_creation_input_tokens":30,"cache_read_input_tokens":150,"output_tokens":10},"content":[]}}
5
+ {"type":"assistant","timestamp":"2026-09-01T00:00:05Z","uuid":"synthetic-child-block-2","message":{"id":"synthetic-response-c","model":"claude-sonnet-5","usage":{"input_tokens":5,"cache_creation_input_tokens":30,"cache_read_input_tokens":150,"output_tokens":12},"content":[]}}
@@ -0,0 +1,4 @@
1
+ {"type":"fork-context-ref"}
2
+ {"type":"assistant","timestamp":"2026-09-01T00:00:02Z","uuid":"synthetic-root-block-3","message":{"id":"synthetic-response-a","model":"claude-sonnet-5","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":100,"output_tokens":9},"content":[]}}
3
+ {"type":"assistant","timestamp":"2026-09-01T00:00:05Z","uuid":"synthetic-copy-c","message":{"id":"synthetic-response-c","model":"claude-sonnet-5","usage":{"input_tokens":5,"cache_creation_input_tokens":30,"cache_read_input_tokens":150,"output_tokens":12},"content":[]}}
4
+ {"type":"assistant","timestamp":"2026-09-01T00:00:06Z","uuid":"synthetic-child-d","message":{"id":"synthetic-response-d","model":"claude-sonnet-5","usage":{"input_tokens":2,"cache_read_input_tokens":200,"output_tokens":3},"content":[]}}
@@ -0,0 +1,4 @@
1
+ {"type":"assistant","timestamp":"2026-09-01T00:00:00Z","uuid":"synthetic-root-block-1","message":{"id":"synthetic-response-a","model":"claude-sonnet-5","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":100,"output_tokens":5},"content":[]}}
2
+ {"type":"assistant","timestamp":"2026-09-01T00:00:01Z","uuid":"synthetic-root-block-2","message":{"id":"synthetic-response-a","model":"claude-sonnet-5","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":100,"output_tokens":5},"content":[]}}
3
+ {"type":"assistant","timestamp":"2026-09-01T00:00:02Z","uuid":"synthetic-root-block-3","message":{"id":"synthetic-response-a","model":"claude-sonnet-5","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":100,"output_tokens":9},"content":[]}}
4
+ {"type":"assistant","timestamp":"2026-09-01T00:00:03Z","uuid":"synthetic-root-spawn","message":{"id":"synthetic-response-b","model":"claude-sonnet-5","usage":{"input_tokens":11,"cache_read_input_tokens":130,"output_tokens":2},"content":[{"type":"tool_use","id":"synthetic-spawn-1","name":"Agent","input":{"description":"synthetic task"}}]}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentguard-run/burn",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Local session usage and runaway-agent circuit breaker. Explain tokens, cache rewrites and API list cost, track pace, warn on heavy turns, and gate agent fan-out with signed receipts. Nothing leaves the machine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",