Skip to content

Commit a2cd058

Browse files
V48 Gate 3 (specification-implementation): lock rich telemetry to LLM calls + Tool uses; consolidate hierarchy pills (F19)
After F18 the raw-content rows were gone, but the synthesis telemetry still rendered intermediate store values as standalone rows (`try`, `setup-plan`, `thricified-generation`, a cwd path, a bare agent name) and split the hierarchy across rows instead of consolidating it onto one rich log line. Formal log-line contract (now enforced): the rich telemetry renders EXACTLY two formal kinds, plus a few terminal/high-level signals — nothing else. - LLM call: the inference leaf (llm/output, stream type `generation`) carrying the full hierarchy Phase->Agent->Step->Failsafe->Thricified + source-safe content + provider/model/usage, each level its own pill. - Tool use: tool|tools:result|error carrying Phase->Agent->Step + tool name/arguments (no Failsafe/Thricified). Every other store event (step/agent/phase name stores, prompt-side llm keys, the llm/response registry copy, cwd paths, generation markers, tool sub-keys) is intermediate CONTEXT: it advances a rolling hierarchy but never becomes a row. Implementation: - terminal-run-activity.ts: buildTerminalRunActivityFromEvents maintains a rolling {phase, agent, step, failsafe, generation} context updated from every event, classifies each event, and emits a row ONLY for LLM calls / Tool uses / terminal signals. LLM rows stamp their own 5-field hierarchy; tool rows stamp Phase/Agent/Step from context + the tool name/args accumulated per tool-execution node (tool stores carry no hierarchy and use the singular `tool` namespace). Distinct calls that share withheld text stay distinct via a unique null-separated row key (TELEMETRY_ROW_KEY_SEP). - pipeline-execution-log.tsx: derive displayText from the row key (text before the null separator), look up details by the full key, run text heuristics on displayText, and bypass message de-dup for uniquely-keyed rows so distinct LLM/tool calls never collapse. Spec (BITCODE_SPEC_V48_NOTES.md) records the formal log-line contract; QA ledger records F19. uapi tsc 0; new F19 contract test (fragments suppressed; LLM row = 5-pill hierarchy; tool row = Phase/Agent/Step + tool) + 10-suite telemetry/terminal/deposit regression batch (38 tests) green; spec checker green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a30c388 commit a2cd058

5 files changed

Lines changed: 291 additions & 19 deletions

File tree

