Skip to content
Open
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
26 changes: 22 additions & 4 deletions .claude/skills/run-codex/scripts/run-codex-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ import {CodexAcpServer} from "../../../../src/CodexAcpServer";
import type {AgentSideConnection} from "@agentclientprotocol/sdk";

// Parse command line arguments
function parseArgs(): { prompt: string; cwd: string; output: string; json: boolean } {
function parseArgs(): {
prompt: string;
cwd: string;
output: string;
json: boolean;
systemPromptAppend?: string;
} {
const args = process.argv.slice(2);
let prompt = "";
let cwd = process.cwd();
let output = "all";
let json = false;
let systemPromptAppend: string | undefined;

for (let i = 0; i < args.length; i++) {
const arg = args[i];
Expand All @@ -34,6 +41,8 @@ function parseArgs(): { prompt: string; cwd: string; output: string; json: boole
output = args[++i] || "all";
} else if (arg === "--json") {
json = true;
} else if (arg === "--system-prompt-append") {
systemPromptAppend = args[++i] || "";
} else if (arg === "--help" || arg === "-h") {
console.log(`
Usage: npm run codex-test -- [options]
Expand All @@ -43,6 +52,8 @@ Options:
-c, --cwd <path> Working directory for the session (default: current dir)
-o, --output <type> Output type: all, codex, acp, summary (default: all)
--json Output events as JSON
--system-prompt-append <text>
Append session-scoped developer instructions
-h, --help Show this help message

Examples:
Expand All @@ -59,7 +70,7 @@ Examples:
process.exit(1);
}

return { prompt, cwd, output, json };
return { prompt, cwd, output, json, systemPromptAppend };
}

type MethodCallEvent = { method: string; args: unknown[] };
Expand All @@ -76,7 +87,7 @@ function createMockAcpConnection(events: MethodCallEvent[]): AgentSideConnection
}

async function main() {
const { prompt, cwd, output, json } = parseArgs();
const { prompt, cwd, output, json, systemPromptAppend } = parseArgs();

// Find Codex binary
const pathToCodex = path.resolve(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "codex.cmd" : "codex");
Expand All @@ -91,6 +102,7 @@ async function main() {
console.log(`Prompt: ${prompt}`);
console.log(`CWD: ${cwd}`);
console.log(`Output: ${output}`);
console.log(`System prompt append: ${systemPromptAppend?.trim() ? "configured" : "none"}`);
console.log("=".repeat(60));
console.log("");

Expand Down Expand Up @@ -145,7 +157,13 @@ async function main() {

// Create session
console.log("\n--- Creating Session ---\n");
const sessionResponse = await codexAcpAgent.newSession({ cwd, mcpServers: [] });
const sessionResponse = await codexAcpAgent.newSession({
cwd,
mcpServers: [],
...(systemPromptAppend && {
_meta: {systemPrompt: {append: systemPromptAppend}},
}),
});
console.log(`Session ID: ${sessionResponse.sessionId}`);
console.log(`Model: ${sessionResponse.models?.currentModelId}`);

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol]
- Shell command, file change, [permission request](docs/permission-extension.md), MCP tool call, terminal output, reasoning, plan, web search, image generation, image view, token usage, and review events.
- [Native ACP subagent sessions](docs/subagent-sessions.md) (after capability negotiation) with separate child histories and root-routed permissions; a legacy tool-call fallback otherwise.
- Session-scoped long-running goals through the provider-neutral [goal extension](docs/goal-extension.md).
- Client-provided, session-scoped instructions through the [system prompt append extension](docs/system-prompt-extension.md), mapped to Codex developer instructions without replacing its base prompt.
- A per-turn [agent file-change report](docs/agent-file-change-report.md) after capability negotiation.
- Client-provided MCP servers over command-based stdio config and HTTP transport.
- Slash commands: `/status`, `/mcp`, `/skills`, `/goal`, `/review`, `/review-branch`, `/review-commit`, `/compact`, and `/logout`, as well as configured skills.
Expand Down
46 changes: 46 additions & 0 deletions docs/system-prompt-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# System prompt append extension

`codex-acp` supports appending client-owned, session-scoped instructions without replacing Codex's base/system prompt. The adapter maps the appended text to Codex `developerInstructions`, which is injected as a developer-role instruction layer.

## Capability

The adapter advertises the extension in the `initialize` response:

```json
{
"_meta": {
"systemPrompt": {
"version": 1,
"append": true,
"maxBytes": 262144
}
}
}
```

`append: true` is the only supported mode. The adapter does not support replacing Codex's base instructions.

## Session requests

Clients append instructions with `_meta.systemPrompt.append`:

```json
{
"cwd": "/workspace/project",
"mcpServers": [],
"_meta": {
"systemPrompt": {
"append": "Act as a database performance expert for this session."
}
}
}
```

The extension is accepted on `session/new`, `session/resume`, `session/load`, and `session/fork`:

- On `session/new`, the text configures the new Codex thread's developer instructions.
- On `session/resume` and `session/load`, supplied text is reapplied as the thread configuration override. Omitting the field leaves Codex's restored configuration unchanged.
- On `session/fork`, supplied text is applied to the fork. Omitting it leaves instruction inheritance to Codex.
- Ordinary `session/prompt` requests never repeat or modify the session-scoped instructions.

Blank append text is treated as absent. Non-string append values, unsupported fields, string-form `systemPrompt` overrides, and content larger than the advertised UTF-8 byte limit are rejected with `invalid_params` before session side effects.
7 changes: 7 additions & 0 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
import {CodexSubagentSubscriptions} from "./subagents/CodexSubagentSubscriptions";
import {forkSession as runForkSession} from "./SessionFork";
import type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata";
import {readSystemPromptAppend} from "./SystemPrompt";
export type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata";

/**
Expand Down Expand Up @@ -471,6 +472,7 @@ export class CodexAcpClient {
}

async resumeSession(request: acp.ResumeSessionRequest, onSubscribed?: () => void): Promise<SessionMetadata> {
const developerInstructions = readSystemPromptAppend(request._meta);
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
await this.refreshSkills(request.cwd, additionalDirectories);

Expand All @@ -479,6 +481,7 @@ export class CodexAcpClient {
cwd: request.cwd,
modelProvider: await this.getResumeModelProvider(),
threadId: request.sessionId,
...(developerInstructions !== undefined && {developerInstructions}),
});
onSubscribed?.();
const codexModels = await this.fetchAvailableModels();
Expand Down Expand Up @@ -510,6 +513,7 @@ export class CodexAcpClient {
}

async loadSession(request: acp.LoadSessionRequest, onSubscribed?: () => void): Promise<SessionMetadataWithThread> {
const developerInstructions = readSystemPromptAppend(request._meta);
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
await this.refreshSkills(request.cwd, additionalDirectories);

Expand All @@ -518,6 +522,7 @@ export class CodexAcpClient {
cwd: request.cwd,
modelProvider: await this.getResumeModelProvider(),
threadId: request.sessionId,
...(developerInstructions !== undefined && {developerInstructions}),
});
onSubscribed?.();
const historyResponse = await this.codexClient.threadRead({
Expand Down Expand Up @@ -546,13 +551,15 @@ export class CodexAcpClient {
}

async newSession(request: acp.NewSessionRequest): Promise<SessionMetadata> {
const developerInstructions = readSystemPromptAppend(request._meta);
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
await this.refreshSkills(request.cwd, additionalDirectories);

const response = await this.codexClient.threadStart({
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers),
modelProvider: this.getModelProvider(),
cwd: request.cwd,
...(developerInstructions !== undefined && {developerInstructions}),
});

const codexModels = await this.fetchAvailableModels();
Expand Down
6 changes: 6 additions & 0 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {SteeringQueue} from "./SteeringQueue";
import type {QuotaMeta} from "./QuotaMeta";
import {logger} from "./Logger";
import {sanitizeMcpServerName} from "./McpServerName";
import {readSystemPromptAppend, SYSTEM_PROMPT_CAPABILITY} from "./SystemPrompt";
import {createResponseItemHistoryFallbackUpdates} from "./ResponseItemHistoryFallback";
import {
GOAL_CONTROL_ACTIONS,
Expand Down Expand Up @@ -347,6 +348,7 @@ export class CodexAcpServer {
},
authMethods: getCodexAuthMethods(_params.clientCapabilities),
_meta: {
systemPrompt: SYSTEM_PROMPT_CAPABILITY,
steering: {
supported: true,
},
Expand Down Expand Up @@ -710,6 +712,7 @@ export class CodexAcpServer {
}

async loadSession(params: acp.LoadSessionRequest): Promise<LegacyLoadSessionResponse> {
readSystemPromptAppend(params._meta);
if (this.providerUpdate !== null) {
await this.providerUpdate;
}
Expand All @@ -736,6 +739,7 @@ export class CodexAcpServer {
}

async resumeSession(params: acp.ResumeSessionRequest): Promise<LegacyResumeSessionResponse> {
readSystemPromptAppend(params._meta);
if (this.providerUpdate !== null) {
await this.providerUpdate;
}
Expand All @@ -755,6 +759,7 @@ export class CodexAcpServer {
}

async forkSession(params: acp.ForkSessionRequest): Promise<acp.ForkSessionResponse> {
readSystemPromptAppend(params._meta);
if (this.providerUpdate !== null) {
await this.providerUpdate;
}
Expand Down Expand Up @@ -867,6 +872,7 @@ export class CodexAcpServer {
async newSession(
params: acp.NewSessionRequest,
): Promise<LegacyNewSessionResponse> {
readSystemPromptAppend(params._meta);
if (this.providerUpdate !== null) {
await this.providerUpdate;
}
Expand Down
3 changes: 3 additions & 0 deletions src/SessionFork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {ModeKind} from "./app-server/ModeKind";
import type {ServiceTier} from "./app-server/ServiceTier";
import type {Model, ThreadForkParams} from "./app-server/v2";
import type {SessionMetadata} from "./SessionMetadata";
import {readSystemPromptAppend} from "./SystemPrompt";

export type SessionForkDependencies = {
codexClient: CodexAppServerClient;
Expand All @@ -26,6 +27,7 @@ export async function forkSession(
additionalDirectories: string[],
dependencies: SessionForkDependencies,
): Promise<SessionMetadata> {
const developerInstructions = readSystemPromptAppend(request._meta);
await dependencies.refreshSkills(request.cwd, additionalDirectories);
const lastTurnId = await resolveForkTurnId(request, dependencies.codexClient);
const response = await dependencies.codexClient.threadFork({
Expand All @@ -36,6 +38,7 @@ export async function forkSession(
),
cwd: request.cwd,
...(lastTurnId !== undefined && {lastTurnId}),
...(developerInstructions !== undefined && {developerInstructions}),
modelProvider: await dependencies.getResumeModelProvider(),
threadId: request.sessionId,
});
Expand Down
63 changes: 63 additions & 0 deletions src/SystemPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {RequestError} from "@agentclientprotocol/sdk";

export const SYSTEM_PROMPT_EXTENSION_VERSION = 1;
export const SYSTEM_PROMPT_APPEND_MAX_BYTES = 256 * 1024;

export type SystemPromptCapability = {
version: typeof SYSTEM_PROMPT_EXTENSION_VERSION;
append: true;
maxBytes: typeof SYSTEM_PROMPT_APPEND_MAX_BYTES;
};

export const SYSTEM_PROMPT_CAPABILITY: SystemPromptCapability = {
version: SYSTEM_PROMPT_EXTENSION_VERSION,
append: true,
maxBytes: SYSTEM_PROMPT_APPEND_MAX_BYTES,
};

/**
* Reads the provider-neutral system-prompt extension used on ACP session
* lifecycle requests. Codex receives the appended text as developer
* instructions, leaving its base/system instructions unchanged.
*/
export function readSystemPromptAppend(
meta?: Record<string, unknown> | null,
): string | undefined {
const rawSystemPrompt = meta?.["systemPrompt"];
if (rawSystemPrompt === undefined) {
return undefined;
}
if (!isUnknownRecord(rawSystemPrompt)) {
throw RequestError.invalidParams(
undefined,
"systemPrompt must be an object containing an append string",
);
}

const unsupportedKeys = Object.keys(rawSystemPrompt).filter(key => key !== "append");
if (unsupportedKeys.length > 0) {
throw RequestError.invalidParams(
undefined,
`systemPrompt contains unsupported fields: ${unsupportedKeys.join(", ")}`,
);
}

const append = rawSystemPrompt["append"];
if (typeof append !== "string") {
throw RequestError.invalidParams(undefined, "systemPrompt.append must be a string");
}
if (new TextEncoder().encode(append).byteLength > SYSTEM_PROMPT_APPEND_MAX_BYTES) {
throw RequestError.invalidParams(
undefined,
`systemPrompt.append must not exceed ${SYSTEM_PROMPT_APPEND_MAX_BYTES} UTF-8 bytes`,
);
}
if (append.trim().length === 0) {
return undefined;
}
return append;
}

function isUnknownRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
5 changes: 5 additions & 0 deletions src/__tests__/CodexACPAgent/initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ describe('CodexACPAgent - initialize', () => {
},
authMethods: getCodexAuthMethods(),
_meta: {
systemPrompt: {
version: 1,
append: true,
maxBytes: 262144,
},
steering: {
supported: true,
},
Expand Down
Loading