@threadcrumbs/common 0.1.2 → 0.1.4

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.
@@ -8,20 +8,25 @@ import {
8
8
  DETAIL_LIMITS,
9
9
  RECENT_MS,
10
10
  activityDetail,
11
+ askOf,
11
12
  clip,
12
13
  clipBlock,
13
14
  emptyActivity,
14
15
  endCall,
15
16
  keepNewest,
17
+ keepText,
16
18
  number,
19
+ openTool,
17
20
  planItems,
18
21
  planSummary,
19
22
  settleCalls,
20
23
  startCall,
21
24
  text,
25
+ textList,
22
26
  titleFromPrompt,
23
27
  toolFile,
24
28
  toolInput,
29
+ toolNow,
25
30
  toolTarget,
26
31
  } from '../lib/telemetry-fields.mjs'
27
32
 
@@ -183,8 +188,11 @@ function sessionsFrom(db, profile, lengths, home, at) {
183
188
  const open = turnOpen(tip, root)
184
189
  if (!open && at - lastAt(tip) > RECENT_MS) continue
185
190
  const ids = chain.map((r) => r.id)
186
- const errands = rows.filter((r) => isErrand(r) && ids.includes(r.parent_session_id) && turnOpen(r, chainOf(r).at(-1))).length
187
- out.push(toSession({ db, profile, tip, root, chain, ids, open, errands, lengths, home, messages, visible, at, lastAt: lastAt(tip) }))
191
+ const working = rows
192
+ .filter((r) => isErrand(r) && ids.includes(r.parent_session_id) && turnOpen(r, chainOf(r).at(-1)))
193
+ .sort((a, b) => seconds(a.started_at) - seconds(b.started_at))
194
+ .map((r) => ({ id: r.id, name: text(r.title) || 'Subagent' }))
195
+ out.push(toSession({ db, profile, tip, root, chain, ids, open, errands: working.length, workingSubagents: working, lengths, home, messages, visible, at, lastAt: lastAt(tip) }))
188
196
  }
189
197
  return out
190
198
  }
