Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions packages/pi-plugin/src/tools/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
A1_HASH_BASELINE_HEADING,
A1_TOOL_SECTION_HEADING,
a1GoldenSectionOffset,
readA1GoldenDocument,
} from "@magic-context/core/shared/prompt-surface-a1-golden";
import {
createPromptSurfaceRuntime,
LIGHT_TOOL_DESCRIPTIONS,
Expand Down Expand Up @@ -240,16 +244,10 @@ function readA1GoldenTools(): Record<
string,
{ description: string; parameters: Record<string, unknown> }
> {
const document = readFileSync(
join(
import.meta.dir,
"../../../plugin/src/shared/prompt-surface-a1-golden.md",
),
"utf8",
);
const document = readA1GoldenDocument();
const toolSection = document.slice(
document.indexOf("## 2. Tool surface"),
document.indexOf("## 3. System-prompt hash baseline"),
a1GoldenSectionOffset(document, A1_TOOL_SECTION_HEADING),
a1GoldenSectionOffset(document, A1_HASH_BASELINE_HEADING),
);
const headings = [...toolSection.matchAll(/^### (ctx_[a-z_]+) —.*$/gm)];
return Object.fromEntries(
Expand Down
27 changes: 27 additions & 0 deletions packages/plugin/scripts/export-agent-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Usage: bun packages/plugin/scripts/export-agent-surface.ts [outPath]
* Default out: .alfonso/agent-surface-export.md (repo root)
*/
import { createHash } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import Tokenizer from "ai-tokenizer";
Expand Down Expand Up @@ -112,6 +113,32 @@ for (const [name, definition] of Object.entries(definitions)) {
out.push("");
}

// ── Section 3: system-prompt hash baseline ───────────────────────────────────
// Derived, not transcribed: the hash handler persists the MD5 of
// `output.system.join("\n")`, and in the no-host-prefix baseline each guidance
// section IS the whole system array — so the hash is just the MD5 of the
// guidance bytes emitted in section 1.
out.push("## 3. System-prompt hash baseline");
out.push("");
out.push(
'The hash handler persists the MD5 of `output.system.join("\\\\n")`. The values below use each captured guidance section as the complete system array, which is the deterministic no-host-prefix baseline for later compatibility tests. The guidance bytes above are the source bytes; this table records the hash bytes that must remain unchanged when the default prompt surface is selected.',
);
out.push("");
out.push("| Variant | Guidance bytes | MD5 system-prompt hash |");
out.push("|---|---:|---|");
for (const [label, text] of variants) {
// Table labels are the short form ("PRIMARY full"), not the full heading.
const shortLabel = label.split(" (")[0];
const bytes = Buffer.byteLength(text, "utf8");
const md5 = createHash("md5").update(text).digest("hex");
out.push(`| ${shortLabel} | ${bytes} | \`${md5}\` |`);
}
out.push("");
out.push(
'The OpenCode and Pi runtime compatibility tests consume this snapshot for omitted `prompt_surface` and explicit `{ default: "full" }`: both assert guidance, registered tool descriptions, tool IDs, and hashes; OpenCode also asserts these parameter schemas directly, while Pi asserts its TypeBox-owned schemas stay byte-identical across both config forms.',
);
out.push("");

const outPath = resolve(process.argv[2] ?? "../../.alfonso/agent-surface-export.md");
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, out.join("\n"));
Expand Down
17 changes: 10 additions & 7 deletions packages/plugin/src/plugin/tool-registry.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
/// <reference types="bun-types" />

import { afterEach, describe, expect, it } from "bun:test";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { type ToolDefinition, tool } from "@opencode-ai/plugin";
import type { MagicContextPluginConfig } from "../config";
import { closeDatabase, openDatabase } from "../features/magic-context/storage";
import { resetCtxReduceRegisteredGloballyForTest } from "../hooks/magic-context/ctx-reduce-availability";
import {
A1_HASH_BASELINE_HEADING,
A1_TOOL_SECTION_HEADING,
a1GoldenSectionOffset,
readA1GoldenDocument,
} from "../shared/prompt-surface-a1-golden";
import type { PromptSurfaceRuntime } from "../shared/prompt-surface-runtime";
import {
createPromptSurfaceRuntime,
Expand Down Expand Up @@ -204,13 +210,10 @@ describe("createToolRegistry — compaction-off mode (#266 S4)", () => {
type GoldenTool = { description: string; parameters: Record<string, unknown> };

function readA1GoldenTools(): Record<string, GoldenTool> {
const document = readFileSync(
join(import.meta.dir, "../shared/prompt-surface-a1-golden.md"),
"utf8",
);
const document = readA1GoldenDocument();
const toolSection = document.slice(
document.indexOf("## 2. Tool surface"),
document.indexOf("## 3. System-prompt hash baseline"),
a1GoldenSectionOffset(document, A1_TOOL_SECTION_HEADING),
a1GoldenSectionOffset(document, A1_HASH_BASELINE_HEADING),
);
const headings = [...toolSection.matchAll(/^### (ctx_[a-z_]+) —.*$/gm)];
return Object.fromEntries(
Expand Down
41 changes: 41 additions & 0 deletions packages/plugin/src/shared/prompt-surface-a1-golden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";

/**
* Shared accessors for the A1 prompt-surface golden.
*
* The golden (`prompt-surface-a1-golden.md`, generated by
* `scripts/export-agent-surface.ts`) is read by tests in BOTH packages, and its
* section headings are the contract between the generator and those readers.
* Keeping the path, the heading strings, and the offset guard here means a
* change to the golden's section format is a one-file edit rather than a hunt
* across packages.
*
* This module resolves the golden relative to ITSELF, so consumers never carry
* their own `../..` path arithmetic — that arithmetic differs per package and is
* exactly the kind of thing that rots silently when files move.
*/

export const A1_TOOL_SECTION_HEADING = "## 2. Tool surface";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The A1_TOOL_SECTION_HEADING constant doesn't mirror the actual golden heading — the file declares ## 2. Tool surface (description + parameters as serialized to the provider), while the constant only carries the ## 2. Tool surface prefix. It works only because indexOf does a prefix/substring match, which contradicts the module's own stated contract (that these heading strings make a golden format change a one-file edit). If the golden ever gains a section whose heading starts with ## 2. Tool surface, the offset guard would silently resolve to the wrong section. Suggest storing the exact on-disk heading so the constant truly reflects the contract it documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/shared/prompt-surface-a1-golden.ts, line 19:

<comment>The `A1_TOOL_SECTION_HEADING` constant doesn't mirror the actual golden heading — the file declares `## 2. Tool surface (description + parameters as serialized to the provider)`, while the constant only carries the `## 2. Tool surface` prefix. It works only because `indexOf` does a prefix/substring match, which contradicts the module's own stated contract (that these heading strings make a golden format change a one-file edit). If the golden ever gains a section whose heading starts with `## 2. Tool surface`, the offset guard would silently resolve to the wrong section. Suggest storing the exact on-disk heading so the constant truly reflects the contract it documents.</comment>

<file context>
@@ -0,0 +1,41 @@
+ * exactly the kind of thing that rots silently when files move.
+ */
+
+export const A1_TOOL_SECTION_HEADING = "## 2. Tool surface";
+export const A1_HASH_BASELINE_HEADING = "## 3. System-prompt hash baseline";
+
</file context>

export const A1_HASH_BASELINE_HEADING = "## 3. System-prompt hash baseline";

export function readA1GoldenDocument(): string {
return readFileSync(join(import.meta.dir, "prompt-surface-a1-golden.md"), "utf8");
}

/**
* Locate a golden section heading, refusing to continue when it is absent.
*
* A bare `indexOf` returns -1 for a missing heading, and `slice(start, -1)`
* silently yields an empty (or truncated) section rather than failing: the
* golden then parses as ZERO tools and the consuming test passes by comparing
* empty to empty. Failing loudly here keeps a malformed golden from reading as a
* clean run.
*/
export function a1GoldenSectionOffset(document: string, heading: string): number {
const offset = document.indexOf(heading);
if (offset === -1) {
throw new Error(`A1 golden is missing the "${heading}" section heading`);
}
return offset;
}
Loading