@faithfulalabi/agent-lens 0.6.0 → 0.7.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.
@@ -2,16 +2,13 @@ import { existsSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
3
  import { isUnderAnyRoot } from '../archive/paths.js';
4
4
  import { toolResultsDirOf } from '../corpus/paths.js';
5
- import { contentBlocks } from '../transcript/blocks.js';
5
+ import { contentBlocks, imageSourceAt, resultText, SERVABLE_IMAGE_TYPES, } from '../transcript/blocks.js';
6
6
  import { DriftCounter } from '../transcript/drift.js';
7
7
  import { classifyLine } from '../transcript/line.js';
8
8
  function byteLength(text) {
9
9
  return Buffer.byteLength(text, 'utf8');
10
10
  }
11
- function resultText(children) {
12
- return children.flatMap((child) => (child.kind === 'text' ? [child.text] : [])).join('\n');
13
- }
14
- function blockAt(env, archivePath, byteOffset, len, blockIndex) {
11
+ function lineAt(env, archivePath, byteOffset, len) {
15
12
  let bytes;
16
13
  try {
17
14
  bytes = env.reader.read(archivePath, byteOffset, len);
@@ -22,17 +19,20 @@ function blockAt(env, archivePath, byteOffset, len, blockIndex) {
22
19
  if (bytes.length === 0)
23
20
  return undefined;
24
21
  try {
25
- const line = classifyLine(JSON.parse(bytes.toString('utf8')), {
22
+ return classifyLine(JSON.parse(bytes.toString('utf8')), {
26
23
  byteOffset,
27
24
  byteLength: len,
28
25
  drift: new DriftCounter(),
29
26
  });
30
- return contentBlocks(line)[blockIndex];
31
27
  }
32
28
  catch {
33
29
  return undefined;
34
30
  }
35
31
  }
32
+ function blockAt(env, archivePath, byteOffset, len, blockIndex) {
33
+ const line = lineAt(env, archivePath, byteOffset, len);
34
+ return line === undefined ? undefined : contentBlocks(line)[blockIndex];
35
+ }
36
36
  function stored(row, field) {
37
37
  const text = (field === 'text' ? row.text : row.input) ?? '';
38
38
  return { text, bytes: field === 'text' ? row.text_bytes : row.input_bytes };
@@ -148,6 +148,33 @@ function resolveText(row, archivePath, env) {
148
148
  return fromColumn(row, 'text', row.output_storage);
149
149
  }
150
150
  }
151
+ export const IMAGE_LINE_MAX = 16 * 1024 * 1024;
152
+ function imageLineOf(row) {
153
+ const { result_offset, result_len, result_block } = row;
154
+ if (result_offset !== null && result_len !== null && result_block !== null) {
155
+ return { offset: result_offset, len: result_len, block: result_block };
156
+ }
157
+ return row.block_index === null
158
+ ? undefined
159
+ : { offset: row.src_offset, len: row.src_len, block: row.block_index };
160
+ }
161
+ export function resolveImage(row, ordinal, archivePath, env) {
162
+ const at = imageLineOf(row);
163
+ if (at === undefined)
164
+ return { ok: false, reason: 'gone' };
165
+ if (at.len > IMAGE_LINE_MAX)
166
+ return { ok: false, reason: 'too_large' };
167
+ const line = archivePath === undefined ? undefined : lineAt(env, archivePath, at.offset, at.len);
168
+ const source = line === undefined ? undefined : imageSourceAt(line, at.block, ordinal);
169
+ if (source === undefined)
170
+ return { ok: false, reason: 'gone' };
171
+ if (!SERVABLE_IMAGE_TYPES.has(source.media_type))
172
+ return { ok: false, reason: 'unsupported' };
173
+ const bytes = Buffer.from(source.data, 'base64');
174
+ if (bytes.length === 0)
175
+ return { ok: false, reason: 'gone' };
176
+ return { ok: true, media_type: source.media_type, bytes };
177
+ }
151
178
  export function createContentEnv(reader, roots) {
152
179
  return {
153
180
  reader,
@@ -161,3 +188,6 @@ export function createContentResolver(archivePathOf, env) {
161
188
  export function createSpillLocator(archivePathOf, env) {
162
189
  return (row) => spillSource(row, archivePathOf(row.session_id), env);
163
190
  }
191
+ export function createImageResolver(archivePathOf, env) {
192
+ return (row, ordinal) => resolveImage(row, ordinal, archivePathOf(row.session_id), env);
193
+ }
@@ -131,7 +131,7 @@ const EVENT_COLUMNS = `id, turn_id, seq, kind, ts, request_id, block_index, name
131
131
  wire_input, wire_input_bytes, wire_input_storage,
132
132
  text, text_bytes, output_storage, spill_path, spill_bytes,
133
133
  model, tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, est_cost,
134
- child_session_id, agent_type, agent_status, raw_type, raw_subtype`;
134
+ child_session_id, agent_type, agent_status, raw_type, raw_subtype, image_count`;
135
135
  export function buildEventPageSql(session_id, query) {
136
136
  return {
137
137
  sql: `SELECT ${EVENT_COLUMNS} FROM events WHERE session_id = ? AND seq >= ?` +
@@ -166,7 +166,7 @@ export function readEventsByIds(db, session_id, ids) {
166
166
  }
167
167
  export const EVENT_CONTENT_COLUMNS = `id, session_id, block_index, input, input_bytes, input_storage,
168
168
  text, text_bytes, output_storage, spill_path, spill_bytes,
169
- src_offset, src_len, result_offset, result_len, result_block`;
169
+ src_offset, src_len, result_offset, result_len, result_block, image_count`;
170
170
  export function readEventContentRow(db, event_id) {
171
171
  return db
172
172
  .prepare(`SELECT ${EVENT_CONTENT_COLUMNS} FROM events WHERE id = ?`)
@@ -1,4 +1,4 @@
1
- export const SCHEMA_VERSION = 4;
1
+ export const SCHEMA_VERSION = 5;
2
2
  export const SCHEMA_DDL = `-- ~/.agent-lens/cache.db
3
3
  --
4
4
  -- THIS FILE IS A CACHE. Every byte is a pure function of ~/.agent-lens/archive/** --
@@ -21,7 +21,7 @@ export const SCHEMA_DDL = `-- ~/.agent-lens/cache.db
21
21
  PRAGMA journal_mode = WAL;
22
22
  PRAGMA synchronous = NORMAL;
23
23
  PRAGMA foreign_keys = OFF; -- projections are dropped wholesale, per session, by hand
24
- PRAGMA user_version = 4; -- SCHEMA_VERSION; mismatch => unlink + recreate
24
+ PRAGMA user_version = 5; -- SCHEMA_VERSION; mismatch => unlink + recreate
25
25
 
26
26
  -- ===================================================================== sessions
27
27
  -- The file index AND the session-list row AND the projection freshness header,
@@ -254,6 +254,7 @@ CREATE TABLE events (
254
254
  result_offset INTEGER, -- byte offset of the tool_result line
255
255
  result_len INTEGER,
256
256
  result_block INTEGER,
257
+ image_count INTEGER NOT NULL DEFAULT 0, -- images servable by ordinal; never their bytes
257
258
 
258
259
  model TEXT, -- \\ stamped on the FIRST event of each requestId
259
260
  tokens_in INTEGER, -- } group ONLY. Naive summing is 1.51x HIGH,
@@ -66,10 +66,10 @@ const INSERT_EVENT_SQL = `INSERT INTO events
66
66
  duration_ms, duration_source, input, input_bytes, input_storage,
67
67
  wire_input, wire_input_bytes, wire_input_storage, text, text_bytes,
68
68
  output_storage, spill_path, spill_bytes, src_offset, src_len, result_offset,
69
- result_len, result_block, model, tokens_in, tokens_out, tokens_cache_read,
69
+ result_len, result_block, image_count, model, tokens_in, tokens_out, tokens_cache_read,
70
70
  tokens_cache_write, est_cost, child_session_id, agent_type, agent_status, raw_type,
71
71
  raw_subtype, attrs)
72
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
72
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
73
73
  const POPULATE_FTS_SQL = `INSERT INTO events_fts(rowid, text, input)
74
74
  SELECT rowid, text, input FROM events WHERE session_id = ?`;
75
75
  const WRITE_HEADER_SQL = `UPDATE sessions SET
@@ -179,7 +179,7 @@ function insertEvents(db, events, spills) {
179
179
  cache_read: event.tokens_cache_read,
180
180
  cache_write: event.tokens_cache_write,
181
181
  });
182
- insert.run(event.id, event.session_id, event.turn_id, event.seq, event.kind, event.ts, event.request_id ?? null, event.block_index, event.name ?? null, event.status ?? null, event.duration_ms ?? null, event.duration_source ?? null, event.input ?? null, event.input_bytes ?? null, event.input_storage ?? null, event.wire_input ?? null, event.wire_input_bytes ?? null, event.wire_input_storage ?? null, event.text ?? null, event.text_bytes ?? null, output.output_storage, output.spill_path, output.spill_bytes, event.src_offset, event.src_len, event.result_offset ?? null, event.result_len ?? null, event.result_block ?? null, event.model ?? null, event.tokens_in ?? null, event.tokens_out ?? null, event.tokens_cache_read ?? null, event.tokens_cache_write ?? null, est_cost, event.child_session_id ?? null, event.agent_type ?? null, event.agent_status ?? null, event.raw_type, event.raw_subtype ?? null, event.attrs);
182
+ insert.run(event.id, event.session_id, event.turn_id, event.seq, event.kind, event.ts, event.request_id ?? null, event.block_index, event.name ?? null, event.status ?? null, event.duration_ms ?? null, event.duration_source ?? null, event.input ?? null, event.input_bytes ?? null, event.input_storage ?? null, event.wire_input ?? null, event.wire_input_bytes ?? null, event.wire_input_storage ?? null, event.text ?? null, event.text_bytes ?? null, output.output_storage, output.spill_path, output.spill_bytes, event.src_offset, event.src_len, event.result_offset ?? null, event.result_len ?? null, event.result_block ?? null, event.image_count, event.model ?? null, event.tokens_in ?? null, event.tokens_out ?? null, event.tokens_cache_read ?? null, event.tokens_cache_write ?? null, est_cost, event.child_session_id ?? null, event.agent_type ?? null, event.agent_status ?? null, event.raw_type, event.raw_subtype ?? null, event.attrs);
183
183
  }
184
184
  }
185
185
  function spillColumns(event, spills) {
@@ -1,6 +1,6 @@
1
1
  import { asyncAgentLaunch, taskNotification } from '../transcript/agents.js';
2
2
  import { attachmentRecord } from '../transcript/attachments.js';
3
- import { contentBlocks } from '../transcript/blocks.js';
3
+ import { contentBlocks, imageCount, resultText } from '../transcript/blocks.js';
4
4
  import { isHumanPrompt, MACHINERY_TAGS } from '../transcript/human.js';
5
5
  import { foldControlLines, foldSessionEnvelope, promptGroupId, toolDenialKind, turnDurationMs, } from '../transcript/line.js';
6
6
  import { foldRequestGroup, groupByRequestId, modelOfRequestGroup, requestIdOf, } from '../transcript/usage.js';
@@ -36,9 +36,6 @@ function prose(blocks) {
36
36
  .join('\n')
37
37
  .trim();
38
38
  }
39
- function resultText(children) {
40
- return children.flatMap((child) => (child.kind === 'text' ? [child.text] : [])).join('\n');
41
- }
42
39
  function blockText(block) {
43
40
  switch (block?.kind) {
44
41
  case 'text':
@@ -146,6 +143,7 @@ export function runPipeline(lines, ctx) {
146
143
  result_offset: line.byte_offset,
147
144
  result_len: line.byte_length,
148
145
  result_block: blockIndex,
146
+ image_count: imageCount(block),
149
147
  });
150
148
  }
151
149
  const text = prose(blocks);
@@ -219,6 +217,7 @@ export function runPipeline(lines, ctx) {
219
217
  result_offset: undefined,
220
218
  result_len: undefined,
221
219
  result_block: undefined,
220
+ image_count: block === undefined ? 0 : imageCount(block),
222
221
  model,
223
222
  tokens_in: tokens?.input_tokens,
224
223
  tokens_out: tokens?.output_tokens,
@@ -98,6 +98,7 @@ export function joinToolCalls(events, results, notifications, drift) {
98
98
  event.result_offset = result.result_offset;
99
99
  event.result_len = result.result_len;
100
100
  event.result_block = result.result_block;
101
+ event.image_count = result.image_count;
101
102
  event.duration_ms = elapsedMs(event.ts, result.ts);
102
103
  event.duration_source = 'elapsed';
103
104
  event.status = statusOf(result);
@@ -49,6 +49,12 @@ export function parseSort(raw) {
49
49
  return sort === undefined ? { ok: false, error: 'invalid sort' } : { ok: true, value: sort };
50
50
  }
51
51
  const FIELDS = ['text', 'input'];
52
+ export function parseImageOrdinal(raw) {
53
+ const value = Number(raw);
54
+ return NON_NEGATIVE_INT.test(raw) && Number.isSafeInteger(value)
55
+ ? { ok: true, value }
56
+ : { ok: false, error: 'invalid image index' };
57
+ }
52
58
  export function parseField(raw) {
53
59
  if (raw === undefined || raw === '')
54
60
  return { ok: true, value: 'text' };
@@ -105,6 +111,11 @@ export function toDetailProjection(header, outcome) {
105
111
  }
106
112
  return PROJECTION_STATES.includes(stored.state) ? stored : { ...stored, state: 'none' };
107
113
  }
114
+ const IMAGE_FAILURES = {
115
+ gone: { status: 410, error: 'image no longer on disk' },
116
+ too_large: { status: 413, error: 'image too large to serve' },
117
+ unsupported: { status: 415, error: 'image type not servable' },
118
+ };
108
119
  function storedContent(row, field) {
109
120
  const isText = field === 'text';
110
121
  const resolved = {
@@ -200,6 +211,25 @@ export function registerApi(app, deps) {
200
211
  body.spill_path = resolved.spill_path;
201
212
  return c.json(body);
202
213
  });
214
+ app.get('/api/events/:id/images/:n', (c) => {
215
+ const ordinal = parseImageOrdinal(c.req.param('n'));
216
+ if (!ordinal.ok)
217
+ return c.json({ error: ordinal.error }, 400);
218
+ const row = readEventContentRow(db, c.req.param('id'));
219
+ if (row === undefined || ordinal.value >= row.image_count)
220
+ return jsonNotFound(c);
221
+ const image = deps.resolveImage?.(row, ordinal.value) ?? { ok: false, reason: 'gone' };
222
+ if (!image.ok) {
223
+ const failure = IMAGE_FAILURES[image.reason];
224
+ return c.json({ error: failure.error }, failure.status);
225
+ }
226
+ return c.body(new Uint8Array(image.bytes), 200, {
227
+ 'Content-Type': image.media_type,
228
+ 'Content-Length': String(image.bytes.length),
229
+ 'X-Content-Type-Options': 'nosniff',
230
+ 'Cache-Control': 'no-store',
231
+ });
232
+ });
203
233
  app.get('/api/search', (c) => {
204
234
  const parsed = parseSearchQuery(c.req.query());
205
235
  if (!parsed.ok)
@@ -5,7 +5,7 @@ import { mkdirSync } from 'node:fs';
5
5
  import { readOrCreateToken } from '../shared/index.js';
6
6
  import { resolveArchiveRoot, resolveTranscriptRoot } from '../archive/index.js';
7
7
  import { createArchiveReader } from '../archive/read.js';
8
- import { createContentResolver, createContentEnv } from '../content/resolve.js';
8
+ import { createContentEnv, createContentResolver, createImageResolver, } from '../content/resolve.js';
9
9
  import { createCorpusSweep } from '../corpus/watch.js';
10
10
  import { createProjectionEnv } from '../corpus/env.js';
11
11
  import { openDb } from '../db/open.js';
@@ -64,6 +64,8 @@ export async function startServer(options = {}) {
64
64
  });
65
65
  const hub = createStreamHub();
66
66
  const warm = createWarmQueue({ db, env: createProjectionEnv(reader, roots), hub });
67
+ const archivePathOf = (id) => readEventArchivePath(db, id)?.archive_path;
68
+ const contentEnv = createContentEnv(reader, [roots.archiveRoot, roots.transcriptRoot]);
67
69
  const app = buildApiApp({
68
70
  db,
69
71
  token,
@@ -73,7 +75,8 @@ export async function startServer(options = {}) {
73
75
  sweep,
74
76
  hub,
75
77
  warm,
76
- resolveContent: createContentResolver((id) => readEventArchivePath(db, id)?.archive_path, createContentEnv(reader, [roots.archiveRoot, roots.transcriptRoot])),
78
+ resolveContent: createContentResolver(archivePathOf, contentEnv),
79
+ resolveImage: createImageResolver(archivePathOf, contentEnv),
77
80
  });
78
81
  const fetch = app.fetch;
79
82
  const explicit = options.port !== undefined;
@@ -70,6 +70,40 @@ export function classifyBlock(raw, position) {
70
70
  export function classifyContent(content) {
71
71
  return normalizeContent(content).map((block) => classifyBlock(block, 'top'));
72
72
  }
73
+ export function resultText(children) {
74
+ const texts = children.flatMap((child) => (child.kind === 'text' ? [child.text] : []));
75
+ const images = children.flatMap((child) => (child.kind === 'image' ? [child.placeholder] : []));
76
+ return [...texts, ...images].join('\n');
77
+ }
73
78
  export function contentBlocks(line) {
74
- return classifyContent(obj(line.raw.message, undefined)?.content);
79
+ return classifyContent(rawContent(line));
80
+ }
81
+ function rawContent(line) {
82
+ return obj(line.raw.message, undefined)?.content;
83
+ }
84
+ export const SERVABLE_IMAGE_TYPES = new Set([
85
+ 'image/png',
86
+ 'image/jpeg',
87
+ 'image/gif',
88
+ 'image/webp',
89
+ ]);
90
+ export function imageCount(block) {
91
+ if (block.kind === 'image')
92
+ return 1;
93
+ if (block.kind !== 'tool_result')
94
+ return 0;
95
+ return block.children.filter((child) => child.kind === 'image').length;
96
+ }
97
+ export function imageSourceAt(line, blockIndex, ordinal) {
98
+ const block = obj(normalizeContent(rawContent(line))[blockIndex], EMPTY_RECORD);
99
+ const images = block.type === 'image'
100
+ ? [block]
101
+ : block.type === 'tool_result'
102
+ ? normalizeContent(block.content).filter((child) => obj(child, EMPTY_RECORD).type === 'image')
103
+ : [];
104
+ if (ordinal >= images.length)
105
+ return undefined;
106
+ const image = obj(images[ordinal], EMPTY_RECORD);
107
+ const source = obj(image.source, EMPTY_RECORD);
108
+ return { media_type: str(source.media_type, ''), data: str(source.data, '') };
75
109
  }
@@ -1 +1 @@
1
- export const PROJECTOR_VERSION = 13;
1
+ export const PROJECTOR_VERSION = 15;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faithfulalabi/agent-lens",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Local-first agentic tracing platform: inspect everything your agent did in a session.",
5
5
  "type": "module",
6
6
  "license": "MIT",