Compare commits

..
22 changed files with 420 additions and 90 deletions
+56 -4
View File
@@ -7,15 +7,17 @@ on:
env:
HUSKY: "0"
permissions:
contents: write
pull-requests: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
@@ -25,11 +27,61 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check out release pull request
if: steps.release.outputs.prs_created == 'true'
uses: actions/checkout@v6
with:
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
persist-credentials: false
- name: Set up Node.js for generated documentation
if: steps.release.outputs.prs_created == 'true'
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
package-manager-cache: false
- name: Install dependencies
if: steps.release.outputs.prs_created == 'true'
run: npm ci
- name: Regenerate release documentation
if: steps.release.outputs.prs_created == 'true'
run: npm run docs:generate
- name: Commit canonical release catalog
if: steps.release.outputs.prs_created == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_PR_BRANCH: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
run: |
changed_paths="$(git diff --name-only)"
if [[ -z "${changed_paths}" ]]; then
echo "Release documentation is already canonical."
exit 0
fi
if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then
echo "docs:generate changed unexpected tracked paths:" >&2
printf '%s\n' "${changed_paths}" >&2
exit 1
fi
git diff --check
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -- docs/product-catalog.json
git commit -m "chore: normalize release product catalog"
gh auth setup-git
git push origin "HEAD:${RELEASE_PR_BRANCH}"
publish:
name: Publish to npm
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.5.0"
".": "2.5.1"
}
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## [2.5.1](https://github.com/morluto/rea/compare/rea-agents-2.5.0...rea-agents-2.5.1) (2026-07-25)
### Bug Fixes
* **ci:** normalize generated release metadata ([d43a461](https://github.com/morluto/rea/commit/d43a461958c7bef56a61f75d7f7da73b43e7c387))
* **ci:** normalize release pull request metadata ([119954c](https://github.com/morluto/rea/commit/119954c701533491ccbc4de649592e059f3f5d39))
### Performance Improvements
* **browser:** lazy-load Playwright sessions ([305e729](https://github.com/morluto/rea/commit/305e72995bd52caee7ad8b42aae531b8924e7d0d))
* **dotnet:** reuse authenticated artifact snapshots ([c5e8871](https://github.com/morluto/rea/commit/c5e8871bda737c52d6b09303f9c95e225bf845f2))
* eliminate repeated work in analysis hot paths ([fbe442b](https://github.com/morluto/rea/commit/fbe442b1e2395b93d1bffa2c192ea5ac9a0fab34))
* **evidence:** validate ledger mutations incrementally ([809dfbf](https://github.com/morluto/rea/commit/809dfbfe4868478bf62be40e5f9db35d1b52f6be))
* **javascript:** reuse parsed source across analyses ([adc4852](https://github.com/morluto/rea/commit/adc4852f3aea5bd2847ce9efb53fe43ad030dde6))
* **server:** cache advertised JSON schemas ([6743644](https://github.com/morluto/rea/commit/674364443dc12b89fe6444fdfb364f1b53086f90))
## [2.5.0](https://github.com/morluto/rea/compare/rea-agents-2.4.0...rea-agents-2.5.0) (2026-07-24)
+1 -1
View File
@@ -530,7 +530,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.5.1", "mcp"]
}
}
}
+1 -1
View File
@@ -183,7 +183,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.5.1", "mcp"]
}
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.5.1", "mcp"]
}
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.5.1", "mcp"]
}
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.5.1", "mcp"]
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"catalog_schema_version": 1,
"package": {
"name": "rea-agents",
"version": "2.5.0",
"version": "2.5.1",
"sdk": {
"server": "2.0.0-beta.4",
"client": "2.0.0-beta.4",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "2.5.0",
"version": "2.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "2.5.0",
"version": "2.5.1",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "rea-agents",
"version": "2.5.0",
"version": "2.5.1",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
+61 -18
View File
@@ -3,6 +3,7 @@ import canonicalize from "canonicalize";
import {
createEvidenceBundle,
parseEvidenceBundle,
validateResidualUnknownAddition,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
@@ -216,8 +217,6 @@ export class EvidenceLedger {
} catch (cause: unknown) {
return err(new UnknownRegistryError("invalid-transition", { cause }));
}
const pendingRecords = new Map(this.#records);
pendingRecords.set(evidence.evidence_id, evidence);
const existingUnknown = this.#unknownHeads.get(unknown.unknown_id);
if (existingUnknown !== undefined) {
if (
@@ -225,20 +224,12 @@ export class EvidenceLedger {
existingUnknown.revision_digest !== unknown.revision_digest
)
return err(new UnknownRegistryError("already-exists"));
const checked = this.#validateCandidate(
pendingRecords,
this.#unknownRevisions,
);
const checked = this.#appendIncremental([evidence]);
if (!checked.ok) return checked;
this.#commit(pendingRecords, this.#unknownRevisions, checked.value);
return ok(null);
}
pendingRecords.set(mutation.evidence_id, mutation);
const pendingUnknowns = new Map(this.#unknownRevisions);
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
const checked = this.#appendIncremental([evidence, mutation], unknown);
if (!checked.ok) return checked;
this.#commit(pendingRecords, pendingUnknowns, checked.value);
return ok(structuredClone(unknown));
}
@@ -332,16 +323,68 @@ export class EvidenceLedger {
}),
);
}
const pendingRecords = new Map(this.#records);
pendingRecords.set(mutationEvidence.evidence_id, mutationEvidence);
const pendingUnknowns = new Map(this.#unknownRevisions);
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
const checked = this.#appendIncremental([mutationEvidence], unknown);
if (!checked.ok) return checked;
this.#commit(pendingRecords, pendingUnknowns, checked.value);
return ok(structuredClone(unknown));
}
#appendIncremental(
records: readonly Evidence[],
unknown?: ResidualUnknown,
): Result<
void,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
const additions = new Map<string, Evidence>();
let addedBytes = 0;
for (const evidence of records) {
const existing =
additions.get(evidence.evidence_id) ??
this.#records.get(evidence.evidence_id);
if (existing !== undefined) {
if (!recordsAgree(existing, evidence))
return err(new EvidenceIntegrityError("Conflicting evidence record"));
continue;
}
additions.set(evidence.evidence_id, evidence);
addedBytes += serializedBytes(evidence);
}
const unknownKey =
unknown === undefined ? undefined : unknownRevisionKey(unknown);
if (unknownKey !== undefined && this.#unknownRevisions.has(unknownKey))
return err(new UnknownRegistryError("integrity"));
const unknownBytes = unknown === undefined ? 0 : serializedBytes(unknown);
if (
this.#exceedsRecordLimit(
this.#records.size + additions.size,
this.#unknownRevisions.size + (unknown === undefined ? 0 : 1),
)
)
return err(new EvidenceLimitError("records", this.limits.maxRecords));
if (this.#bytes + addedBytes + unknownBytes > this.limits.maxBytes)
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
if (unknown !== undefined)
try {
validateResidualUnknownAddition(
unknown,
{
get: (id) => additions.get(id) ?? this.#records.get(id),
has: (id) => additions.has(id) || this.#records.has(id),
},
this.#unknownHeads,
);
} catch (cause: unknown) {
return err(new UnknownRegistryError("integrity", { cause }));
}
for (const [id, evidence] of additions) this.#records.set(id, evidence);
if (unknown !== undefined && unknownKey !== undefined) {
this.#unknownRevisions.set(unknownKey, unknown);
this.#unknownHeads.set(unknown.unknown_id, unknown);
}
this.#bytes += addedBytes + unknownBytes;
return ok(undefined);
}
#validateCandidate(
records: ReadonlyMap<string, Evidence>,
unknowns: ReadonlyMap<string, ResidualUnknown>,
+23 -10
View File
@@ -1,16 +1,18 @@
import { createHash } from "node:crypto";
import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import {
DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
type JavaScriptSemanticLimits,
} from "../domain/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
JavaScriptStaticAnalysis,
} from "../domain/javascriptStaticAnalysisTypes.js";
import { failedJavaScriptStaticAnalysis } from "../domain/javascriptStaticAnalysisHelpers.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
@@ -142,13 +144,24 @@ const analyzeArtifactFile = (
state.truncatedScopes += 1;
return;
}
const analysis = analyzeJavaScriptStaticSource(file.text.value, {
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
});
const parsed = parseJavaScriptSource(file.text.value);
if (parsed === null) {
const analysis = failedJavaScriptStaticAnalysis();
state.files.push({ file, javascript: analysis, semantic: null });
state.parseFailures += 1;
return;
}
const analysis = analyzeParsedJavaScriptStaticSource(
file.text.value,
parsed,
{
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
},
);
const staticFindings = findingCount(analysis);
const semanticFindingBudget = Math.max(
0,
@@ -164,7 +177,7 @@ const analyzeArtifactFile = (
};
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
? analyzeJavaScriptSemantics(file.text.value, semanticLimits)
? analyzeParsedJavaScriptSemantics(parsed, semanticLimits)
: null;
state.files.push({
file,
@@ -29,9 +29,19 @@ import type {
BrowserScenarioSessionPort,
} from "./BrowserScenarioSessionPort.js";
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
import { PlaywrightScenarioSessionFactory } from "./PlaywrightScenarioSession.js";
const OPERATION = "capture_browser_scenario" as const;
let defaultFactory: Promise<BrowserScenarioSessionFactory> | undefined;
const lazyPlaywrightFactory: BrowserScenarioSessionFactory = {
async open(scenario, options) {
defaultFactory ??= import("./PlaywrightScenarioSession.js").then(
({ PlaywrightScenarioSessionFactory }) =>
new PlaywrightScenarioSessionFactory(),
);
return (await defaultFactory).open(scenario, options);
},
};
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
@@ -353,7 +363,7 @@ export class PlaywrightBrowserScenarioProvider
implements BrowserScenarioCapturePort
{
constructor(
private readonly factory: BrowserScenarioSessionFactory = new PlaywrightScenarioSessionFactory(),
private readonly factory: BrowserScenarioSessionFactory = lazyPlaywrightFactory,
) {}
identity(): ProviderIdentity {
+73 -1
View File
@@ -239,7 +239,7 @@ const validateUnknownGraph = (
const validateVerifiedResolution = (
unknown: ResidualUnknown,
evidenceById: ReadonlyMap<string, Evidence>,
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get">,
): void => {
if (unknown.contradicting_evidence_ids.length > 0)
throw new TypeError(
@@ -260,6 +260,78 @@ const validateVerifiedResolution = (
);
};
/**
* Validate one new residual-unknown revision against an already valid ledger.
* This keeps ordinary mutations proportional to the affected revision while
* full bundle imports continue to validate the complete graph.
*/
export const validateResidualUnknownAddition = (
unknown: ResidualUnknown,
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get" | "has">,
currentHeads: ReadonlyMap<string, ResidualUnknown>,
): void => {
const current = currentHeads.get(unknown.unknown_id);
if (
current === undefined
? unknown.revision !== 1 || unknown.previous_revision_digest !== null
: unknown.revision !== current.revision + 1 ||
unknown.previous_revision_digest !== current.revision_digest
)
throw new TypeError(
`Residual unknown ${unknown.unknown_id} revision chain is broken`,
);
for (const evidenceId of referencedEvidenceIds(unknown))
if (!evidenceById.has(evidenceId))
throw new TypeError(
`Residual unknown references missing evidence ${evidenceId}`,
);
for (const relationship of unknown.relationships)
if (
relationship.unknown_id !== unknown.unknown_id &&
!currentHeads.has(relationship.unknown_id)
)
throw new TypeError(
`Residual unknown references missing unknown ${relationship.unknown_id}`,
);
if (unknown.resolution?.disposition === "verified")
validateVerifiedResolution(unknown, evidenceById);
for (const relationship of unknown.relationships)
if (
relationship.type === "depends-on" &&
reachesUnknown(
relationship.unknown_id,
unknown.unknown_id,
currentHeads,
unknown,
)
)
throw new TypeError("Residual unknown dependency graph contains a cycle");
};
const reachesUnknown = (
start: string,
goal: string,
currentHeads: ReadonlyMap<string, ResidualUnknown>,
candidate: ResidualUnknown,
): boolean => {
const pending = [start];
const visited = new Set<string>();
while (pending.length > 0) {
const id = pending.pop();
if (id === undefined) break;
if (id === goal) return true;
if (visited.has(id)) continue;
visited.add(id);
const unknown =
id === candidate.unknown_id ? candidate : currentHeads.get(id);
if (unknown === undefined) continue;
for (const relationship of unknown.relationships)
if (relationship.type === "depends-on")
pending.push(relationship.unknown_id);
}
return false;
};
const evidenceQualifies = (
unknown: ResidualUnknown,
evidence: Evidence,
+15 -11
View File
@@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import {
@@ -41,6 +40,10 @@ import {
propertyName,
range,
} from "./javascriptStaticAnalysisHelpers.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
interface BindPatternInput {
readonly pattern: t.Node;
@@ -68,18 +71,19 @@ interface AddBindingInput {
export const analyzeJavaScriptSemantics = (
source: string,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptSemanticIr()
: analyzeParsedJavaScriptSemantics(file, inputLimits);
};
/** Recover semantics from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptSemantics = (
file: ParsedJavaScriptSource,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits };
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptSemanticIr();
}
const state = createState(file.program, limits);
collectDefinitions(file.program, state);
const references = collectSemanticReferences(file.program, state);
+20
View File
@@ -0,0 +1,20 @@
import { parse } from "@babel/parser";
/** Babel AST produced by REA's inert JavaScript parser boundary. */
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
/** Parse JavaScript or TypeScript once without retaining unused comment nodes. */
export const parseJavaScriptSource = (
source: string,
): ParsedJavaScriptSource | null => {
try {
return parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
attachComment: false,
plugins: ["jsx", "typescript"],
});
} catch {
return null;
}
};
+18 -13
View File
@@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
@@ -29,22 +28,28 @@ import type {
JavaScriptStaticAnalysis,
JavaScriptStaticAnalysisLimits,
} from "./javascriptStaticAnalysisTypes.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
/** Parse one bounded JavaScript artifact and recover static structure only. */
export const analyzeJavaScriptStaticSource = (
source: string,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptStaticAnalysis();
}
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptStaticAnalysis()
: analyzeParsedJavaScriptStaticSource(source, file, limits);
};
/** Recover static structure from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptStaticSource = (
source: string,
file: ParsedJavaScriptSource,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
const accumulator = createJavaScriptAnalysisAccumulator();
traverseStaticSource(source, file, accumulator, limits);
addSourceMapDirectives(source, accumulator, limits.maxFindings);
@@ -53,7 +58,7 @@ export const analyzeJavaScriptStaticSource = (
const traverseStaticSource = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): void => {
@@ -76,7 +81,7 @@ const traverseStaticSource = (
const finalizeStaticAnalysis = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
+56 -8
View File
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import {
@@ -85,6 +86,8 @@ export class ManagedStaticProvider implements AnalysisProvider {
}
class ManagedStaticClient implements AnalysisClient {
#snapshotBytes: Buffer | undefined;
constructor(private readonly target: BinaryTarget) {}
async execute(
@@ -123,11 +126,12 @@ class ManagedStaticClient implements AnalysisClient {
`Artifact size ${String(metadata.size)} exceeds max_file_bytes ${String(maxFileBytes)}.`,
),
);
const bytes = await readFile(
this.target.path,
options?.signal === undefined ? undefined : { signal: options.signal },
);
if (bytes.length > maxFileBytes)
const snapshot = this.#snapshotBytes;
const observed =
snapshot === undefined
? await readManagedSnapshot(this.target.path, options?.signal)
: await hashManagedSource(this.target.path, options?.signal);
if (observed.byteLength > maxFileBytes)
return err(
new AnalysisCapabilityUnavailableError(
IDENTITY.id,
@@ -135,13 +139,16 @@ class ManagedStaticClient implements AnalysisClient {
`Artifact grew beyond max_file_bytes ${String(maxFileBytes)} while it was read.`,
),
);
const observedDigest = createHash("sha256").update(bytes).digest("hex");
if (observedDigest !== this.target.sha256)
if (observed.sha256 !== this.target.sha256)
return err(
new EvidenceIntegrityError(
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observedDigest} at ${this.target.path}`,
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observed.sha256} at ${this.target.path}`,
),
);
const bytes = snapshot ?? observed.bytes;
if (bytes === undefined)
throw new TypeError("Managed snapshot bytes are unavailable");
this.#snapshotBytes = bytes;
const result = inspectManagedOperation(
operation,
parameters,
@@ -164,10 +171,51 @@ class ManagedStaticClient implements AnalysisClient {
}
close(): Promise<void> {
this.#snapshotBytes = undefined;
return Promise.resolve();
}
}
interface ManagedSourceObservation {
readonly byteLength: number;
readonly sha256: string;
readonly bytes?: Buffer;
}
const readManagedSnapshot = async (
path: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const bytes = await readFile(
path,
signal === undefined ? undefined : { signal },
);
return {
byteLength: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes,
};
};
const hashManagedSource = async (
path: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const digest = createHash("sha256");
let byteLength = 0;
const stream = createReadStream(
path,
signal === undefined ? undefined : { signal },
);
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk))
throw new TypeError("Managed source stream returned non-buffer data");
byteLength += chunk.length;
digest.update(chunk);
}
return { byteLength, sha256: digest.digest("hex") };
};
const isManagedOperation = (
operation: AnalysisOperation,
): operation is ManagedToolName =>
+1 -1
View File
@@ -1,7 +1,7 @@
/** Generated from package.json and package-lock.json; do not edit. */
export const PACKAGE_METADATA = {
name: "rea-agents",
version: "2.5.0", // x-release-please-version
version: "2.5.1", // x-release-please-version
serverSdkVersion: "2.0.0-beta.4",
clientSdkVersion: "2.0.0-beta.4",
coreSdkVersion: "2.0.0-beta.4",
+42 -12
View File
@@ -2,6 +2,15 @@ import { z } from "zod";
import type { ToolContract } from "../contracts/toolContracts.js";
const ADVERTISED_INPUT_SCHEMAS = new WeakMap<
z.ZodObject,
Readonly<Record<string, unknown>>
>();
const ADVERTISED_OUTPUT_SCHEMAS = new WeakMap<
z.ZodObject,
Readonly<Record<string, unknown>>
>();
/** Project the public contract fields accepted by MCP tool registration. */
export const toolRegistrationOptions = (contract: ToolContract) => ({
title: contract.title,
@@ -22,10 +31,8 @@ const advertisedInputSchema = (schema: z.ZodObject) => ({
vendor: "rea",
validate: (input: unknown) => ({ value: input }),
jsonSchema: {
input: () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
output: () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
input: () => advertisedInputJsonSchema(schema),
output: () => advertisedInputJsonSchema(schema),
},
},
});
@@ -41,19 +48,42 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
"~standard": {
...standard,
jsonSchema: {
input: () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
output: () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
input: () => advertisedOutputJsonSchema(schema, converter),
output: () => advertisedOutputJsonSchema(schema, converter),
},
},
};
};
const advertisedInputJsonSchema = (
schema: z.ZodObject,
): Readonly<Record<string, unknown>> =>
cachedJsonSchema(ADVERTISED_INPUT_SCHEMAS, schema, () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
);
const advertisedOutputJsonSchema = (
schema: z.ZodObject,
converter: NonNullable<z.ZodObject["~standard"]["jsonSchema"]>["output"],
): Readonly<Record<string, unknown>> =>
cachedJsonSchema(ADVERTISED_OUTPUT_SCHEMAS, schema, () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
);
const cachedJsonSchema = (
cache: WeakMap<z.ZodObject, Readonly<Record<string, unknown>>>,
schema: z.ZodObject,
create: () => Record<string, unknown>,
): Readonly<Record<string, unknown>> => {
const cached = cache.get(schema);
if (cached !== undefined) return cached;
const created = create();
cache.set(schema, created);
return created;
};
const inputJsonSchema = (schema: z.ZodObject): Record<string, unknown> => {
const converter = schema["~standard"].jsonSchema?.input;
if (converter === undefined)
+15
View File
@@ -62,6 +62,21 @@ const emptySchemaPaths = (value: unknown, path = "$"): string[] => {
};
describe("tool contract surface", () => {
it("reuses finalized advertised schemas", () => {
const contract = TOOL_CONTRACTS[0];
expect(contract).toBeDefined();
if (contract === undefined) return;
const first = toolRegistrationOptions(contract);
const second = toolRegistrationOptions(contract);
expect(first.inputSchema["~standard"].jsonSchema.input()).toBe(
second.inputSchema["~standard"].jsonSchema.output(),
);
expect(first.outputSchema["~standard"].jsonSchema.input()).toBe(
second.outputSchema["~standard"].jsonSchema.output(),
);
});
it("gives fallback parameter guidance operational meaning", () => {
const contract = TOOL_CONTRACTS.find(
({ name }) => name === "analyze_function",