mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
refactor(mcp): consolidate Evidence recording and operation progress
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import type { ProgressReporter } from "../application/ProgressReporter.js";
|
||||
|
||||
/** Report one lifecycle operation start through the shared monotonic boundary. */
|
||||
export const reportLifecycleStart = (
|
||||
/** Report one operation start through the shared monotonic boundary. */
|
||||
export const reportOperationStart = (
|
||||
progress: ProgressReporter,
|
||||
phase: string,
|
||||
message = "started",
|
||||
): Promise<void> => progress.report({ phase, completed: 0, total: 1, message });
|
||||
|
||||
/** Report one lifecycle operation's terminal disposition. */
|
||||
export const reportLifecycleEnd = (
|
||||
/** Report one operation's terminal disposition. */
|
||||
export const reportOperationEnd = (
|
||||
progress: ProgressReporter,
|
||||
phase: string,
|
||||
succeeded: boolean,
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
reportLifecycleEnd,
|
||||
reportLifecycleStart,
|
||||
} from "./lifecycleProgress.js";
|
||||
reportOperationEnd,
|
||||
reportOperationStart,
|
||||
} from "./operationProgress.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
@@ -83,9 +83,9 @@ export const registerCloseLifecycleTool = ({
|
||||
const closing = logToolExecution(logger, closeContract.name, () =>
|
||||
closeWithResources(() => session.close({ progress })),
|
||||
);
|
||||
await reportLifecycleStart(progress, closeContract.name);
|
||||
await reportOperationStart(progress, closeContract.name);
|
||||
const closed = await closing;
|
||||
await reportLifecycleEnd(progress, closeContract.name, closed.ok);
|
||||
await reportOperationEnd(progress, closeContract.name, closed.ok);
|
||||
return server.delivery.toCallToolResult(closed, closeContract);
|
||||
}
|
||||
const closing = logToolExecution(logger, closeContract.name, () =>
|
||||
@@ -95,13 +95,13 @@ export const registerCloseLifecycleTool = ({
|
||||
}),
|
||||
),
|
||||
);
|
||||
await reportLifecycleStart(
|
||||
await reportOperationStart(
|
||||
progress,
|
||||
closeContract.name,
|
||||
"saving snapshot; closing provider",
|
||||
);
|
||||
const closed = await closing;
|
||||
await reportLifecycleEnd(progress, closeContract.name, closed.ok);
|
||||
await reportOperationEnd(progress, closeContract.name, closed.ok);
|
||||
return server.delivery.toCallToolResult(
|
||||
closed.ok ? ok({ ...closed.value }) : closed,
|
||||
closeContract,
|
||||
|
||||
@@ -31,6 +31,10 @@ import { logToolExecution } from "./toolLogging.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { executeFunctionAnalysisEvidence } from "../application/FunctionAnalysisEvidence.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import {
|
||||
reportOperationStart,
|
||||
reportOperationEnd,
|
||||
} from "./operationProgress.js";
|
||||
|
||||
/** Optional session services used by enhanced tool registration. */
|
||||
export interface EnhancedToolRegistration {
|
||||
@@ -222,12 +226,7 @@ const executeEnhancedTool = async (
|
||||
const { validatedCall, context } = request;
|
||||
const name = validatedCall.name;
|
||||
const progress = mcpProgressReporter(context);
|
||||
await progress.report({
|
||||
phase: name,
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "started",
|
||||
});
|
||||
await reportOperationStart(progress, name);
|
||||
const parsedInput = validatedCall.input;
|
||||
const parameters = jsonParameters(parsedInput);
|
||||
if (name === "analyze_function")
|
||||
@@ -251,13 +250,7 @@ const executeEnhancedTool = async (
|
||||
),
|
||||
),
|
||||
);
|
||||
await progress.report({
|
||||
phase: name,
|
||||
completed: 1,
|
||||
total: 1,
|
||||
message: result.ok ? "completed" : "failed",
|
||||
terminal: true,
|
||||
});
|
||||
await reportOperationEnd(progress, name, result.ok);
|
||||
if (result.ok) {
|
||||
const evidence = createWorkflowEvidence({
|
||||
target: registration.activeTarget?.(),
|
||||
@@ -338,13 +331,7 @@ const executeFunctionTool = async (
|
||||
},
|
||||
),
|
||||
);
|
||||
await progress.report({
|
||||
phase: "analyze_function",
|
||||
completed: 1,
|
||||
total: 1,
|
||||
message: result.ok ? "completed" : "failed",
|
||||
terminal: true,
|
||||
});
|
||||
await reportOperationEnd(progress, "analyze_function", result.ok);
|
||||
if (!result.ok)
|
||||
return registration.delivery.toCallToolResult(result, contract);
|
||||
return registration.delivery.toEvidenceToolResult(
|
||||
|
||||
@@ -16,6 +16,11 @@ import { mcpProgressReporter } from "./mcpProgress.js";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import { recordSessionEvidenceSources } from "./sessionEvidence.js";
|
||||
import {
|
||||
reportOperationStart,
|
||||
reportOperationEnd,
|
||||
} from "./operationProgress.js";
|
||||
|
||||
interface EvidenceToolRegistration {
|
||||
readonly logger: Logger;
|
||||
@@ -73,12 +78,7 @@ export const registerEvidenceTools = (
|
||||
options.analysisForAdmission?.(admittedAnalysis) ??
|
||||
admittedAnalysis;
|
||||
const progress = mcpProgressReporter(context);
|
||||
await progress.report({
|
||||
phase: contract.name,
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "started",
|
||||
});
|
||||
await reportOperationStart(progress, contract.name);
|
||||
const executionParameters =
|
||||
options.parametersForExecution?.(contract.name, parameters) ??
|
||||
parameters;
|
||||
@@ -91,13 +91,7 @@ export const registerEvidenceTools = (
|
||||
progress,
|
||||
}),
|
||||
);
|
||||
await progress.report({
|
||||
phase: contract.name,
|
||||
completed: 1,
|
||||
total: 1,
|
||||
message: execution.ok ? "completed" : "failed",
|
||||
terminal: true,
|
||||
});
|
||||
await reportOperationEnd(progress, contract.name, execution.ok);
|
||||
if (!execution.ok)
|
||||
return server.delivery.toCallToolResult(execution, contract);
|
||||
const sourceEvidence =
|
||||
@@ -129,14 +123,12 @@ export const registerEvidenceTools = (
|
||||
evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id),
|
||||
},
|
||||
);
|
||||
for (const source of sourceEvidence) {
|
||||
const sourceRecorded = options.recordEvidence?.(source);
|
||||
if (sourceRecorded !== undefined && !sourceRecorded.ok)
|
||||
return server.delivery.toCallToolResult(
|
||||
sourceRecorded,
|
||||
contract,
|
||||
);
|
||||
}
|
||||
const sourceRecorded = recordSessionEvidenceSources(
|
||||
options.recordEvidence,
|
||||
sourceEvidence,
|
||||
);
|
||||
if (!sourceRecorded.ok)
|
||||
return server.delivery.toCallToolResult(sourceRecorded, contract);
|
||||
const projected =
|
||||
options.projectEvidence?.(contract.name, parameters, evidence) ??
|
||||
ok(evidence);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { deliverEvidenceResult } from "./contractToolHandler.js";
|
||||
|
||||
/** Register local script export without requiring a live browser provider. */
|
||||
export const registerWebScriptTool = (
|
||||
@@ -39,13 +40,11 @@ export const registerWebScriptTool = (
|
||||
signal: context.mcpReq.signal,
|
||||
}),
|
||||
);
|
||||
if (!result.ok)
|
||||
return server.delivery.toCallToolResult(result, contract);
|
||||
const recorded = options.recordEvidence?.(result.value);
|
||||
return server.delivery.toEvidenceToolResult(
|
||||
result.value,
|
||||
return deliverEvidenceResult(
|
||||
server,
|
||||
result,
|
||||
contract,
|
||||
recorded,
|
||||
options.recordEvidence,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
||||
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
@@ -16,6 +16,10 @@ import { readWithoutFifoWriter } from "../../fixtures/fifoInput.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { scriptScenarioFixture } from "../../fixtures/webScriptCapture.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { webScriptExportResultSchema } from "../../../src/domain/webScriptExport.js";
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"returns a capture_path invalid_request for a named pipe without waiting or publishing output",
|
||||
@@ -136,3 +140,64 @@ it("refuses an existing output directory as invalid output_directory input", asy
|
||||
});
|
||||
expect(await readdir(outputDirectory)).toEqual(["marker.txt"]);
|
||||
});
|
||||
|
||||
it("publishes script bytes and retains the returned Evidence without starting an analysis provider", async () => {
|
||||
const root = await createTestTempDirectory("rea-script-export-retained-mcp-");
|
||||
const capturePath = join(root, "capture.json");
|
||||
const outputDirectory = join(root, "export");
|
||||
const bytes = Buffer.from("export const callerValue = 'retained';\n");
|
||||
await writeFile(
|
||||
capturePath,
|
||||
JSON.stringify(
|
||||
scriptScenarioFixture([
|
||||
{ url: "https://caller.test/app/main.js", bytes },
|
||||
]),
|
||||
),
|
||||
);
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("local script export must not start an analysis provider");
|
||||
});
|
||||
const server = createServer({ kind: "session", session });
|
||||
const client = new Client({ name: "script-export-retention", version: "1" });
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const response = await client.callTool({
|
||||
name: "export_web_scripts",
|
||||
arguments: { capture_path: capturePath, output_directory: outputDirectory },
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const evidence = parseEvidence(response.structuredContent);
|
||||
expect(evidence.operation).toBe("export_web_scripts");
|
||||
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
|
||||
expect(session.exportEvidenceBundle().records).toEqual([evidence]);
|
||||
const exported = webScriptExportResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(exported.output_directory).toBe(outputDirectory);
|
||||
expect(exported.scripts).toHaveLength(1);
|
||||
const content = exported.scripts[0]?.content;
|
||||
expect(content?.state).toBe("exported");
|
||||
if (content?.state !== "exported")
|
||||
throw new Error("script bytes were not published");
|
||||
if (exported.analysis_input === null)
|
||||
throw new Error("published script bytes have no analysis input root");
|
||||
expect(
|
||||
await readFile(
|
||||
join(exported.analysis_input.input_path, content.relative_path),
|
||||
),
|
||||
).toEqual(bytes);
|
||||
expect(
|
||||
JSON.parse(await readFile(exported.manifest.path, "utf8")),
|
||||
).toMatchObject({
|
||||
capture_path: capturePath,
|
||||
output_directory: outputDirectory,
|
||||
scripts: exported.scripts,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user