diff --git a/src/domain/investigationWorkspace.ts b/src/domain/investigationWorkspace.ts index 5845f4c09f..4b24601224 100644 --- a/src/domain/investigationWorkspace.ts +++ b/src/domain/investigationWorkspace.ts @@ -398,8 +398,8 @@ const migrateLegacyWorkspace = (input: unknown): InvestigationWorkspace => { const migrated = buildWorkspace({ workspaceId: parsed.workspace_id, name: parsed.name, - revision: parsed.revision + 1, - previousRevisionDigest: parsed.revision_digest, + revision: parsed.revision, + previousRevisionDigest: parsed.previous_revision_digest, bundle, runs: runs.map((run) => ({ ...run, diff --git a/src/server/registerEnhancedTools.ts b/src/server/registerEnhancedTools.ts index d1f2134f65..ce88a0a3e7 100644 --- a/src/server/registerEnhancedTools.ts +++ b/src/server/registerEnhancedTools.ts @@ -116,7 +116,9 @@ const registerEnhancedTool = ( recordUnknown: registration.recordUnknown, }); if (!unknowns.ok) return toCallToolResult(unknowns, contract); - return toCallToolResult({ ok: true, value: evidence }, contract); + return toCallToolResult({ ok: true, value: evidence }, contract, { + evidenceResourcesAvailable: recorded !== undefined, + }); } return toCallToolResult(result, contract); }, diff --git a/src/server/registerEvidenceTools.ts b/src/server/registerEvidenceTools.ts index 9b1ebdf677..f5ef12b0d0 100644 --- a/src/server/registerEvidenceTools.ts +++ b/src/server/registerEvidenceTools.ts @@ -102,7 +102,9 @@ export const registerEvidenceTools = ( const recorded = options.recordEvidence?.(evidence); return recorded !== undefined && !recorded.ok ? toCallToolResult(recorded, contract) - : toCallToolResult({ ok: true, value: evidence }, contract); + : toCallToolResult({ ok: true, value: evidence }, contract, { + evidenceResourcesAvailable: recorded !== undefined, + }); }, ); } diff --git a/src/server/registerOfficialTools.ts b/src/server/registerOfficialTools.ts index 52d8cb0b8e..36696dcddd 100644 --- a/src/server/registerOfficialTools.ts +++ b/src/server/registerOfficialTools.ts @@ -103,7 +103,9 @@ const registerOfficialTool = ( const recorded = registration.recordEvidence?.(evidence); if (recorded !== undefined && !recorded.ok) return toCallToolResult(recorded, contract); - return toCallToolResult({ ok: true, value: evidence }, contract); + return toCallToolResult({ ok: true, value: evidence }, contract, { + evidenceResourcesAvailable: recorded !== undefined, + }); } if ( result.error instanceof AnalysisCapabilityUnavailableError && diff --git a/src/server/toolResult.ts b/src/server/toolResult.ts index f452d916a2..b2644d8b9c 100644 --- a/src/server/toolResult.ts +++ b/src/server/toolResult.ts @@ -9,6 +9,10 @@ import type { Result } from "../domain/result.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { ToolContract } from "../contracts/toolContracts.js"; +interface ToolResultOptions { + readonly evidenceResourcesAvailable?: boolean; +} + /** * Translate an application result into MCP text content. * Error tags and safe messages remain visible while underlying causes, process @@ -17,8 +21,11 @@ import type { ToolContract } from "../contracts/toolContracts.js"; export const toCallToolResult = ( result: Result, contract: ToolContract, + options: ToolResultOptions = {}, ): CallToolResult => - result.ok ? successResult(result.value, contract) : errorResult(result.error); + result.ok + ? successResult(result.value, contract, options) + : errorResult(result.error); const errorResult = (error: AnalysisError): CallToolResult => { const projected = projectAnalysisError(error); @@ -37,6 +44,7 @@ const errorResult = (error: AnalysisError): CallToolResult => { const successResult = ( value: JsonValue, contract: ToolContract, + options: ToolResultOptions, ): CallToolResult => { const candidate = contract.kind === "session" ? { result: value } : value; const parsed = contract.outputSchema.safeParse(candidate); @@ -62,14 +70,22 @@ const successResult = ( return { content: [ { type: "text", text: formatValue(value) }, - ...evidenceResourceLinks(value), + ...evidenceResourceLinks( + value, + contract.kind === "session" || + options.evidenceResourcesAvailable === true, + ), ], structuredContent: candidate, }; }; -const evidenceResourceLinks = (value: JsonValue): CallToolResult["content"] => { +const evidenceResourceLinks = ( + value: JsonValue, + available: boolean, +): CallToolResult["content"] => { if ( + !available || typeof value !== "object" || value === null || Array.isArray(value) || diff --git a/tests/investigationWorkspace.test.ts b/tests/investigationWorkspace.test.ts index cdf1c7625f..ae0992069d 100644 --- a/tests/investigationWorkspace.test.ts +++ b/tests/investigationWorkspace.test.ts @@ -155,8 +155,8 @@ describe("persistent cross-version investigation workspace", () => { }); expect(migrated).toMatchObject({ - revision: current.revision + 1, - previous_revision_digest: legacyDigest, + revision: current.revision, + previous_revision_digest: current.previous_revision_digest, runs: [ { legacy_request_identity: true, @@ -174,9 +174,11 @@ describe("persistent cross-version investigation workspace", () => { throw new TypeError("legacy fixture is not canonical"); await writeFile(input.workspace_path, encodedLegacy, { mode: 0o600 }); await expect( - runCrossVersionInvestigation(input, policy(directory), { - inputRoots: [directory], - }), + runCrossVersionInvestigation( + { ...input, expected_workspace_revision: current.revision }, + policy(directory), + { inputRoots: [directory] }, + ), ).resolves.toMatchObject({ ok: true, value: { reused: true } }); await expect( runCrossVersionInvestigation( diff --git a/tests/officialTools.test.ts b/tests/officialTools.test.ts index 64a024c626..8f614655ed 100644 --- a/tests/officialTools.test.ts +++ b/tests/officialTools.test.ts @@ -164,6 +164,11 @@ describe("official Hopper proxy tools", () => { arguments: VALID_INPUTS[contract.name], }); expect(result.isError).not.toBe(true); + expect(result.content).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "resource_link" }), + ]), + ); } expect(invocations.map(({ name }) => name)).toEqual( diff --git a/tests/toolResult.test.ts b/tests/toolResult.test.ts index 7429738696..0a0de8389e 100644 --- a/tests/toolResult.test.ts +++ b/tests/toolResult.test.ts @@ -61,7 +61,7 @@ describe("tool result projection", () => { }); }); - it("links successful evidence to its session resource URI", () => { + it("links successful evidence only when its session resource exists", () => { const evidence = createEvidence( undefined, { id: "fixture", name: "Fixture", version: "1" }, @@ -79,6 +79,15 @@ describe("tool result projection", () => { expect( toCallToolResult(ok(evidence), evidenceContract).content, + ).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ type: "resource_link" }), + ]), + ); + expect( + toCallToolResult(ok(evidence), evidenceContract, { + evidenceResourcesAvailable: true, + }).content, ).toContainEqual( expect.objectContaining({ type: "resource_link",