@@ -213,11 +221,15 @@ function detailFrom(db, rootId, at) {
213
221
 
214
222
  const activity = emptyActivity()
215
223
  const prompts = []
224
+ const thoughts = []
225
+ const replies = []
216
226
  const pick = (column, fallback = 'NULL') => (messages.has(column) ? column : `${fallback} AS ${column}`)
227
+ const said = (column) => (messages.has(column) ? `CASE WHEN role = 'assistant' THEN ${column} END AS ${column}` : `NULL AS ${column}`)
228
+ const reasons = messages.has('reasoning') || messages.has('reasoning_content')
217
229
  const order = messages.has('timestamp') ? 'timestamp, id' : 'id'
218
230
  const records = db
219
231
  .prepare(
220
- `SELECT id, role, CASE WHEN role = 'user' THEN content END AS content, ${pick('tool_calls')}, ${pick('tool_call_id')}, ${pick('timestamp', '0')}
232
+ `SELECT id, role, CASE WHEN role IN ('user', 'assistant') THEN content END AS content, ${said('reasoning')}, ${said('reasoning_content')}, ${pick('tool_calls')}, ${pick('tool_call_id')}, ${pick('timestamp', '0')}
221
233
  FROM messages WHERE session_id IN (${placeholders}) ${visible} ORDER BY ${order}`,
222
234
  )
223
235
  .all(...ids)
@@ -227,10 +239,12 @@ function detailFrom(db, rootId, at) {
227
239
  settleCalls(activity)
228
240
  const prompt = text(record.content).trim()
229
241
  if (prompt) keepNewest(prompts, { at: time, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
230
- } else if (record.role === 'assistant' && record.tool_calls) {
231
- for (const call of parseJson(record.tool_calls, [])) {
242
+ } else if (record.role === 'assistant') {
243
+ keepText(thoughts, 'thoughts', { at: time, text: text(record.reasoning) || text(record.reasoning_content) })
244
+ keepText(replies, 'replies', { at: time, text: text(record.content) })
245
+ for (const call of (record.tool_calls && parseJson(record.tool_calls, [])) || []) {
232
246
  if (!call?.id) continue
233
- const args = typeof call.function?.arguments === 'string' ? parseJson(call.function.arguments, null) : call.function?.arguments
247
+ const args = callArgs(call)
234
248
  startCall(activity, { id: text(call.id), name: text(call.function?.name), target: toolTarget(args, cwd), input: toolInput(args), ...toolFile(args, cwd), startedAt: time })
235
249
  }
236
250
  } else if (record.role === 'tool' && record.tool_call_id) {
@@ -252,7 +266,7 @@ function detailFrom(db, rootId, at) {
252
266
  status: working ? 'working' : FAILED_END.test(text(r.end_reason)) ? 'error' : r.ended_at != null ? 'done' : 'idle',
253
267
  model: text(r.model),
254
268
  tokens: tokens.some((t) => t != null) ? { input: tokens[0] || 0, output: tokens[1] || 0, cacheRead: tokens[2] || 0, cacheWrite: tokens[3] || 0 } : null,
255
- tool: working && label ? { name: label[1].trim(), startedAt: seconds(r.last_activity_at) } : null,
269
+ tool: working && label ? toolNow({ name: label[1].trim(), startedAt: seconds(r.last_activity_at) }) : null,
256
270
  startedAt: seconds(r.started_at),
257
271
  lastActivityAt: lastAt(r),
258
272
  }
@@ -289,11 +303,14 @@ function detailFrom(db, rootId, at) {
289
303
  subagents,
290
304
  files: null,
291
305
  prompts,
306
+ thoughts: reasons ? textList(thoughts) : null,
307
+ replies: textList(replies),
308
+ contextBreakdown: null,
292
309
  errors,
293
310
  }
294
311
  }
295
312
 
296
- function openCalls(db, sessionId, messages, visible) {
313
+ function openCalls(db, sessionId, messages, visible, cwd) {
297
314
  if (!['tool_calls', 'tool_call_id', 'timestamp'].every((c) => messages.has(c))) return []
298
315
  const last = db
299
316
  .prepare(`SELECT id, tool_calls, timestamp FROM messages WHERE session_id = ? AND role = 'assistant' ${visible} ORDER BY id DESC LIMIT 1`)
@@ -310,7 +327,17 @@ function openCalls(db, sessionId, messages, visible) {
310
327
  )
311
328
  return (Array.isArray(calls) ? calls : [])
312
329
  .filter((c) => c?.id && !answered.has(c.id))
313
- .map((c) => ({ name: text(c.function?.name), startedAt: seconds(last.timestamp) }))
330
+ .map((c) => openTool(text(c.function?.name), seconds(last.timestamp), callArgs(c), cwd))
331
+ }
332
+
333
+ const callArgs = (call) => (typeof call.function?.arguments === 'string' ? parseJson(call.function.arguments, null) : call.function?.arguments)
334
+
335
+ function turnStart(db, ids, messages, visible) {
336
+ if (!messages.has('timestamp')) return null
337
+ const row = db
338
+ .prepare(`SELECT MAX(timestamp) AS at FROM messages WHERE session_id IN (${ids.map(() => '?').join(', ')}) AND role = 'user' ${visible}`)
339
+ .get(...ids)
340
+ return row?.at == null ? null : seconds(row.at)
314
341
  }
315
342
 
316
343
  function latestPlan(db, ids, messages) {
@@ -339,25 +366,28 @@ function sum(chain, column) {
339
366
  return known ? total : null
340
367
  }
341
368
 
342
- function toSession({ db, profile, tip, root, chain, ids, open, errands, lengths, home, messages, visible, at, lastAt }) {
369
+ function toSession({ db, profile, tip, root, chain, ids, open, errands, workingSubagents, lengths, home, messages, visible, at, lastAt }) {
343
370
  const description = text(tip.last_activity_description)
344
371
  const placeholders = ids.map(() => '?').join(', ')
345
- const calls = open ? openCalls(db, tip.id, messages, visible) : []
372
+ const calls = open ? openCalls(db, tip.id, messages, visible, text(tip.cwd)) : []
346
373
  const working = calls.filter((c) => !DELEGATE_TOOLS.has(c.name))
374
+ const asking = ASKING.exec(description)
347
375
  let status
376
+ let statusReason
348
377
  if (open) {
349
- if (ASKING.test(description)) status = 'asking'
350
- else status = !working.length && (calls.length || errands) ? 'delegating' : 'working'
351
- } else if (errands) status = 'delegating'
352
- else if (tip.last_read_at != null) status = seconds(tip.last_activity_at || tip.started_at) > seconds(tip.last_read_at) ? 'done' : 'idle'
353
- else status = tip.ended_at == null && at - lastAt < ACTIVE_WINDOW_MS ? 'done' : 'idle'
378
+ if (asking) [status, statusReason] = ['asking', /approval/i.test(asking[1]) ? 'approval' : 'question']
379
+ else if (!working.length && (calls.length || errands)) [status, statusReason] = ['delegating', 'subagents']
380
+ else [status, statusReason] = ['working', 'turn-open']
381
+ } else if (errands) [status, statusReason] = ['delegating', 'subagents']
382
+ else if (tip.last_read_at != null) [status, statusReason] = seconds(tip.last_activity_at || tip.started_at) > seconds(tip.last_read_at) ? ['done', 'unread'] : ['idle', 'read']
383
+ else [status, statusReason] = tip.ended_at == null && at - lastAt < ACTIVE_WINDOW_MS ? ['done', 'recent'] : ['idle', 'quiet']
354
384
 
355
385
  let tool = null
356
386
  if (status === 'working' || status === 'delegating') {
357
387
  const latest = (working.length ? working : calls).at(-1)
358
388
  const label = TOOL_LABEL.exec(description)
359
- if (latest) tool = latest
360
- else if (label) tool = { name: label[1].trim(), startedAt: seconds(tip.last_activity_at) }
389
+ if (latest) tool = toolNow(latest)
390
+ else if (label) tool = toolNow({ name: label[1].trim(), startedAt: seconds(tip.last_activity_at) })
361
391
  }
362
392
 
363
393
  const where = text(tip.git_repo_root) || text(tip.cwd)
@@ -393,11 +423,17 @@ function toSession({ db, profile, tip, root, chain, ids, open, errands, lengths,
393
423
  ? { input: tokens[0] || 0, output: tokens[1] || 0, cacheRead: tokens[2] || 0, cacheWrite: tokens[3] || 0 }
394
424
  : null,
395
425
  context: used ? { used, limit: contextLimit(lengths, text(tip.model), tip.billing_base_url) } : null,
426
+ contextHistory: null,
396
427
  costUsd: costs.some((c) => c != null) ? costs.reduce((total, c) => total + number(c), 0) : null,
397
428
  lines: null,
398
429
  plan: planSummary(latestPlan(db, ids, messages)),
399
430
  subagents: errands,
431
+ workingSubagents,
400
432
  prompts: number(prompts?.n),
433
+ turnStartedAt: open ? turnStart(db, ids, messages, visible) : null,
434
+ statusReason,
435
+ ask: askOf(status),
436
+ usageLimits: null,
401
437
  }
402
438
  }
403
439