fix: align aggregate tools with session capabilities
This commit is contained in:
parent
f3b3fb07de
commit
ede91ff1f0
|
|
@ -671,10 +671,10 @@
|
|||
"mcp_resource_templates": 11
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "daef46f5c6af3e04bf5adde36468da9a87c862057f8e64ff423b22028f5680c4",
|
||||
"tools_sha256": "58bc4f72c5f8de4328728ead9f3a28ec5548484b7b9cf2ed85cf0edcc689a63c",
|
||||
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
|
||||
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
|
||||
"combined_sha256": "5b09f9fa9e75e9cd520fa405cb869e1c6bdbbcc576f52d5a334482975ec7b1fe",
|
||||
"combined_sha256": "e83a177f5eca63bbad17886a61f5a1a376d44afe862c51c56e5d38339f7d41f8",
|
||||
"providers_sha256": "918ccd6b8ae442cd5844dcfde55086f458176241c8d166c1e9dd0f242ccd2bb0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schema_version": 1,
|
||||
"manifest_id": "ecm_9571e8e6cbe76192d3d6b2b0f3f039aa72d13c45c93278510abf8db4951a8aa8",
|
||||
"manifest_id": "ecm_c1609d2cc959477e200df1a368ade5e3d68ad6fd4041009b78a5778a3b76e9bc",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "30e1c7251836d3c9d32d2dc17f5813320b36c22ea4f2516f2c8ee2022d1f76cd"
|
||||
"sha256": "1398947e9119a5a9af08514413d8cfdd8dcdc9373ebab1ba6eb525126db5ffff"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
|
|||
metadata:
|
||||
version: "23"
|
||||
tool_count: 114
|
||||
catalog_digest: "5b09f9fa9e75e9cd520fa405cb869e1c6bdbbcc576f52d5a334482975ec7b1fe"
|
||||
catalog_digest: "e83a177f5eca63bbad17886a61f5a1a376d44afe862c51c56e5d38339f7d41f8"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import type {
|
|||
AnalysisOperation,
|
||||
AnalysisOperationPort,
|
||||
} from "./AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../domain/errors.js";
|
||||
import { AnalysisOutputError, type AnalysisError } from "../domain/errors.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
type Facet =
|
||||
| { readonly state: "available"; readonly value: JsonValue }
|
||||
|
|
@ -83,7 +83,9 @@ export const inspectAddressContext = async (
|
|||
input: { readonly address: string; readonly document?: string | undefined },
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const args = parameters(input.document, input.address);
|
||||
const document = await resolveDocument(analysis, input.document, signal);
|
||||
if (!document.ok) return document;
|
||||
const args = parameters(document.value, input.address);
|
||||
const operations = [
|
||||
"address_name",
|
||||
"resolve_containing_procedure",
|
||||
|
|
@ -95,7 +97,7 @@ export const inspectAddressContext = async (
|
|||
operations.map((operation) =>
|
||||
analysis.execute(
|
||||
operation,
|
||||
operation === "list_bookmarks" ? parameters(input.document) : args,
|
||||
operation === "list_bookmarks" ? parameters(document.value) : args,
|
||||
executionOptions(signal),
|
||||
),
|
||||
),
|
||||
|
|
@ -110,7 +112,7 @@ export const inspectAddressContext = async (
|
|||
const bookmarks = facet(results[4], operations[4]);
|
||||
return ok({
|
||||
address: input.address,
|
||||
document: input.document ?? null,
|
||||
document: document.value,
|
||||
name,
|
||||
procedure,
|
||||
comment,
|
||||
|
|
@ -128,6 +130,28 @@ export const inspectAddressContext = async (
|
|||
});
|
||||
};
|
||||
|
||||
const resolveDocument = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
document: string | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<Result<string, AnalysisError>> => {
|
||||
if (document !== undefined) return ok(document);
|
||||
const current = await analysis.execute(
|
||||
"current_document",
|
||||
{},
|
||||
executionOptions(signal),
|
||||
);
|
||||
if (!current.ok) return current;
|
||||
return typeof current.value.result === "string"
|
||||
? ok(current.value.result)
|
||||
: err(
|
||||
new AnalysisOutputError(
|
||||
"current_document",
|
||||
"expected a document identity string",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const facet = (
|
||||
result: Result<AnalysisExecution, AnalysisError> | undefined,
|
||||
operation: AnalysisOperation,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
|
|||
],
|
||||
};
|
||||
|
||||
const SESSION_COMPOSITION_REQUIREMENTS: Readonly<
|
||||
Record<string, readonly string[]>
|
||||
> = {
|
||||
get_navigation_context: [
|
||||
"current_document",
|
||||
"current_address",
|
||||
"resolve_containing_procedure",
|
||||
],
|
||||
};
|
||||
|
||||
/** Build stable per-operation availability for discovery and tool visibility. */
|
||||
export const buildCapabilityInventory = (
|
||||
sessionStatus: JsonValue,
|
||||
|
|
@ -185,6 +195,9 @@ const clientRequirementsFor = (
|
|||
};
|
||||
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
const sessionComposition = SESSION_COMPOSITION_REQUIREMENTS[context.name];
|
||||
if (sessionComposition !== undefined)
|
||||
return composedAvailabilityFor(sessionComposition, context.descriptors);
|
||||
const javascriptApplication = javascriptApplicationAvailability(context);
|
||||
if (javascriptApplication !== null) return javascriptApplication;
|
||||
const policyDecision = policyAvailability(context);
|
||||
|
|
@ -366,6 +379,13 @@ const composedAvailability = (
|
|||
reason: "provider_missing",
|
||||
remediation: "No provider composition is declared for this operation.",
|
||||
};
|
||||
return composedAvailabilityFor(requirements, descriptors);
|
||||
};
|
||||
|
||||
const composedAvailabilityFor = (
|
||||
requirements: readonly string[],
|
||||
descriptors: ReadonlyMap<string, ProviderDescriptor>,
|
||||
): Availability => {
|
||||
const missing = requirements.find((operation) => !descriptors.has(operation));
|
||||
if (missing !== undefined)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
|||
),
|
||||
session(
|
||||
"list_unknowns",
|
||||
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
|
||||
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. Pages default to 100 items; while has_more is true, pass next_offset as offset and continue until false. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
|
||||
listUnknownsInputSchema,
|
||||
),
|
||||
session(
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -302,6 +302,12 @@ export const registerUnknownTools = ({
|
|||
);
|
||||
if (!parsed.ok) return toCallToolResult(parsed, updateContract);
|
||||
const result = session.updateUnknown(parsed.value);
|
||||
if (result.ok)
|
||||
void server.server
|
||||
.sendResourceUpdated({
|
||||
uri: `rea://unknown/${result.value.unknown_id}`,
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return result.ok
|
||||
? toCallToolResult(result, updateContract, {
|
||||
resourceLinks: [
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ describe("analysis context queries", () => {
|
|||
const bookmarkInputs: Readonly<Record<string, unknown>>[] = [];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation, input) => {
|
||||
if (operation === "current_document")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution("fixture", provider)),
|
||||
);
|
||||
if (operation === "comment")
|
||||
return Promise.resolve(
|
||||
err(new AnalysisProtocolError("comments unavailable")),
|
||||
|
|
@ -145,6 +149,7 @@ describe("analysis context queries", () => {
|
|||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
document: "fixture",
|
||||
name: { state: "available", value: "entry" },
|
||||
comment: {
|
||||
state: "unavailable",
|
||||
|
|
@ -156,7 +161,7 @@ describe("analysis context queries", () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(bookmarkInputs).toEqual([{}]);
|
||||
expect(bookmarkInputs).toEqual([{ document: "fixture" }]);
|
||||
});
|
||||
|
||||
it("propagates cancellation instead of degrading it to an unavailable facet", async () => {
|
||||
|
|
@ -171,7 +176,10 @@ describe("analysis context queries", () => {
|
|||
};
|
||||
|
||||
await expect(
|
||||
inspectAddressContext(analysis, { address: "0x401000" }),
|
||||
inspectAddressContext(analysis, {
|
||||
address: "0x401000",
|
||||
document: "fixture",
|
||||
}),
|
||||
).resolves.toEqual(err(cancelled));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -194,4 +194,36 @@ describe("capability inventory", () => {
|
|||
missing_optional: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("advertises navigation context only when every cursor operation is available", () => {
|
||||
const capabilities = [
|
||||
"current_document",
|
||||
"current_address",
|
||||
"resolve_containing_procedure",
|
||||
].map((operation) => ({
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
}));
|
||||
expect(
|
||||
entry(
|
||||
"get_navigation_context",
|
||||
status({ open: true, kind: "executable", capabilities }),
|
||||
),
|
||||
).toMatchObject({ available: true, reason: "available" });
|
||||
expect(
|
||||
entry(
|
||||
"get_navigation_context",
|
||||
status({
|
||||
open: true,
|
||||
kind: "executable",
|
||||
capabilities: capabilities.slice(0, 2),
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_missing",
|
||||
remediation: expect.stringContaining("resolve_containing_procedure"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -136,9 +136,14 @@ describe("target-free MCP lifecycle", () => {
|
|||
});
|
||||
const mcp = new Client({ name: "session-test", version: "1.0.0" });
|
||||
let resourceListChanges = 0;
|
||||
let unknownResourceUpdates = 0;
|
||||
mcp.setNotificationHandler("notifications/resources/list_changed", () => {
|
||||
resourceListChanges += 1;
|
||||
});
|
||||
mcp.setNotificationHandler("notifications/resources/updated", (notice) => {
|
||||
if (notice.params.uri.startsWith("rea://unknown/"))
|
||||
unknownResourceUpdates += 1;
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
resources.push(mcp, server);
|
||||
|
|
@ -297,6 +302,7 @@ describe("target-free MCP lifecycle", () => {
|
|||
});
|
||||
expect(resolved.isError).not.toBe(true);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(unknownResourceUpdates).toBe(1);
|
||||
expect(resourceListChanges).toBe(changesBeforeMutation);
|
||||
expect(
|
||||
structured(
|
||||
|
|
|
|||
Loading…
Reference in New Issue