/**
* The step scrubber: a run read one step at a time. Selecting a step shows the
* call that was made, the whole of what went into it and came back as
* formatted JSON, and the handles in scope by that point — the same reading
* for a run that finished an hour ago and one that is still going.
*/
import { html, LitElement, nothing, type TemplateResult } from "lit";
import { isObjectOrArray } from "@commonfabric/utils/types";
import {
type ConsoleArgumentRef,
type ConsoleHandle,
type ConsoleStep,
consoleStepArguments,
} from "../steps.ts";
const json = (value: unknown): string => {
try {
return JSON.stringify(value, null, 2);
} catch {
// A payload holding a cycle is not one the run produced, but showing
// something beats an empty pane.
return String(value);
}
};
/** The one way a cell is drawn, wherever this view draws one. */
import "./cell-chip.ts";
/**
* How long a run of numbers has to be before the page calls it out. Numbers
* are never sealed, so a long array of them is a channel wide enough to carry
* arbitrary content — well past what an inert scalar result looks like.
*/
const WIDE_NUMERIC_RUN = 32;
/**
* The short names of a label's clauses. An atom's `type` is a CFC atom URL
* whose last segment identifies it — `ExternalIngest` for the atom marking
* a value that arrived from outside the fabric. A clause is
* arbitrary CFC JSON, so one that is not a typed atom is reported by its shape
* rather than dropped: a clause the page cannot name is still one the label
* carries.
*/
const atomNames = (clauses: readonly unknown[] = []): string[] =>
clauses.map((clause) => {
const type = isObjectOrArray(clause)
? (clause as { type?: unknown }).type
: undefined;
return typeof type === "string"
? type.split("/").pop() ?? type
: JSON.stringify(clause) ?? "clause";
});
/**
* Where the scrubber sits when `count` steps are what it is scrubbing. A run
* shorter than the one that was open cannot hold the step it was scrubbed to,
* so the selection follows the steps down rather than pointing past their end,
* where the rail highlights nothing and an arrow key moves from a step that is
* not there.
*/
export const clampSelection = (selected: number, count: number): number =>
Math.min(Math.max(selected, 0), Math.max(count - 1, 0));
/** Elides a value's rendering so one long literal cannot fill the pane. */
const truncate = (text: string, limit: number): string =>
text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
/** A step's one-line label in the rail. */
const stepLabel = (step: ConsoleStep): string =>
step.kind === "tool" ? step.toolName ?? "tool" : step.kind;
const omissionLabels = {
"artifact-only": "kept on the artifact, not sent to the model",
"observation-denied": "withheld by policy",
"bare-fabric-identifier-scrub": "bare-fabric-identifier-scrub",
"model-context-truncation": "model-context-truncation",
"superseded-run-pattern-diagnostic-collapse":
"superseded-run-pattern-diagnostic-collapse",
};
/** The expandable omission block's label for one tool result. */
export const withheldSummary = (step: ConsoleStep): string =>
step.withheld.status === "unrecorded"
? "model omissions · no record"
: step.withheld.status === "record-unreadable"
? "model omissions · record unreadable"
: step.withheld.status === "record-entry-missing"
? "model omissions · entry missing"
: `${
[
...new Set(
step.withheld.locations.map((location) =>
omissionLabels[location.rule]
),
),
].join("; ") || "model omissions"
} · ${step.withheld.locations.length}`;
/**
* What the full tool artifact records beside the model-facing result, under
* `summary`, which says by default what omission rules applied.
*/
export const withheldView = (
step: ConsoleStep,
summary = withheldSummary(step),
): TemplateResult => {
if (step.withheld.status === "unrecorded") {
return html`
${summary}
No omission record exists for this tool result. Legacy runs cannot be
reconstructed honestly from the model-facing transcript alone.
`;
}
if (step.withheld.status === "record-unreadable") {
return html`
${summary}
The omission record exists but is unreadable or does not match the
current contract. This result cannot be reconstructed honestly.
`;
}
if (step.withheld.status === "record-entry-missing") {
return html`
${summary}
The omission record exists but has no entry for this tool result.
Which omission rules applied cannot be determined.
`;
}
if (step.withheld.locations.length === 0) {
return html`
${summary}
`
)}
`;
};
/**
* The labels CFC computed for one call's inputs. Taint and the prompt slot's
* influence are separate views of the same inputs; both are listed, one row
* per entry, each on its own axis.
*/
const stepLabelEntries = (step: ConsoleStep) => [
...step.invocation?.cfcInputLabels?.entries ?? [],
...step.invocation?.promptSlotInfluenceLabels?.entries ?? [],
];
/**
* Whether CFC recorded anything about one call: a decision, an event, or
* labels on its inputs.
*/
export const stepCfcRecorded = (step: ConsoleStep): boolean =>
step.policy !== undefined || step.policyEvents.length > 0 ||
stepLabelEntries(step).length > 0;
/** What CFC decided about one call, and any event it raised. */
export const stepPolicyView = (
step: ConsoleStep,
): TemplateResult | typeof nothing => {
const labelEntries = stepLabelEntries(step);
if (!stepCfcRecorded(step)) {
return nothing;
}
return html`
${step.handlesIntroduced.includes(handle.token)
? "new here"
: `step ${handle.introducedAtStep}`}
`
)}
`;
}
/**
* What this call was given, argument by argument. A reference is a chip
* rather than a string: it carries the name the cell has, the handle behind
* it, the shape it declared and the labels riding on it, and it leads back
* to the step that produced it — which is the whole of tracing a value
* through a run.
*/
#arguments(step: ConsoleStep): TemplateResult | typeof nothing {
const args = consoleStepArguments(step, this.handles);
if (args.length === 0) {
return nothing;
}
const references = args.filter((argument) => argument.isReference);
return html`
${argument.key}
${origin === undefined
? html`
from an earlier turn
`
: html`
`}
`;
}
/**
* What the result let across as a value. The harness seals a string the
* schema does not pin to an enum or a const, but never a number — so an
* array of them is a channel as wide as its author cared to make it, and
* the size of one is stated here rather than left to be counted out of the
* JSON below.
*/
#disclosure(step: ConsoleStep): TemplateResult | typeof nothing {
const disclosure = step.disclosure;
if (disclosure === undefined) {
return nothing;
}
const wide = disclosure.longestNumericRun >= WIDE_NUMERIC_RUN;
return html`
disclosure
${disclosure.valueBytes} B as value
${disclosure
.sealedPositions} sealed behind a reference${disclosure
.longestNumericRun === 0
? ""
: ` · longest numeric run ${disclosure.longestNumericRun}`}
${wide
? html`
A run of ${disclosure
.longestNumericRun} numbers crossed as value. Numbers are never sealed, so an
array of them carries whatever its author chose to encode.
`
: nothing}
`;
}
/** What the model was thinking before the step, when the provider said. */
#reasoning(step: ConsoleStep): unknown {
return step.reasoning === undefined ? nothing : html`