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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ Upstream: t3code 0.0.31
- Selected text stays visible on a file line opened from a link (#13548 by Theo Browne)
- When browser preview tools fail, agents are told what to try instead (#13559 by Theo Browne)
- Browser preview snapshots stay small enough for agents to read in full (#13558 by Theo Browne)
- Running threads keep syncing in the background, so opening one is instant (#13554 by Theo Browne)
- Run a one-line shell command from a reply in the thread's terminal (#13060 by Bilal Bakr)
- Newer Codex models get Avi Code's instructions again, even after compaction (#13547 by Theo Browne)
- Opening a running thread no longer flashes the sync status (#13551 by Theo Browne)
- Back and Escape from settings or usage return to the main app (#13516 by Theo Browne)
- New worktree threads no longer say Local checkout during setup (#13590 by Theo Browne)
- Running terminal icons in the sidebar pulse in sync (#12962 by Theo Browne)
- Background git checks no longer fill your disk with failed repack files (#13812 by Theo Browne)
- The database log file shrinks back after big writes (#13684 by Theo Browne)
- A database query that failed once is retried right away (#10584 by Aditya Garud)
Expand Down
49 changes: 32 additions & 17 deletions apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { ProviderInteractionMode } from "@t3tools/contracts";
import type { V2TurnStartParams__AdditionalContextEntry } from "effect-codex-app-server/schema";

const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `

## Avi Code collaborative browser
const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `## Avi Code collaborative browser

You are running inside Avi Code. The \`t3-code\` MCP server is the product-native collaborative browser shared with the user. When it exposes \`preview_*\` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.

For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.

Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.
`;
Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.`;

export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational)

Expand Down Expand Up @@ -131,7 +129,6 @@ plan content should be human and agent digestible. The final plan must be plan-o
Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a \`<proposed_plan>\` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.

Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec.
${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}
</collaboration_mode>`;

export const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Collaboration Mode: Default
Expand All @@ -145,7 +142,6 @@ Your active mode changes only when new developer instructions with a different \
The \`request_user_input\` tool is unavailable in Default mode. If you call it while in Default mode, it will return an error.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}
</collaboration_mode>`;

export interface CodexRuntimeInfo {
Expand All @@ -158,15 +154,34 @@ function toSingleLine(value: string): string {
return value.replaceAll(/\s+/g, " ").trim();
}

export function buildCodexDeveloperInstructions(
interactionMode: ProviderInteractionMode,
/** Mode prompt for `turn/start.collaborationMode.settings.developer_instructions`. */
export function buildCodexDeveloperInstructions(interactionMode: ProviderInteractionMode): string {
return interactionMode === "plan"
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
}

/**
* Avi's context for `turn/start.additionalContext`, sent with every turn on
* every model. Codex renders each entry as a `<key>value</key>` developer
* message and resends it only when the value changes.
*
* This must stay out of the collaboration mode: when the model catalog ships
* its own text for a mode, as newer models do, Codex uses that text and drops
* the client's `developer_instructions` entirely. Older models still read the
* mode prompt there, and read this context the same way newer ones do.
*
* Ported from upstream #13547.
*/
export function buildCodexAdditionalContext(
runtime: CodexRuntimeInfo,
): string {
const base =
interactionMode === "plan"
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
return `${base}

<runtime_info>In case you're asked: you are running in Avi Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`;
): Record<string, V2TurnStartParams__AdditionalContextEntry> {
// Separate keys keep each value under Codex's per-entry token cap.
return {
avi_code_runtime: {
kind: "application",
value: `<runtime_info>In case you're asked: you are running in Avi Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`,
},
avi_code_tools: { kind: "application", value: T3_CODE_BROWSER_TOOL_INSTRUCTIONS },
};
}
193 changes: 141 additions & 52 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import * as CodexErrors from "effect-codex-app-server/errors";
import * as CodexRpc from "effect-codex-app-server/rpc";

import {
buildCodexAdditionalContext,
buildCodexDeveloperInstructions,
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
} from "../CodexDeveloperInstructions.ts";
import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";
import {
buildAdditionalContextInjection,
buildTurnStartParams,
hasConfiguredMcpServer,
isRootContextCompaction,
isRecoverableThreadResumeError,
openCodexThread,
readCodexThread,
Expand Down Expand Up @@ -285,12 +288,13 @@ describe("buildTurnStartParams", () => {
settings: {
model: "gpt-5.3-codex",
reasoning_effort: "medium",
developer_instructions: buildCodexDeveloperInstructions("plan", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
}),
developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
},
},
additionalContext: buildCodexAdditionalContext({
model: "gpt-5.3-codex",
reasoningEffort: "medium",
}),
});
});

Expand Down Expand Up @@ -334,16 +338,17 @@ describe("buildTurnStartParams", () => {
settings: {
model: "gpt-5.3-codex",
reasoning_effort: "medium",
developer_instructions: buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
}),
developer_instructions: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
},
},
additionalContext: buildCodexAdditionalContext({
model: "gpt-5.3-codex",
reasoningEffort: "medium",
}),
});
});

it("reports the same fallback model and effort in settings and instructions", () => {
it("reports the same fallback model and effort in settings and runtime context", () => {
const params = Effect.runSync(
buildTurnStartParams({
threadId: "provider-thread-1",
Expand All @@ -356,9 +361,39 @@ describe("buildTurnStartParams", () => {
const settings = params.collaborationMode?.settings;
NodeAssert.equal(settings?.model, DEFAULT_MODEL);
NodeAssert.equal(settings?.reasoning_effort, "medium");
NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));
NodeAssert.ok(
params.additionalContext?.avi_code_runtime?.value.includes(`as ${DEFAULT_MODEL} with medium`),
);
});

// Newer models replace `developer_instructions` with the catalog's own mode
// text, so Avi's runtime and tool context must ride in `additionalContext`,
// which every model receives.
it.effect("keeps runtime and browser context out of the mode prompt", () =>
Effect.gen(function* () {
for (const interactionMode of ["default", "plan"] as const) {
const params = yield* buildTurnStartParams({
threadId: "provider-thread-1",
runtimeMode: "full-access",
prompt: "Go",
model: "gpt-5.4",
effort: "high",
interactionMode,
});
const instructions = params.collaborationMode?.settings.developer_instructions ?? "";
NodeAssert.doesNotMatch(instructions, /runtime_info|preview_status/);
NodeAssert.deepStrictEqual(Object.keys(params.additionalContext ?? {}), [
"avi_code_runtime",
"avi_code_tools",
]);
NodeAssert.match(
params.additionalContext?.avi_code_runtime?.value ?? "",
/as gpt-5\.4 with high reasoning effort/,
);
}
}),
);

it.effect("routes approvals to the auto reviewer in auto mode", () =>
Effect.gen(function* () {
const params = yield* buildTurnStartParams({
Expand Down Expand Up @@ -411,64 +446,118 @@ describe("buildTurnStartParams", () => {
});

describe("buildCodexDeveloperInstructions", () => {
it("appends runtime info after the mode instructions", () => {
const instructions = buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "high",
});

NodeAssert.ok(instructions.startsWith(CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS));
NodeAssert.match(instructions, /Avi Code/);
NodeAssert.match(instructions, /Codex harness/);
NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/);
it("returns the mode prompt for each interaction mode", () => {
NodeAssert.equal(
buildCodexDeveloperInstructions("default"),
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
);
NodeAssert.equal(
buildCodexDeveloperInstructions("plan"),
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
);
});
});

it("includes runtime info alongside plan mode instructions", () => {
const instructions = buildCodexDeveloperInstructions("plan", {
describe("buildCodexAdditionalContext", () => {
const runtimeText = (runtime: { model: string; reasoningEffort: string }) =>
buildCodexAdditionalContext(runtime).avi_code_runtime?.value ?? "";

it("describes the Avi Code runtime", () => {
const context = buildCodexAdditionalContext({
model: "gpt-5.3-codex",
reasoningEffort: "medium",
reasoningEffort: "high",
});

NodeAssert.ok(instructions.startsWith(CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS));
NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/);
NodeAssert.equal(context.avi_code_runtime?.kind, "application");
NodeAssert.match(context.avi_code_runtime?.value ?? "", /Avi Code/);
NodeAssert.match(context.avi_code_runtime?.value ?? "", /Codex harness/);
NodeAssert.match(
context.avi_code_runtime?.value ?? "",
/as gpt-5\.3-codex with high reasoning effort/,
);
});

it("varies with the model and effort of each turn", () => {
const first = buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
});
const second = buildCodexDeveloperInstructions("default", {
model: "gpt-5.4",
reasoningEffort: "high",
});

NodeAssert.notEqual(first, second);
NodeAssert.notEqual(
runtimeText({ model: "gpt-5.3-codex", reasoningEffort: "medium" }),
runtimeText({ model: "gpt-5.4", reasoningEffort: "high" }),
);
});

it("flattens multiline metadata into single-line runtime info", () => {
const instructions = buildCodexDeveloperInstructions("default", {
model: "gpt\n5.3\ncodex",
reasoningEffort: " high\neffort ",
});
const text = runtimeText({ model: "gpt\n5.3\ncodex", reasoningEffort: " high\neffort " });

NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/);
NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/);
NodeAssert.match(text, /as gpt 5\.3 codex with high effort reasoning effort/);
NodeAssert.doesNotMatch(text, /<runtime_info>[^<]*\n/);
});

it("prefers the product-native preview tools", () => {
const tools =
buildCodexAdditionalContext({ model: "gpt-5.4", reasoningEffort: "medium" }).avi_code_tools
?.value ?? "";
NodeAssert.match(tools, /t3-code/);
NodeAssert.match(tools, /preview_status/);
NodeAssert.match(tools, /preview_open/);
NodeAssert.match(tools, /Do not switch to global browser skills/);
});
});

describe("T3 browser developer instructions", () => {
it("prefers the product-native preview tools in both collaboration modes", () => {
for (const instructions of [
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
]) {
NodeAssert.match(instructions, /t3-code/);
NodeAssert.match(instructions, /preview_status/);
NodeAssert.match(instructions, /preview_open/);
NodeAssert.match(instructions, /Do not switch to global browser skills/);
}
describe("additional context after compaction", () => {
const compacted = (threadId: string) =>
({
method: "item/completed",
params: {
threadId,
turnId: `${threadId}-turn`,
completedAtMs: 0,
item: { type: "contextCompaction", id: `compaction-${threadId}` },
},
}) as unknown as Parameters<typeof isRootContextCompaction>[0];

it("reacts only to the root thread's own compaction", () => {
NodeAssert.equal(isRootContextCompaction(compacted("root"), "root"), true);
// A child agent's compaction must not inject into the root thread.
NodeAssert.equal(isRootContextCompaction(compacted("child"), "root"), false);
NodeAssert.equal(isRootContextCompaction(compacted("root"), undefined), false);
const otherItem = {
method: "item/completed",
params: { threadId: "root", turnId: "t", item: { type: "agentMessage", id: "m" } },
} as unknown as Parameters<typeof isRootContextCompaction>[0];
NodeAssert.equal(isRootContextCompaction(otherItem, "root"), false);
});

it.effect("re-sends the last turn's context as developer messages", () =>
Effect.gen(function* () {
const params = yield* buildTurnStartParams({
threadId: "root",
runtimeMode: "full-access",
prompt: "Go",
model: "gpt-5.4",
interactionMode: "default",
});
NodeAssert.ok(params.additionalContext);
const injection = buildAdditionalContextInjection("root", params.additionalContext);

NodeAssert.equal(injection.threadId, "root");
const items = injection.items as ReadonlyArray<{
type: string;
role: string;
content: ReadonlyArray<{ type: string; text: string }>;
}>;
NodeAssert.deepStrictEqual(
items.map((item) => [item.type, item.role, item.content[0]?.type]),
[
["message", "developer", "input_text"],
["message", "developer", "input_text"],
],
);
NodeAssert.match(
items[0]?.content[0]?.text ?? "",
/^<avi_code_runtime><runtime_info>.*as gpt-5\.4 with medium reasoning effort.*<\/avi_code_runtime>$/s,
);
NodeAssert.match(items[1]?.content[0]?.text ?? "", /^<avi_code_tools>## Avi Code/);
}),
);
});

describe("hasConfiguredMcpServer", () => {
Expand Down
Loading
Loading