mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
* feat(environment): render more read tools from the state and describe responses Keep main's environment serving and fold in coverage for non-entity reads, response summaries, and MCP tool annotations. Squashed rebase of #5352 onto main. * fix(environment): keep rendered documents and quoted samples inside their budgets A recorded body of exponential numbers can render past 256 KiB and turn a working read into a state size error, so that document stays a recorded replay. Description samples are measured after JSON quoting, and the replay-fallback catalog check uses a tool count the longer response descriptions still separate. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(environment): pin the replay card this branch serves The replay-only card test pinned main's 11,131-byte card. This branch's descriptions name each tool's recorded responses, so the card it serves is 12,818 bytes; the pin and its name follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
import type { JsonValue } from "../jsonValue.js";
|
|
import { compareUnicodeCodePoints as compare } from "../unicodeCodePointOrder.js";
|
|
import {
|
|
same,
|
|
type JsonObject,
|
|
type StateEntity,
|
|
} from "./environmentStateRules.js";
|
|
|
|
/** One entity object as one tool recorded it. */
|
|
export interface StateObservation {
|
|
readonly position: number;
|
|
readonly ordinal: number;
|
|
readonly tool: string;
|
|
readonly item: boolean;
|
|
readonly object: JsonObject;
|
|
}
|
|
|
|
/** Every init observation of one entity, with where it was first seen. */
|
|
export interface ObservedEntity {
|
|
readonly type: string;
|
|
readonly collection: string;
|
|
readonly id: string;
|
|
/** The earliest recorded sighting: timeline position, then position in its list. */
|
|
order: readonly [number, number];
|
|
readonly observations: StateObservation[];
|
|
}
|
|
|
|
/** Collection order: type, collection, then first sighting. */
|
|
export const compareObservedEntities = (
|
|
left: ObservedEntity,
|
|
right: ObservedEntity,
|
|
): number =>
|
|
compare(left.type, right.type) ||
|
|
compare(left.collection, right.collection) ||
|
|
left.order[0] - right.order[0] ||
|
|
left.order[1] - right.order[1];
|
|
|
|
/** One entity: item views' latest values win over list views', and a view whose latest values differ keeps them. */
|
|
export const mergeObservedEntity = (entity: ObservedEntity): StateEntity => {
|
|
const latest = new Map<string, StateObservation>();
|
|
for (const observation of entity.observations)
|
|
latest.set(observation.tool, observation);
|
|
const fields = new Map<string, JsonValue>();
|
|
const ranked = [...latest.values()].sort(
|
|
(left, right) =>
|
|
Number(left.item) - Number(right.item) || left.position - right.position,
|
|
);
|
|
for (const observation of ranked)
|
|
for (const [name, value] of Object.entries(observation.object))
|
|
fields.set(name, value);
|
|
return {
|
|
type: entity.type,
|
|
collection: entity.collection,
|
|
id: entity.id,
|
|
fields: Object.fromEntries(fields),
|
|
view_fields: [...latest.values()]
|
|
.map((observation) => ({
|
|
tool: observation.tool,
|
|
fields: Object.fromEntries(
|
|
Object.entries(observation.object).filter(
|
|
([name, value]) => !same(fields.get(name), value),
|
|
),
|
|
),
|
|
}))
|
|
.filter((view) => Object.keys(view.fields).length > 0)
|
|
.sort((left, right) => compare(left.tool, right.tool)),
|
|
// Cited in timeline order, whichever plan observed them first.
|
|
record_ordinals: [
|
|
...new Set(
|
|
entity.observations
|
|
.toSorted((left, right) => left.position - right.position)
|
|
.map((observation) => observation.ordinal),
|
|
),
|
|
],
|
|
};
|
|
};
|
|
|
|
/**
|
|
* How observed values changed, per entity in timeline order. A shared field that differs from the
|
|
* previous repeat of the same request line is volatile, unless a recorded write lies between them;
|
|
* a field that differs from its previous observation on another line across a write rewrites the
|
|
* type. Either across a write means the reads do not agree on one initial state.
|
|
*/
|
|
export const classifyEntityChanges = (
|
|
entities: Iterable<ObservedEntity>,
|
|
lineOf: (observation: StateObservation) => readonly string[],
|
|
writeBetween: (earlier: StateObservation, later: StateObservation) => boolean,
|
|
): {
|
|
readonly rewritten: ReadonlySet<string>;
|
|
readonly volatile: ReadonlyMap<string, Set<string>>;
|
|
} => {
|
|
const rewritten = new Set<string>();
|
|
const volatile = new Map<string, Set<string>>();
|
|
const changed = (before: StateObservation, after: StateObservation) =>
|
|
Object.keys(after.object).filter(
|
|
(name) =>
|
|
Object.hasOwn(before.object, name) &&
|
|
!same(before.object[name], after.object[name]),
|
|
);
|
|
for (const entity of entities) {
|
|
const timeline = [...entity.observations].sort(
|
|
(left, right) => left.position - right.position,
|
|
);
|
|
const lastOfLine = new Map<string, StateObservation>();
|
|
// Sparse projections do not erase where each field was last observed.
|
|
const lastOfField = new Map<
|
|
string,
|
|
{ readonly observation: StateObservation; readonly line: string }
|
|
>();
|
|
timeline.forEach((after) => {
|
|
const line = JSON.stringify(lineOf(after));
|
|
const repeat = lastOfLine.get(line);
|
|
if (repeat !== undefined) {
|
|
if (writeBetween(repeat, after)) {
|
|
// Key additions/removals are changes too, even when every shared field is unchanged.
|
|
if (!same(repeat.object, after.object)) rewritten.add(entity.type);
|
|
} else
|
|
for (const name of changed(repeat, after))
|
|
volatile.set(
|
|
entity.type,
|
|
(volatile.get(entity.type) ?? new Set()).add(name),
|
|
);
|
|
}
|
|
const sighting = { observation: after, line };
|
|
for (const name of Object.keys(after.object)) {
|
|
const previous = lastOfField.get(name);
|
|
if (
|
|
previous !== undefined &&
|
|
previous.line !== line &&
|
|
!same(previous.observation.object[name], after.object[name]) &&
|
|
writeBetween(previous.observation, after)
|
|
)
|
|
rewritten.add(entity.type);
|
|
lastOfField.set(name, sighting);
|
|
}
|
|
lastOfLine.set(line, after);
|
|
});
|
|
}
|
|
return { rewritten, volatile };
|
|
};
|