mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
2e6b38cef0
* 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>
251 lines
8.4 KiB
TypeScript
251 lines
8.4 KiB
TypeScript
import type { AnalysisViewResult } from "../../domain/analysisView/analysisView.js";
|
|
import {
|
|
changesState,
|
|
type StateDocument,
|
|
type StateEntity,
|
|
type StateType,
|
|
type StateView,
|
|
} from "../../domain/analysisView/environmentStateRules.js";
|
|
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import type {
|
|
AnalysisInputIssue,
|
|
AnalysisResourceConstraintError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewService.js";
|
|
import {
|
|
ENVIRONMENT_SERVE_OPERATION,
|
|
inputError,
|
|
parseEvidenceFile,
|
|
parseViewEvidence,
|
|
type EnvironmentManifestTool,
|
|
type EnvironmentSource,
|
|
} from "./environmentSource.js";
|
|
import {
|
|
environmentToolDefinitions,
|
|
type EnvironmentDataPlane,
|
|
type EnvironmentToolDefinition,
|
|
} from "./environmentTools.js";
|
|
|
|
type EnvironmentStateView = Extract<
|
|
AnalysisViewResult,
|
|
{ kind: "environment-state" }
|
|
>;
|
|
|
|
/** The initial state an environment-state view extracted for the served manifest, authenticated and cross-checked. */
|
|
export interface EnvironmentState {
|
|
readonly evidenceId: string;
|
|
readonly types: readonly StateType[];
|
|
/** Render plans keyed by tool name. */
|
|
readonly views: ReadonlyMap<string, StateView>;
|
|
readonly entities: readonly StateEntity[];
|
|
readonly documents: readonly StateDocument[];
|
|
}
|
|
|
|
/** Serving from the state is read-only: tools whose requests may change state are not served. */
|
|
const servedFromState = (tool: EnvironmentManifestTool): boolean =>
|
|
!changesState(tool.method, tool.path_template);
|
|
|
|
/** The tools an environment serves and the data plane its card declares. */
|
|
export interface ServedCatalog {
|
|
readonly tools: readonly EnvironmentToolDefinition[];
|
|
readonly dataPlane: EnvironmentDataPlane;
|
|
}
|
|
|
|
/** The catalog an environment serves: every manifest tool for replay, the read tools and the data plane from a state. */
|
|
export const servedCatalog = (
|
|
source: EnvironmentSource,
|
|
fromState: boolean,
|
|
name: string,
|
|
operation: string = ENVIRONMENT_SERVE_OPERATION,
|
|
): Result<ServedCatalog, AnalysisResourceConstraintError> => {
|
|
const dataPlane: EnvironmentDataPlane = fromState ? "state" : "none";
|
|
const catalogSource: EnvironmentSource = fromState
|
|
? {
|
|
...source,
|
|
manifest: {
|
|
...source.manifest,
|
|
tools: source.manifest.tools.filter(servedFromState),
|
|
},
|
|
}
|
|
: source;
|
|
const tools = environmentToolDefinitions(catalogSource, name, {
|
|
operation,
|
|
dataPlane,
|
|
});
|
|
return tools.ok ? ok({ tools: tools.value, dataPlane }) : tools;
|
|
};
|
|
|
|
const refusal = (
|
|
operation: string,
|
|
issue: AnalysisInputIssue,
|
|
userMessage: string,
|
|
): AnalysisError =>
|
|
inputError(
|
|
operation,
|
|
[{ ...issue, path: ["state", "normalized_result", ...issue.path] }],
|
|
userMessage,
|
|
);
|
|
|
|
/** Why the state does not belong to the served manifest and capture, if it does not. */
|
|
const stateMismatch = (
|
|
operation: string,
|
|
view: EnvironmentStateView,
|
|
source: EnvironmentSource,
|
|
): AnalysisError | undefined => {
|
|
if (view.parent_evidence_id !== source.evidenceId)
|
|
return refusal(
|
|
operation,
|
|
{
|
|
path: ["parent_evidence_id"],
|
|
reason: "invalid_value",
|
|
expected: source.evidenceId,
|
|
message: `The state cites parent Evidence ${view.parent_evidence_id}; the evidence file is ${source.evidenceId}.`,
|
|
},
|
|
"The state was extracted from a different capture than the supplied evidence file.",
|
|
);
|
|
const rules = source.manifest.view.template_rules;
|
|
if (
|
|
view.view.template_rules.length !== rules.length ||
|
|
view.view.template_rules.some((rule, index) => rule !== rules[index])
|
|
)
|
|
return refusal(
|
|
operation,
|
|
{
|
|
path: ["view", "template_rules"],
|
|
reason: "invalid_value",
|
|
expected: [...rules],
|
|
message:
|
|
"The state must be extracted with the manifest's template_rules, in the same order, so its tool names are the manifest's.",
|
|
},
|
|
"The state was extracted with different template rules than the manifest.",
|
|
);
|
|
const tools = new Set(source.manifest.tools.map((tool) => tool.name));
|
|
const index = view.views.findIndex((entry) => !tools.has(entry.tool));
|
|
return index < 0
|
|
? undefined
|
|
: refusal(
|
|
operation,
|
|
{
|
|
path: ["views", index, "tool"],
|
|
reason: "invalid_value",
|
|
message: "Every state view must name a tool of the served manifest.",
|
|
},
|
|
"The state names a tool the manifest does not have.",
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Load the environment-state Evidence for an already loaded manifest and
|
|
* capture, refusing a state extracted from another capture or with other
|
|
* template rules; refusals name the command that loaded it.
|
|
*/
|
|
export const loadEnvironmentState = (
|
|
stateValue: unknown,
|
|
source: EnvironmentSource,
|
|
operation: string = ENVIRONMENT_SERVE_OPERATION,
|
|
): Result<EnvironmentState, AnalysisError> => {
|
|
const parsed = parseViewEvidence(
|
|
operation,
|
|
"state",
|
|
stateValue,
|
|
"environment-state",
|
|
);
|
|
if (!parsed.ok) return parsed;
|
|
const { view, evidenceId } = parsed.value;
|
|
const mismatch = stateMismatch(operation, view, source);
|
|
if (mismatch !== undefined) return err(mismatch);
|
|
return ok({
|
|
evidenceId,
|
|
types: view.types,
|
|
views: new Map(view.views.map((entry) => [entry.tool, entry])),
|
|
entities: view.entities,
|
|
documents: view.documents,
|
|
});
|
|
};
|
|
|
|
/** Derive the state the capture observed with the manifest's rules, projected as retained Evidence, and check it as a supplied state. */
|
|
export const deriveEnvironmentState = (
|
|
source: EnvironmentSource,
|
|
captureValue: unknown,
|
|
operation: string = ENVIRONMENT_SERVE_OPERATION,
|
|
): Result<EnvironmentState, AnalysisError> => {
|
|
const capture = parseEvidenceFile(operation, "evidence", captureValue);
|
|
if (!capture.ok) return capture;
|
|
const captureId = capture.value.evidence_id;
|
|
const projected = inspectAnalysisViewValidated(
|
|
{
|
|
source: { kind: "retained-evidence", evidence_id: captureId },
|
|
view: {
|
|
kind: "environment-state",
|
|
template_rules: [...source.manifest.view.template_rules],
|
|
},
|
|
},
|
|
(evidenceId) => (evidenceId === captureId ? capture.value : undefined),
|
|
);
|
|
return projected.ok
|
|
? loadEnvironmentState(projected.value, source, operation)
|
|
: projected;
|
|
};
|
|
|
|
/** What a command reports about the state: the state Evidence it serves, or why it serves replay. */
|
|
export type StateServingReport =
|
|
| { readonly served: true; readonly evidence_id: string }
|
|
| { readonly served: false; readonly reason: "replay-only" }
|
|
| {
|
|
readonly served: false;
|
|
readonly reason: "unavailable";
|
|
readonly code: string;
|
|
readonly message: string;
|
|
};
|
|
|
|
/** The state an environment serves, if any, and how its command reports it. */
|
|
export interface ServedState {
|
|
readonly state: EnvironmentState | undefined;
|
|
readonly report: StateServingReport;
|
|
}
|
|
|
|
/** What preparing the state yields: served or unavailable, never replay-only, which only the serve command chooses. */
|
|
export interface PreparedState extends ServedState {
|
|
readonly report: Exclude<
|
|
StateServingReport,
|
|
{ readonly reason: "replay-only" }
|
|
>;
|
|
}
|
|
|
|
const unavailable = (error: AnalysisError): PreparedState => {
|
|
const { code, message } = projectAnalysisError(error);
|
|
return {
|
|
state: undefined,
|
|
report: { served: false, reason: "unavailable", code, message },
|
|
};
|
|
};
|
|
|
|
/** Serve the derived state when its catalog fits under the served name; otherwise serve replay and report why. */
|
|
export const prepareServedState = (
|
|
source: EnvironmentSource,
|
|
captureValue: unknown,
|
|
options: {
|
|
readonly name?: string | undefined;
|
|
readonly operation?: string;
|
|
} = {},
|
|
): PreparedState => {
|
|
const operation = options.operation ?? ENVIRONMENT_SERVE_OPERATION;
|
|
// The state catalog depends only on the manifest and name, so checking it first spares deriving a state that cannot be served.
|
|
const catalog = servedCatalog(
|
|
source,
|
|
true,
|
|
options.name ?? source.manifest.name,
|
|
operation,
|
|
);
|
|
if (!catalog.ok) return unavailable(catalog.error);
|
|
const derived = deriveEnvironmentState(source, captureValue, operation);
|
|
return derived.ok
|
|
? {
|
|
state: derived.value,
|
|
report: { served: true, evidence_id: derived.value.evidenceId },
|
|
}
|
|
: unavailable(derived.error);
|
|
};
|