Zion Boggan
repos/TreeTrace/src/adapters/shared.js
zionboggan.com ↗
153 lines · javascript
History for this file →
1
import { truncate } from '../util.js';
2
import { looksLikeRefusal } from '../parse.js';
3
 
4
export function emptyStats() {
5
  return {
6
    userLines: 0,
7
    assistantLines: 0,
8
    toolUses: 0,
9
    models: new Set(),
10
    filesTouched: new Set(),
11
    inputTokens: 0,
12
    outputTokens: 0,
13
    interruptions: 0,
14
    rejections: 0,
15
    rejectionsByKind: Object.create(null),
16
  };
17
}
18
 
19
export function newSession(path, sessionId) {
20
  return {
21
    sessionId: sessionId || null,
22
    path,
23
    title: null,
24
    customTitle: null,
25
    version: null,
26
    cwd: null,
27
    gitBranch: null,
28
    firstTs: null,
29
    lastTs: null,
30
    prompts: [],
31
    index: new Map(),
32
    leafUuid: null,
33
    activeLeafUuid: null,
34
    stats: emptyStats(),
35
    isContinuation: false,
36
  };
37
}
38
 
39
export function finalizeSession(session) {
40
  session.stats.models = [...session.stats.models];
41
  session.stats.filesTouched = [...session.stats.filesTouched];
42
  session.stats.rejectionsByKind = { ...session.stats.rejectionsByKind };
43
  if (session.customTitle) session.title = session.customTitle;
44
  return session;
45
}
46
 
47
export function noteTimestamp(session, ts) {
48
  if (!ts) return;
49
  if (!session.firstTs) session.firstTs = ts;
50
  session.lastTs = ts;
51
}
52
 
53
export function pushTurn(session, idx, text, ts, { hasImage = false, hadToolResultContext = false } = {}) {
54
  const trimmed = (text || '').trim();
55
  if (!trimmed && !hasImage) return null;
56
  const uuid = `${session.sessionId || 'turn'}-u${idx}`;
57
  const parentUuid = session._lastUserUuid || null;
58
  session.index.set(uuid, { parentUuid, type: 'user', ts: ts || null });
59
  session.leafUuid = uuid;
60
  session._lastUserUuid = uuid;
61
  session.stats.userLines++;
62
  const prompt = {
63
    uuid,
64
    parentUuid,
65
    ts: ts || null,
66
    text: trimmed || '[image-only prompt: screenshot/annotated feedback]',
67
    hasImage,
68
    hadToolResultContext,
69
    afterInterruption: false,
70
    actions: [],
71
    thinking: 0,
72
    rejections: [],
73
  };
74
  session.prompts.push(prompt);
75
  session._currentPrompt = prompt;
76
  noteTimestamp(session, ts);
77
  return uuid;
78
}
79
 
80
export function addAction(session, action) {
81
  if (session._currentPrompt && action) session._currentPrompt.actions.push(action);
82
}
83
 
84
export function addThinking(session, n = 1) {
85
  if (session._currentPrompt) session._currentPrompt.thinking += n;
86
}
87
 
88
export function addRejection(session, rejection) {
89
  if (!session._currentPrompt || !rejection || typeof rejection.kind !== 'string') return;
90
  if (!Array.isArray(session._currentPrompt.rejections)) session._currentPrompt.rejections = [];
91
  session._currentPrompt.rejections.push(rejection);
92
  session.stats.rejections = (session.stats.rejections || 0) + 1;
93
  session.stats.rejectionsByKind = session.stats.rejectionsByKind || Object.create(null);
94
  session.stats.rejectionsByKind[rejection.kind] = (session.stats.rejectionsByKind[rejection.kind] || 0) + 1;
95
}
96
 
97
export function noteAssistantRefusal(session, text) {
98
  if (!session || !session._currentPrompt) return;
99
  if (!looksLikeRefusal(text)) return;
100
  addRejection(session, {
101
    kind: 'model_refusal',
102
    source: 'text_heuristic',
103
    confidence: 0.7,
104
    toolUseId: null,
105
    tool: null,
106
    ts: null,
107
    evidence: truncate(typeof text === 'string' ? text : '', 160),
108
  });
109
}
110
 
111
export function flattenParts(parts) {
112
  if (typeof parts === 'string') return parts;
113
  if (!Array.isArray(parts)) {
114
    if (parts && typeof parts === 'object' && typeof parts.text === 'string') return parts.text;
115
    return '';
116
  }
117
  const out = [];
118
  for (const part of parts) {
119
    if (typeof part === 'string') out.push(part);
120
    else if (part && typeof part === 'object' && typeof part.text === 'string') out.push(part.text);
121
  }
122
  return out.join('\n');
123
}
124
 
125
export function looksSynthetic(text) {
126
  const t = (text || '').trimStart();
127
  if (!t) return true;
128
  return (
129
    t.startsWith('<environment_context>') ||
130
    t.startsWith('<permissions instructions>') ||
131
    t.startsWith('<collaboration_mode>') ||
132
    t.startsWith('<user_instructions>') ||
133
    t.startsWith('<system-reminder>')
134
  );
135
}
136
 
137
export function readJson(text) {
138
  return JSON.parse(text);
139
}
140
 
141
export function readJsonl(text) {
142
  const records = [];
143
  for (const line of text.split(/\r?\n/)) {
144
    const trimmed = line.trim();
145
    if (!trimmed || trimmed.charCodeAt(0) !== 123) continue;
146
    try {
147
      records.push(JSON.parse(trimmed));
148
    } catch {
149
      continue;
150
    }
151
  }
152
  return records;
153
}