Skip to content
Draft
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
30 changes: 20 additions & 10 deletions src/codex/prompt-text-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const LAYER_SECTION_TAGS: Record<string, string> = {
skills: "skills_instructions",
apps: "apps_instructions",
plugins: "plugins_instructions",
// Context-dependent: it is absent when the active collaboration mode adds no
// instructions, but Codex wraps it in this tag when it does render.
collaboration: "collaboration_mode",
environment: "environment_context",
permissions: "permissions instructions",
// Synthetic: the project doc carries no tag of its own (see extractSections).
Expand All @@ -59,7 +62,6 @@ const UNMAPPED_LAYER_IDS = [
// truth is that this extractor has no verified tag for them.
"personality",
"realtime",
"collaboration",
// The Rust source names a <git_attribution> marker pair, but a world-state section is
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
Expand Down Expand Up @@ -176,6 +178,22 @@ function extractSections(raw: string): Map<string, string> {
/** Test seam: the extraction is the part worth pinning, not the spawn. */
export const extractSectionsForTests = extractSections;

function mapSectionsToLayers(sections: Map<string, string>): Record<string, LayerText> {
const layers: Record<string, LayerText> = {};
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
const text = sections.get(tag) ?? null;
layers[layerId] = text === null
// Registered but not rendered on this turn, which is an ordinary state for
// a diff-rendered section rather than an error.
? { text: null, reason: "not-rendered", bytes: 0 }
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
}
return layers;
}

/** Test seam: pin section-to-layer projection independently of the subprocess. */
export const mapSectionsToLayersForTests = mapSectionsToLayers;

/**
* Probe once and map every known layer to its rendered text.
*
Expand All @@ -202,15 +220,7 @@ export async function probePromptText(timeoutMs = 15_000): Promise<PromptTextPro
// is a failed read - not fifteen layers that each chose to send nothing.
return { ok: false, codexHome, layers: {}, detail: "prompt output could not be parsed" };
}
const layers: Record<string, LayerText> = {};
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
const text = sections.get(tag) ?? null;
layers[layerId] = text === null
// Registered but not rendered on this turn, which is an ordinary state for
// a diff-rendered section rather than an error.
? { text: null, reason: "not-rendered", bytes: 0 }
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
}
const layers = mapSectionsToLayers(sections);

// A file that exists and is empty is not the same as a layer that chose to send
// nothing. Reporting "sent nothing" for an empty AGENTS.md tells the user their
Expand Down
22 changes: 21 additions & 1 deletion tests/codex-prompt-text-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* that attribution to a user as an explanation.
*/
import { describe, expect, test } from "bun:test";
import { extractSectionsForTests } from "../src/codex/prompt-text-probe";
import {
extractSectionsForTests,
mapSectionsToLayersForTests,
} from "../src/codex/prompt-text-probe";

function message(text: string): string {
return JSON.stringify([{ type: "message", role: "developer", content: [{ type: "input_text", text }] }]);
Expand Down Expand Up @@ -41,6 +44,23 @@ describe("section extraction", () => {
expect(sections.get("apps_instructions")).toBe("line one\nline two");
});

test("context-dependent collaboration text maps to its prompt layer", () => {
const rendered = extractSectionsForTests(
message("<collaboration_mode>Pair-programming instructions.</collaboration_mode>"),
);
expect(mapSectionsToLayersForTests(rendered).collaboration).toEqual({
text: "Pair-programming instructions.",
reason: "ok",
bytes: 30,
});

expect(mapSectionsToLayersForTests(new Map()).collaboration).toEqual({
text: null,
reason: "not-rendered",
bytes: 0,
});
});

test("AGENTS.md is bounded by its own INSTRUCTIONS wrapper", () => {
// Capturing to end-of-message swept up whatever untagged prose followed. The
// body is delimited, so the delimiter is the boundary.
Expand Down
Loading