BITCODE_SPEC_V48_NOTES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ Accepted V48 architecture law (decided 2026-06-25):
201201
row, never fragmented by embedded newlines) and the log row title spans carry
202202
`min-w-0` so a long line truncates within its row rather than x-overflowing the
203203
page (QA F18).
204+
- **Formal telemetry log-line contract** (QA F19): the rich telemetry renders
205+
EXACTLY two formal log-line kinds, plus a few terminal/high-level signals —
206+
nothing else. (1) **LLM calls** — the inference leaf, canonically the Thricified
207+
substep output (`llm/output`, stream type `generation`), carrying the full
208+
hierarchy Phase→Agent→Step→Failsafe→Thricified + source-safe content +
209+
provider/model/usage, each level its own pill. (2) **Tool uses**
210+
`tool|tools:result|error`, carrying Phase→Agent→Step + tool name/arguments (no
211+
Failsafe/Thricified). Every other store event (step/agent/phase name stores,
212+
prompt-side llm keys, the `llm/response` registry copy, cwd paths, generation
213+
markers, tool sub-keys) is intermediate CONTEXT: the activity builder folds it
214+
into a rolling `{phase, agent, step, failsafe, generation}` context (so the next
215+
LLM/tool row is stamped with its full hierarchy) but never emits it as a row.
216+
Distinct calls that share withheld text stay distinct rows via a unique
217+
null-separated row key; the renderer displays the text before the separator,
218+
keys details by the full key, and bypasses de-dup for uniquely-keyed rows. This
219+
is what removes the `try`/`setup-plan`/`thricified-generation`/path fragments
220+
and stabilizes the pipeline↔UI payload contract.
204221
- **OTF (on-the-fly instructions) is eradicated entirely** as legacy residue:
205222
the dead telemetry types (`otf_instructions`/`otf_adherence`), the unused
206223
`setOnTheFly` prompt primitive, the live instruction-injection feature (the

BITCODE_V48_QA.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ Track 3-4 scripts (BTD ledger, settlement, pack journaling) get added when those
190190
- `pipeline-execution-log.tsx` + `DepositPageClient.tsx``min-w-0` on the compact/desktop title spans and on the telemetry panel section/wrapper (+ `overflow-hidden` on the panel) so a long line truncates within its row instead of x-overflowing the page.
191191
- Related observation (not yet filed): the deposit run executes `ReadFitsFindingSynthesisSetupPlanAgent` planning a "Read-Need" — a read-lens setup agent running under the deposit lens. Telemetry is now source-safe regardless, but the deposit setup-plan agent identity should be confirmed/queued as a separate correctness finding.
192192

193+
### F19 — Rich telemetry fragments the hierarchy across rows; lock it to LLM calls + Tool uses only
194+
195+
- Severity: medium (telemetry correctness + pipeline↔UI contract stability).
196+
- Observed (2026-06-26): after F18 the raw-content rows were gone, but the Synthesis run telemetry still rendered intermediate store values as standalone rows — `try`, `setup-plan`, `thricified-generation`, a cwd path, a bare agent name — and the hierarchy was split across rows (Phase on one, Step+Generation on another, Agent on a third) instead of consolidated onto one rich log line.
197+
- Contract (decided 2026-06-26): the rich telemetry renders EXACTLY two formal log-line kinds, nothing else — (a) **LLM calls**, which carry the full hierarchy Phase→Agent→Step→Failsafe→Thricified + source-safe content + provider/model/usage; (b) **Tool uses**, which carry Phase→Agent→Step + tool name/arguments (no Failsafe/Thricified). Every other store event (step/agent/phase name stores, prompt-side llm keys, `llm/response` registry copies, paths, generation markers, tool sub-keys) is intermediate context that advances the rolling hierarchy but never becomes a row.
198+
- Cause: `ExecutionStreamAdapter.onStore` emits one event per `execution.store(...)`, and `buildTerminalRunActivityFromEvents` turned any event with a non-empty message into a row. The canonical LLM-call event (`llm/output`, type `generation`) already carries the full hierarchy in its value, but tool stores use the singular `tool` namespace with no hierarchy (and `inferEventType` only recognized plural `tools`), so tool calls were mis-typed and hierarchy-less.
199+
- Repair (2026-06-26, `terminal-run-activity.ts` + `pipeline-execution-log.tsx`):
200+
- The activity builder maintains a rolling `{phase, agent, step, failsafe, generation}` context updated from every event, and emits a row ONLY for LLM calls (`generation` / `llm:output`), Tool uses (`tool|tools:result|error`), and terminal/high-level signals (`completion`/`error`/no-namespace status). LLM rows stamp their own 5-field hierarchy; tool rows stamp Phase/Agent/Step from the rolling context + the tool name/args accumulated per tool-execution node.
201+
- Distinct calls that share withheld text are kept distinct via a unique null-separated row key (`TELEMETRY_ROW_KEY_SEP`); the renderer displays only the text before the separator, looks up details by the full key, and bypasses message de-dup for uniquely-keyed rows.
202+
- Verified: uapi tsc 0; F19 contract test (fragments suppressed, LLM row carries 5-pill hierarchy, tool row carries Phase/Agent/Step + tool) + 10-suite telemetry/terminal/deposit regression batch (38 tests) green.
203+
193204
## Track 1 — Identity / Authentication / Auxillaries — COMPLETE 2026-06-12 (email deferred by decision; F2/F9 and legacy eradication queued for gates)
194205

195206
- [x] Sign up / sign in via Connect Wallet (nav CTA → SignUpWindow → wallet signature on testnet4 → `custom:bitcode-bitcoin` session → `/tps/supabase/callback`) — verified 2026-06-12 after F5 fix; lands on `/packs`. Re-verified from fully nuked state (purged user + cleared site data): created 19:29:21 → session 19:29:25 → binding auto-written 19:29:29 by the bridge on `/packs` mount with no Auxillaries visit; UI consistent across nav, Wallet, and Profile panes.

uapi/app/terminal/terminal-run-activity.ts

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,107 @@ function normalizeEventMessage(payload: any) {
8888
return null;
8989
}
9090

91+
// ---------------------------------------------------------------------------
92+
// Formal telemetry log-line contract (V48, QA F19)
93+
//
94+
// The rich telemetry renders EXACTLY two formal log-line kinds, plus a few
95+
// terminal/high-level signals — nothing else:
96+
// • LLM call — the inference leaf. Canonically the Thricified substep output
97+
// (`llm/output`, stream type `generation`), whose value carries the full
98+
// hierarchy {phase, agent, step, failsafe, generation}. Rendered with all
99+
// five pills + source-safe content + provider/model/usage metadata.
100+
// • Tool use — a tool invocation inside a step (`tool/result` on success,
101+
// `tool/error` on failure). Carries Phase/Agent/Step (from the rolling
102+
// context — tool stores don't embed the hierarchy) + tool name/arguments.
103+
// Every other store event (step/agent/phase name stores, prompt-side llm keys,
104+
// `llm/response` registry copies, cwd paths, generation markers, tool sub-keys)
105+
// is intermediate CONTEXT: it updates the rolling hierarchy but never becomes a
106+
// row. This is what stops `try` / `setup-plan` / `thricified-generation` / path
107+
// fragments from fragmenting the log and stabilizes the pipeline↔UI contract.
108+
// ---------------------------------------------------------------------------
109+
110+
// A null byte separates a row's display text from a unique row key in the
111+
// `output` string. The renderer splits each line on it: text before, key after.
112+
// Distinct LLM/tool calls that share withheld text (e.g. two `[content withheld]`
113+
// reason calls in one step) therefore stay distinct rows instead of collapsing
114+
// under the renderer's text-keyed de-dup.
115+
export const TELEMETRY_ROW_KEY_SEP = '\u0000';
116+
117+
interface ExecContext {
118+
phase?: string | null;
119+
agent?: string | null;
120+
step?: string | null;
121+
failsafe?: string | null;
122+
generation?: string | null;
123+
}
124+
125+
function readEventExecutionState(payload: any): ExecContext {
126+
const es =
127+
(payload?.status?.executionState && typeof payload.status.executionState === 'object'
128+
? payload.status.executionState
129+
: null) ||
130+
(payload?.executionState && typeof payload.executionState === 'object' ? payload.executionState : null) ||
131+
{};
132+
const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
133+
return {
134+
phase: es.phase ?? data.phase ?? null,
135+
agent: es.agent ?? data.agent ?? null,
136+
step: es.step ?? data.step ?? null,
137+
failsafe: es.failsafe ?? data.failsafe ?? null,
138+
generation: es.generation ?? data.generation ?? null,
139+
};
140+
}
141+
142+
// Merge a freshly observed context into the rolling one (non-null wins). Name
143+
// stores (`phase/current`, `agent/name`, `step/name`) and the live-stream
144+
// `phase`/`agent` transition events carry the hierarchy as a bare value rather
145+
// than in executionState, so fold those in explicitly.
146+
function updateRollingContext(ctx: ExecContext, payload: any): void {
147+
const es = readEventExecutionState(payload);
148+
if (es.phase) ctx.phase = es.phase;
149+
if (es.agent) ctx.agent = es.agent;
150+
if (es.step) ctx.step = es.step;
151+
if (es.failsafe) ctx.failsafe = es.failsafe;
152+
if (es.generation) ctx.generation = es.generation;
153+
154+
const ns = String(payload?.namespace || '');
155+
const key = String(payload?.key || '');
156+
const value = payload?.data;
157+
if (typeof value === 'string') {
158+
if (ns === 'phase' && (key === 'current' || key === 'name')) ctx.phase = value;
159+
if (ns === 'agent' && key === 'name') ctx.agent = value;
160+
if (ns === 'step' && key === 'name') ctx.step = value;
161+
}
162+
if (payload?.type === 'phase' && payload?.phase) ctx.phase = String(payload.phase);
163+
if (payload?.type === 'agent' && payload?.agent) ctx.agent = String(payload.agent);
164+
}
165+
166+
type FormalLogLineKind = 'llm' | 'tool' | 'signal' | 'status';
167+
168+
function classifyFormalLogLine(payload: any): FormalLogLineKind | null {
169+
const type = String(payload?.type || '');
170+
const ns = String(payload?.namespace || '');
171+
const key = String(payload?.key || '');
172+
173+
// Formal LLM call: the Thricified substep output is canonical (full hierarchy).
174+
if (type === 'generation') return 'llm';
175+
if (ns === 'llm' && key === 'output') return 'llm';
176+
177+
// Formal tool use: one row per completed tool call (result | error).
178+
if (type === 'tool-use') return 'tool';
179+
if ((ns === 'tool' || ns === 'tools') && (key === 'result' || key === 'error')) return 'tool';
180+
181+
// Terminal signals — not fragments; keep them as informational rows.
182+
if (type === 'completion' || type === 'error') return 'signal';
183+
184+
// High-level status emitted via emitEvent (no store namespace), e.g. "Pipeline
185+
// execution started". onStore fragment status events always carry a namespace
186+
// and are suppressed (context only).
187+
if (type === 'status' && !ns) return 'status';
188+
189+
return null;
190+
}
191+
91192
export function buildTerminalRunActivityFromEvents(
92193
events: ExecutionEvent[],
93194
latestWorkUpdate: any | null,
@@ -110,6 +211,33 @@ export function buildTerminalRunActivityFromEvents(
110211
}
111212
}
112213

214+
// Rolling hierarchy + per-tool-node accumulators drive the formal log-line
215+
// contract: only LLM calls and Tool uses (plus terminal/high-level signals)
216+
// become rows; every other event just advances the rolling context.
217+
const rollingContext: ExecContext = {};
218+
const toolByNode = new Map<string, { name?: string; input?: unknown }>();
219+
let rowSeq = 0;
220+
221+
const pushRow = (displayText: string, enriched: any) => {
222+
const text = toSafeSingleLine(displayText);
223+
if (!text) return;
224+
// Unique key keeps distinct LLM/tool calls from collapsing under the
225+
// renderer's text-keyed de-dup; the renderer strips everything from the
226+
// separator on for display.
227+
const rowKey = `${text}${TELEMETRY_ROW_KEY_SEP}${rowSeq++}`;
228+
outputLines.push(rowKey);
229+
outputDetails[rowKey] = enriched;
230+
};
231+
232+
const stampExecutionState = (state: ExecContext, payload: any, extra?: Record<string, unknown>) => ({
233+
...payload,
234+
executionState: { ...state, ...(extra || {}) },
235+
status: {
236+
...(payload?.status && typeof payload.status === 'object' ? payload.status : {}),
237+
executionState: { ...state, ...(extra || {}) },
238+
},
239+
});
240+
113241
for (const entry of events) {
114242
const payload = entry.event || {};
115243
if (payload?.type === 'work-update' && payload.update) {
@@ -119,12 +247,71 @@ export function buildTerminalRunActivityFromEvents(
119247
continue;
120248
}
121249

250+
// Every event advances the rolling hierarchy before we decide whether it is
251+
// itself a formal log line.
252+
updateRollingContext(rollingContext, payload);
253+
254+
// Accumulate tool name/arguments per tool-execution node so the eventual
255+
// result/error row can render the complete tool-use line.
256+
const ns = String(payload?.namespace || '');
257+
const key = String(payload?.key || '');
258+
const nodeId = String(payload?.executionNodeId || '');
259+
if ((ns === 'tool' || ns === 'tools') && nodeId) {
260+
if (key === 'name' && typeof payload?.data === 'string') {
261+
const acc = toolByNode.get(nodeId) || {};
262+
acc.name = payload.data;
263+
toolByNode.set(nodeId, acc);
264+
} else if (key === 'input' || key === 'arguments') {
265+
const acc = toolByNode.get(nodeId) || {};
266+
acc.input = payload?.data ?? null;
267+
toolByNode.set(nodeId, acc);
268+
}
269+
}
270+
271+
const kind = classifyFormalLogLine(payload);
272+
if (!kind) continue;
273+
274+
if (kind === 'llm') {
275+
// The LLM call carries the full hierarchy itself; fall back to the rolling
276+
// context only for any field the event omits.
277+
const own = readEventExecutionState(payload);
278+
const merged: ExecContext = {
279+
phase: own.phase ?? rollingContext.phase ?? null,
280+
agent: own.agent ?? rollingContext.agent ?? null,
281+
step: own.step ?? rollingContext.step ?? null,
282+
failsafe: own.failsafe ?? null,
283+
generation: own.generation ?? null,
284+
};
285+
const text = String(payload?.message || payload?.status?.message || '[content withheld — source-safe]');
286+
pushRow(text, stampExecutionState(merged, { ...payload, type: 'generation' }));
287+
continue;
288+
}
289+
290+
if (kind === 'tool') {
291+
const acc = toolByNode.get(nodeId) || {};
292+
const toolName =
293+
acc.name || payload?.data?.tool || payload?.metadata?.toolName || (key === 'error' ? 'tool (failed)' : 'tool');
294+
// Tool uses have Phase/Agent/Step but no Failsafe/Thricified.
295+
const merged: ExecContext = {
296+
phase: rollingContext.phase ?? null,
297+
agent: rollingContext.agent ?? null,
298+
step: rollingContext.step ?? null,
299+
};
300+
const enriched = stampExecutionState(merged, { ...payload, type: 'tool-use' }, { tool: toolName });
301+
enriched.metadata = {
302+
...(payload?.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}),
303+
toolName,
304+
toolInput: acc.input ?? null,
305+
};
306+
pushRow(toolName, enriched);
307+
if (nodeId) toolByNode.delete(nodeId);
308+
continue;
309+
}
310+
311+
// Terminal/high-level signal — keep as an informational row.
122312
const normalized = normalizeEventMessage(payload);
123313
if (!normalized) continue;
124-
const safeText = toSafeSingleLine(normalized.text);
125-
if (!safeText) continue;
126-
outputLines.push(safeText);
127-
outputDetails[safeText] = payload;
314+
pushRow(normalized.text, payload);
128315
}
129316

130317
const latestStatusEvent = statusEvents[statusEvents.length - 1];

0 commit comments

Comments
 (0)