| author | |
| committer | |
| log | 6391be2729566a1fe1045ff8617f003580b37bfd |
| tree | ad41c538db15f48d02303b4d9d682915c269df74 |
| parent | ab70c8943f48c08e67e3597f62941c169bc58c16 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>9 files changed, 322 insertions(+), 149 deletions(-)
apps/server/src/provider/CodexDeveloperInstructions.ts+43-33| ... | ... | @@ -1,23 +1,18 @@ |
| 1 | 1 | import type { ProviderInteractionMode } from "@t3tools/contracts"; |
| 2 | import type { V2TurnStartParams__AdditionalContextEntry } from "effect-codex-app-server/schema"; | |
| 2 | 3 | import { buildRuntimeInstructions } from "./RuntimeInstructions.ts"; |
| 3 | 4 | |
| 4 | const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = ` | |
| 5 | ||
| 6 | ## T3 Code collaborative browser | |
| 5 | const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `## T3 Code collaborative browser | |
| 7 | 6 | |
| 8 | 7 | You are running inside T3 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. |
| 9 | 8 | |
| 10 | 9 | 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. |
| 11 | 10 | |
| 12 | 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. | |
| 13 | `; | |
| 14 | ||
| 15 | const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = ` | |
| 11 | 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.`; | |
| 16 | 12 | |
| 17 | ## T3 Code devices | |
| 13 | const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = `## T3 Code devices | |
| 18 | 14 | |
| 19 | The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route. | |
| 20 | `; | |
| 15 | The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, which is on PATH. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Do not call simctl, adb, xcrun, or serve-sim directly while these tools are present. If \`device_list\` reports a platform as unavailable, say so instead of trying another route.`; | |
| 21 | 16 | |
| 22 | 17 | export interface T3CodeToolAvailability { |
| 23 | 18 | readonly browser: boolean; |
| ... | ... | @@ -36,16 +31,17 @@ const normalizeAvailability = ( |
| 36 | 31 | * from Playwright, agent-browser, and raw simctl/adb, so leaving them in would |
| 37 | 32 | * talk it out of the only automation it still has. |
| 38 | 33 | */ |
| 39 | const browserToolInstructions = (availability: boolean | T3CodeToolAvailability): string => { | |
| 34 | const toolInstructions = (availability: boolean | T3CodeToolAvailability): string => { | |
| 40 | 35 | const tools = normalizeAvailability(availability); |
| 41 | return `${tools.browser ? T3_CODE_BROWSER_TOOL_INSTRUCTIONS : ""}${ | |
| 42 | tools.device ? T3_CODE_DEVICE_TOOL_INSTRUCTIONS : "" | |
| 43 | }`; | |
| 36 | return [ | |
| 37 | tools.browser ? T3_CODE_BROWSER_TOOL_INSTRUCTIONS : "", | |
| 38 | tools.device ? T3_CODE_DEVICE_TOOL_INSTRUCTIONS : "", | |
| 39 | ] | |
| 40 | .filter(Boolean) | |
| 41 | .join("\n\n"); | |
| 44 | 42 | }; |
| 45 | 43 | |
| 46 | const codexPlanModeDeveloperInstructions = ( | |
| 47 | browserToolsAvailable: boolean | T3CodeToolAvailability, | |
| 48 | ): string => `<collaboration_mode># Plan Mode (Conversational) | |
| 44 | const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational) | |
| 49 | 45 | |
| 50 | 46 | You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. |
| 51 | 47 | |
| ... | ... | @@ -173,12 +169,9 @@ Do not ask "should I proceed?" in the final output. The user can easily switch o |
| 173 | 169 | Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec. |
| 174 | 170 | |
| 175 | 171 | If the user stays in Plan mode and asks for revisions after a prior \`<proposed_plan>\`, any new \`<proposed_plan>\` must be a complete replacement. If the user indicates that the prior plan is not acceptable but does not provide enough information to produce a complete replacement, address the concern and continue planning without producing a \`<proposed_plan>\` block. If the follow-up neither requires changes nor calls the plan into question (e.g. clarifying question), answer it before the block, then reproduce the prior \`<proposed_plan>\` unchanged. |
| 176 | ${browserToolInstructions(browserToolsAvailable)} | |
| 177 | 172 | </collaboration_mode>`; |
| 178 | 173 | |
| 179 | const codexDefaultModeDeveloperInstructions = ( | |
| 180 | browserToolsAvailable: boolean | T3CodeToolAvailability, | |
| 181 | ): string => `<collaboration_mode># Collaboration Mode: Default | |
| 174 | const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Collaboration Mode: Default | |
| 182 | 175 | |
| 183 | 176 | You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. |
| 184 | 177 | |
| ... | ... | @@ -189,29 +182,46 @@ Your active mode changes only when new developer instructions with a different \ |
| 189 | 182 | Use the \`request_user_input\` tool only when it is listed in the available tools for this turn. |
| 190 | 183 | |
| 191 | 184 | 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. |
| 192 | ${browserToolInstructions(browserToolsAvailable)} | |
| 193 | 185 | </collaboration_mode>`; |
| 194 | 186 | |
| 195 | 187 | export interface CodexRuntimeInfo { |
| 196 | 188 | readonly model: string; |
| 189 | readonly modelName?: string | undefined; | |
| 197 | 190 | readonly reasoningEffort: string; |
| 198 | 191 | } |
| 199 | 192 | |
| 200 | export function buildCodexDeveloperInstructions( | |
| 201 | interactionMode: ProviderInteractionMode, | |
| 193 | /** Mode prompt for `turn/start.collaborationMode.settings.developer_instructions`. */ | |
| 194 | export function buildCodexDeveloperInstructions(interactionMode: ProviderInteractionMode): string { | |
| 195 | return interactionMode === "plan" | |
| 196 | ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS | |
| 197 | : CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS; | |
| 198 | } | |
| 199 | ||
| 200 | /** | |
| 201 | * T3 Code context for `turn/start.additionalContext`. Codex renders each entry | |
| 202 | * as a `<key>value</key>` developer message and resends it only when the value | |
| 203 | * changes. | |
| 204 | * | |
| 205 | * This must stay out of the collaboration mode: when the model catalog ships | |
| 206 | * its own text for a mode, as newer models do, Codex uses that text and drops | |
| 207 | * the client's `developer_instructions` entirely. | |
| 208 | */ | |
| 209 | export function buildCodexAdditionalContext( | |
| 202 | 210 | runtime: CodexRuntimeInfo, |
| 203 | 211 | /** |
| 204 | 212 | * Whether the `t3-code` MCP server is attached to this turn. Callers derive |
| 205 | 213 | * it from the session's actual MCP configuration rather than re-reading the |
| 206 | 214 | * setting, so the prompt cannot claim tools the turn doesn't have. |
| 207 | 215 | */ |
| 208 | browserToolsAvailable: boolean | T3CodeToolAvailability = true, | |
| 209 | ): string { | |
| 210 | const base = | |
| 211 | interactionMode === "plan" | |
| 212 | ? codexPlanModeDeveloperInstructions(browserToolsAvailable) | |
| 213 | : codexDefaultModeDeveloperInstructions(browserToolsAvailable); | |
| 214 | return `${base} | |
| 215 | ||
| 216 | ${buildRuntimeInstructions({ harness: "Codex", ...runtime })}`; | |
| 216 | toolsAvailable: boolean | T3CodeToolAvailability = true, | |
| 217 | ): Record<string, V2TurnStartParams__AdditionalContextEntry> { | |
| 218 | const tools = toolInstructions(toolsAvailable); | |
| 219 | // Separate keys keep each value under Codex's per-entry token cap. | |
| 220 | return { | |
| 221 | t3_code_runtime: { | |
| 222 | kind: "application", | |
| 223 | value: buildRuntimeInstructions({ harness: "Codex", ...runtime }), | |
| 224 | }, | |
| 225 | ...(tools ? { t3_code_tools: { kind: "application", value: tools } } : {}), | |
| 226 | }; | |
| 217 | 227 | } |
apps/server/src/provider/Drivers/CodexDriver.ts+14-17| ... | ... | @@ -172,18 +172,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 172 | 172 | ), |
| 173 | 173 | ); |
| 174 | 174 | |
| 175 | // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error | |
| 176 | // channels at construction time — their failure modes are all on the | |
| 177 | // per-operation closures they return. No `mapError` wrapper is needed | |
| 178 | // here; the registry only has to worry about snapshot-build and | |
| 179 | // spawner-availability failures surfaced from `checkCodexProviderStatus` | |
| 180 | // below. | |
| 181 | const adapter = yield* makeCodexAdapter(effectiveConfig, { | |
| 182 | instanceId, | |
| 183 | environment: processEnv, | |
| 184 | ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), | |
| 185 | }); | |
| 186 | ||
| 187 | 175 | // Build a managed snapshot whose settings never change — mutations come |
| 188 | 176 | // in as instance rebuilds from the registry rather than in-place |
| 189 | 177 | // updates. Pre-provide `ChildProcessSpawner` so the check fits |
| ... | ... | @@ -238,11 +226,20 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 238 | 226 | }), |
| 239 | 227 | ), |
| 240 | 228 | ); |
| 241 | const textGeneration = yield* makeCodexTextGeneration( | |
| 242 | effectiveConfig, | |
| 243 | processEnv, | |
| 244 | snapshot.getSnapshot.pipe(Effect.map((value) => value.models)), | |
| 245 | ); | |
| 229 | const models = snapshot.getSnapshot.pipe(Effect.map((value) => value.models)); | |
| 230 | // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error | |
| 231 | // channels at construction time — their failure modes are all on the | |
| 232 | // per-operation closures they return. No `mapError` wrapper is needed | |
| 233 | // here; the registry only has to worry about snapshot-build and | |
| 234 | // spawner-availability failures surfaced from `checkCodexProviderStatus` | |
| 235 | // above. | |
| 236 | const adapter = yield* makeCodexAdapter(effectiveConfig, { | |
| 237 | instanceId, | |
| 238 | environment: processEnv, | |
| 239 | models, | |
| 240 | ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), | |
| 241 | }); | |
| 242 | const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv, models); | |
| 246 | 243 | const snapshotForCwd = (cwd: string) => |
| 247 | 244 | !effectiveConfig.enabled |
| 248 | 245 | ? snapshot.getSnapshot |
apps/server/src/provider/Layers/CodexAdapter.ts+4| ... | ... | @@ -22,6 +22,7 @@ import { |
| 22 | 22 | type ToolActivityNativeAppReference, |
| 23 | 23 | type ToolActivitySource, |
| 24 | 24 | type ProviderUserInputAnswers, |
| 25 | type ServerProviderModel, | |
| 25 | 26 | RuntimeItemId, |
| 26 | 27 | RuntimeRequestId, |
| 27 | 28 | RuntimeTaskId, |
| ... | ... | @@ -89,6 +90,8 @@ const PROVIDER = ProviderDriverKind.make("codex"); |
| 89 | 90 | export interface CodexAdapterLiveOptions { |
| 90 | 91 | readonly instanceId?: ProviderInstanceId; |
| 91 | 92 | readonly environment?: NodeJS.ProcessEnv; |
| 93 | /** The provider's model list; supplies model display names for runtime info. */ | |
| 94 | readonly models?: Effect.Effect<ReadonlyArray<ServerProviderModel>>; | |
| 92 | 95 | readonly makeRuntime?: ( |
| 93 | 96 | options: CodexSessionRuntimeOptions, |
| 94 | 97 | ) => Effect.Effect< |
| ... | ... | @@ -2277,6 +2280,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2277 | 2280 | providerInstanceId: boundInstanceId, |
| 2278 | 2281 | cwd: input.cwd ?? process.cwd(), |
| 2279 | 2282 | binaryPath: codexConfig.binaryPath, |
| 2283 | ...(options?.models ? { models: options.models } : {}), | |
| 2280 | 2284 | launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment), |
| 2281 | 2285 | ...(options?.environment ? { environment: options.environment } : {}), |
| 2282 | 2286 | ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), |
apps/server/src/provider/Layers/CodexCollabRuntime.integration.test.ts+73| ... | ... | @@ -868,3 +868,76 @@ describe("CodexSessionRuntime collab integration", () => { |
| 868 | 868 | ); |
| 869 | 869 | } |
| 870 | 870 | }); |
| 871 | ||
| 872 | describe("CodexSessionRuntime compaction", () => { | |
| 873 | it.effect("restores T3 context after the root thread compacts", () => | |
| 874 | Effect.gen(function* () { | |
| 875 | const compacted = (threadId: string) => ({ | |
| 876 | method: "item/completed", | |
| 877 | params: { | |
| 878 | threadId, | |
| 879 | turnId: `${threadId}-turn`, | |
| 880 | completedAtMs: 0, | |
| 881 | item: { type: "contextCompaction", id: `compaction-${threadId}` }, | |
| 882 | }, | |
| 883 | }); | |
| 884 | const script = { | |
| 885 | rootThreadId: ROOT, | |
| 886 | recordRequests: true, | |
| 887 | // A child's compaction must not inject into the root thread. | |
| 888 | notifications: [compacted(CHILD_A), compacted(ROOT)], | |
| 889 | }; | |
| 890 | // @effect-diagnostics-next-line preferSchemaOverJson:off | |
| 891 | NodeFS.writeFileSync(scriptPath, JSON.stringify(script), "utf8"); | |
| 892 | NodeFS.rmSync(`${scriptPath}.requests`, { force: true }); | |
| 893 | yield* Effect.addFinalizer(() => | |
| 894 | Effect.sync(() => { | |
| 895 | NodeFS.rmSync(scriptPath, { force: true }); | |
| 896 | NodeFS.rmSync(`${scriptPath}.requests`, { force: true }); | |
| 897 | }), | |
| 898 | ); | |
| 899 | ||
| 900 | const runtime = yield* makeCodexSessionRuntime({ | |
| 901 | threadId: ThreadId.make("thread-compaction-context"), | |
| 902 | binaryPath: peerPath, | |
| 903 | cwd: NodeOS.tmpdir(), | |
| 904 | runtimeMode: "full-access", | |
| 905 | environment: { ...process.env, T3_CODEX_COLLAB_SCRIPT: scriptPath }, | |
| 906 | models: Effect.succeed([ | |
| 907 | { slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", isCustom: false, capabilities: null }, | |
| 908 | ]), | |
| 909 | }); | |
| 910 | const completedFiber = yield* runtime.events.pipe( | |
| 911 | Stream.filter((event) => event.method === "turn/completed"), | |
| 912 | Stream.take(1), | |
| 913 | Stream.runCollect, | |
| 914 | Effect.forkScoped, | |
| 915 | ); | |
| 916 | ||
| 917 | yield* runtime.start(); | |
| 918 | yield* runtime.sendTurn({ input: "keep going", interactionMode: "default" }); | |
| 919 | yield* Fiber.join(completedFiber); | |
| 920 | ||
| 921 | // The restore is awaited before later notifications, so it has landed. | |
| 922 | const requests = readRecordedRequests(); | |
| 923 | assert.lengthOf(requests, 1); | |
| 924 | const [inject] = requests; | |
| 925 | assert.isDefined(inject); | |
| 926 | assert.equal(inject.method, "thread/inject_items"); | |
| 927 | assert.equal(inject.params.threadId, ROOT); | |
| 928 | const texts = ( | |
| 929 | inject.params.items as ReadonlyArray<{ role: string; content: [{ text: string }] }> | |
| 930 | ).map((item) => { | |
| 931 | assert.equal(item.role, "developer"); | |
| 932 | return item.content[0].text; | |
| 933 | }); | |
| 934 | assert.lengthOf(texts, 1); | |
| 935 | assert.match( | |
| 936 | texts[0] ?? "", | |
| 937 | /^<t3_code_runtime><runtime_info>.*as GPT-5\.6 Sol \(model slug: gpt-5\.6-sol\).*<\/t3_code_runtime>$/s, | |
| 938 | ); | |
| 939 | ||
| 940 | yield* runtime.close; | |
| 941 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | |
| 942 | ); | |
| 943 | }); |
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+90-82| ... | ... | @@ -9,7 +9,10 @@ import * as CodexErrors from "effect-codex-app-server/errors"; |
| 9 | 9 | import * as CodexRpc from "effect-codex-app-server/rpc"; |
| 10 | 10 | import * as EffectCodexSchema from "effect-codex-app-server/schema"; |
| 11 | 11 | |
| 12 | import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts"; | |
| 12 | import { | |
| 13 | buildCodexAdditionalContext, | |
| 14 | buildCodexDeveloperInstructions, | |
| 15 | } from "../CodexDeveloperInstructions.ts"; | |
| 13 | 16 | import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; |
| 14 | 17 | import { |
| 15 | 18 | buildTurnStartParams, |
| ... | ... | @@ -232,12 +235,13 @@ describe("buildTurnStartParams", () => { |
| 232 | 235 | settings: { |
| 233 | 236 | model: "gpt-5.3-codex", |
| 234 | 237 | reasoning_effort: "medium", |
| 235 | developer_instructions: buildCodexDeveloperInstructions("plan", { | |
| 236 | model: "gpt-5.3-codex", | |
| 237 | reasoningEffort: "medium", | |
| 238 | }), | |
| 238 | developer_instructions: buildCodexDeveloperInstructions("plan"), | |
| 239 | 239 | }, |
| 240 | 240 | }, |
| 241 | additionalContext: buildCodexAdditionalContext({ | |
| 242 | model: "gpt-5.3-codex", | |
| 243 | reasoningEffort: "medium", | |
| 244 | }), | |
| 241 | 245 | }); |
| 242 | 246 | }); |
| 243 | 247 | |
| ... | ... | @@ -281,12 +285,13 @@ describe("buildTurnStartParams", () => { |
| 281 | 285 | settings: { |
| 282 | 286 | model: "gpt-5.3-codex", |
| 283 | 287 | reasoning_effort: "medium", |
| 284 | developer_instructions: buildCodexDeveloperInstructions("default", { | |
| 285 | model: "gpt-5.3-codex", | |
| 286 | reasoningEffort: "medium", | |
| 287 | }), | |
| 288 | developer_instructions: buildCodexDeveloperInstructions("default"), | |
| 288 | 289 | }, |
| 289 | 290 | }, |
| 291 | additionalContext: buildCodexAdditionalContext({ | |
| 292 | model: "gpt-5.3-codex", | |
| 293 | reasoningEffort: "medium", | |
| 294 | }), | |
| 290 | 295 | }); |
| 291 | 296 | }); |
| 292 | 297 | |
| ... | ... | @@ -303,9 +308,29 @@ describe("buildTurnStartParams", () => { |
| 303 | 308 | const settings = params.collaborationMode?.settings; |
| 304 | 309 | NodeAssert.equal(settings?.model, DEFAULT_MODEL); |
| 305 | 310 | NodeAssert.equal(settings?.reasoning_effort, "medium"); |
| 306 | NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`)); | |
| 311 | NodeAssert.ok( | |
| 312 | params.additionalContext?.t3_code_runtime?.value.includes(`as ${DEFAULT_MODEL} with medium`), | |
| 313 | ); | |
| 307 | 314 | }); |
| 308 | 315 | |
| 316 | it.effect("names the model by display name and slug in the runtime context", () => | |
| 317 | Effect.gen(function* () { | |
| 318 | const params = yield* buildTurnStartParams({ | |
| 319 | threadId: "provider-thread-1", | |
| 320 | runtimeMode: "full-access", | |
| 321 | model: "gpt-5.3-codex", | |
| 322 | modelName: "GPT-5.3-Codex", | |
| 323 | effort: "high", | |
| 324 | interactionMode: "plan", | |
| 325 | }); | |
| 326 | ||
| 327 | NodeAssert.match( | |
| 328 | params.additionalContext?.t3_code_runtime?.value ?? "", | |
| 329 | /as GPT-5\.3-Codex \(model slug: gpt-5\.3-codex\) with high reasoning effort/, | |
| 330 | ); | |
| 331 | }), | |
| 332 | ); | |
| 333 | ||
| 309 | 334 | it.effect("routes approvals to the auto reviewer in auto mode", () => |
| 310 | 335 | Effect.gen(function* () { |
| 311 | 336 | const params = yield* buildTurnStartParams({ |
| ... | ... | @@ -560,99 +585,82 @@ describe("Codex MCP elicitation approvals", () => { |
| 560 | 585 | }); |
| 561 | 586 | |
| 562 | 587 | describe("buildCodexDeveloperInstructions", () => { |
| 563 | it("appends runtime info after the mode instructions", () => { | |
| 564 | const instructions = buildCodexDeveloperInstructions("default", { | |
| 565 | model: "gpt-5.3-codex", | |
| 566 | reasoningEffort: "high", | |
| 567 | }); | |
| 568 | ||
| 569 | NodeAssert.match(instructions, /^<collaboration_mode># Collaboration Mode: Default/); | |
| 570 | NodeAssert.match(instructions, /T3 Code/); | |
| 571 | NodeAssert.match(instructions, /Codex harness/); | |
| 572 | NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/); | |
| 573 | }); | |
| 574 | ||
| 575 | it("describes Markdown media support in the runtime context in both modes", () => { | |
| 588 | it("keeps T3 context out of the mode prompt, which the model catalog can replace", () => { | |
| 576 | 589 | for (const mode of ["default", "plan"] as const) { |
| 577 | const instructions = buildCodexDeveloperInstructions(mode, { | |
| 578 | model: "gpt-5.3-codex", | |
| 579 | reasoningEffort: "high", | |
| 580 | }); | |
| 581 | NodeAssert.match( | |
| 582 | instructions, | |
| 583 | /<runtime_info>.*embed images and videos.*Markdown.*<\/runtime_info>/, | |
| 584 | ); | |
| 590 | const instructions = buildCodexDeveloperInstructions(mode); | |
| 591 | NodeAssert.match(instructions, /^<collaboration_mode>[\s\S]*<\/collaboration_mode>$/); | |
| 592 | NodeAssert.doesNotMatch(instructions, /runtime_info|pull_request_linking|preview_|device_/); | |
| 585 | 593 | } |
| 586 | 594 | }); |
| 595 | }); | |
| 587 | 596 | |
| 588 | it("includes runtime info alongside plan mode instructions", () => { | |
| 589 | const instructions = buildCodexDeveloperInstructions("plan", { | |
| 590 | model: "gpt-5.3-codex", | |
| 591 | reasoningEffort: "medium", | |
| 592 | }); | |
| 597 | describe("buildCodexAdditionalContext", () => { | |
| 598 | const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" }; | |
| 599 | const runtimeValue = (context: ReturnType<typeof buildCodexAdditionalContext>) => | |
| 600 | context.t3_code_runtime?.value ?? ""; | |
| 601 | ||
| 602 | it("describes the harness, model, effort, and Markdown media support", () => { | |
| 603 | const context = buildCodexAdditionalContext(runtime); | |
| 593 | 604 | |
| 594 | NodeAssert.match(instructions, /^<collaboration_mode># Plan Mode/); | |
| 595 | NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/); | |
| 605 | NodeAssert.equal(context.t3_code_runtime?.kind, "application"); | |
| 606 | NodeAssert.match( | |
| 607 | runtimeValue(context), | |
| 608 | /<runtime_info>.*Codex harness, as gpt-5\.3-codex with high reasoning effort.*embed images and videos.*Markdown.*<\/runtime_info>/, | |
| 609 | ); | |
| 596 | 610 | }); |
| 597 | 611 | |
| 598 | 612 | it("varies with the model and effort of each turn", () => { |
| 599 | const first = buildCodexDeveloperInstructions("default", { | |
| 600 | model: "gpt-5.3-codex", | |
| 601 | reasoningEffort: "medium", | |
| 602 | }); | |
| 603 | const second = buildCodexDeveloperInstructions("default", { | |
| 604 | model: "gpt-5.4", | |
| 605 | reasoningEffort: "high", | |
| 606 | }); | |
| 607 | ||
| 608 | NodeAssert.notEqual(first, second); | |
| 613 | NodeAssert.notEqual( | |
| 614 | runtimeValue( | |
| 615 | buildCodexAdditionalContext({ model: "gpt-5.3-codex", reasoningEffort: "medium" }), | |
| 616 | ), | |
| 617 | runtimeValue(buildCodexAdditionalContext({ model: "gpt-5.4", reasoningEffort: "high" })), | |
| 618 | ); | |
| 609 | 619 | }); |
| 610 | 620 | |
| 611 | 621 | it("flattens multiline metadata into single-line runtime info", () => { |
| 612 | const instructions = buildCodexDeveloperInstructions("default", { | |
| 613 | model: "gpt\n5.3\ncodex", | |
| 614 | reasoningEffort: " high\neffort ", | |
| 615 | }); | |
| 622 | const value = runtimeValue( | |
| 623 | buildCodexAdditionalContext({ model: "gpt\n5.3\ncodex", reasoningEffort: " high\neffort " }), | |
| 624 | ); | |
| 616 | 625 | |
| 617 | NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/); | |
| 618 | NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/); | |
| 626 | NodeAssert.match(value, /as gpt 5\.3 codex with high effort reasoning effort/); | |
| 627 | NodeAssert.doesNotMatch(value, /<runtime_info>[^<]*\n/); | |
| 628 | }); | |
| 629 | ||
| 630 | it("keeps every entry under Codex's 1,000 token cap per entry", () => { | |
| 631 | const context = buildCodexAdditionalContext(runtime, { browser: true, device: true }); | |
| 632 | for (const entry of Object.values(context)) { | |
| 633 | // Codex estimates 4 bytes per token and truncates the middle of longer values. | |
| 634 | NodeAssert.ok(Buffer.byteLength(entry.value) < 4_000); | |
| 635 | } | |
| 619 | 636 | }); |
| 620 | 637 | }); |
| 621 | 638 | |
| 622 | describe("T3 browser developer instructions", () => { | |
| 639 | describe("T3 tool instructions", () => { | |
| 623 | 640 | const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" }; |
| 624 | 641 | |
| 625 | it("prefers the product-native preview tools in both collaboration modes", () => { | |
| 626 | for (const mode of ["default", "plan"] as const) { | |
| 627 | const instructions = buildCodexDeveloperInstructions(mode, runtime, true); | |
| 628 | NodeAssert.match(instructions, /t3-code/); | |
| 629 | NodeAssert.match(instructions, /preview_status/); | |
| 630 | NodeAssert.match(instructions, /preview_open/); | |
| 631 | NodeAssert.match(instructions, /Do not switch to global browser skills/); | |
| 632 | } | |
| 642 | it("prefers the product-native preview tools when they are attached", () => { | |
| 643 | const tools = buildCodexAdditionalContext(runtime, true).t3_code_tools?.value ?? ""; | |
| 644 | NodeAssert.match(tools, /t3-code/); | |
| 645 | NodeAssert.match(tools, /preview_status/); | |
| 646 | NodeAssert.match(tools, /preview_open/); | |
| 647 | NodeAssert.match(tools, /Do not switch to global browser skills/); | |
| 648 | NodeAssert.doesNotMatch(tools, /device_open/); | |
| 633 | 649 | }); |
| 634 | 650 | |
| 635 | it("omits the browser block entirely when the preview tools are not attached", () => { | |
| 636 | for (const mode of ["default", "plan"] as const) { | |
| 637 | const instructions = buildCodexDeveloperInstructions(mode, runtime, false); | |
| 638 | NodeAssert.doesNotMatch(instructions, /preview_status/); | |
| 639 | NodeAssert.doesNotMatch(instructions, /preview_open/); | |
| 640 | NodeAssert.doesNotMatch(instructions, /T3 Code collaborative browser/); | |
| 641 | // Steering away from other browser automation must go with the tools; | |
| 642 | // keeping it would leave the model talked out of its only option. | |
| 643 | NodeAssert.doesNotMatch(instructions, /Do not switch to global browser skills/); | |
| 644 | // The rest of the collaboration mode is untouched. | |
| 645 | NodeAssert.match(instructions, /<collaboration_mode>/); | |
| 646 | NodeAssert.match(instructions, /<\/collaboration_mode>/); | |
| 647 | } | |
| 651 | it("describes device tools only when the credential grants them", () => { | |
| 652 | const tools = | |
| 653 | buildCodexAdditionalContext(runtime, { browser: false, device: true }).t3_code_tools?.value ?? | |
| 654 | ""; | |
| 655 | NodeAssert.match(tools, /device_open/); | |
| 656 | NodeAssert.doesNotMatch(tools, /preview_open/); | |
| 648 | 657 | }); |
| 649 | 658 | |
| 650 | it("tracks the turn's MCP configuration rather than defaulting to on", () => { | |
| 651 | NodeAssert.match(buildCodexDeveloperInstructions("default", runtime, true), /preview_open/); | |
| 652 | NodeAssert.doesNotMatch( | |
| 653 | buildCodexDeveloperInstructions("default", runtime, false), | |
| 654 | /preview_open/, | |
| 655 | ); | |
| 659 | it("omits the tool entry entirely when no tools are attached", () => { | |
| 660 | // Steering away from other browser automation must go with the tools; | |
| 661 | // keeping it would leave the model talked out of its only option. | |
| 662 | const context = buildCodexAdditionalContext(runtime, false); | |
| 663 | NodeAssert.deepStrictEqual(Object.keys(context), ["t3_code_runtime"]); | |
| 656 | 664 | }); |
| 657 | 665 | }); |
| 658 | 666 |
apps/server/src/provider/Layers/CodexSessionRuntime.ts+72-15| ... | ... | @@ -14,6 +14,7 @@ import { |
| 14 | 14 | type ProviderTurnStartResult, |
| 15 | 15 | type ProviderUserInputAnswers, |
| 16 | 16 | RuntimeMode, |
| 17 | type ServerProviderModel, | |
| 17 | 18 | ThreadId, |
| 18 | 19 | TurnId, |
| 19 | 20 | } from "@t3tools/contracts"; |
| ... | ... | @@ -40,6 +41,7 @@ import { buildCodexInitializeParams } from "./CodexProvider.ts"; |
| 40 | 41 | import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; |
| 41 | 42 | import { expandHomePath } from "../../pathExpansion.ts"; |
| 42 | 43 | import { |
| 44 | buildCodexAdditionalContext, | |
| 43 | 45 | buildCodexDeveloperInstructions, |
| 44 | 46 | type T3CodeToolAvailability, |
| 45 | 47 | } from "../CodexDeveloperInstructions.ts"; |
| ... | ... | @@ -141,10 +143,13 @@ const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata); |
| 141 | 143 | const isMcpElicitationForm = Schema.is(McpElicitationForm); |
| 142 | 144 | |
| 143 | 145 | // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated |
| 144 | // `V2TurnStartParams` schema includes `collaborationMode` directly. | |
| 146 | // `V2TurnStartParams` schema includes its experimental fields directly. | |
| 145 | 147 | const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe( |
| 146 | 148 | Schema.fieldsAssign({ |
| 147 | 149 | collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), |
| 150 | additionalContext: Schema.optionalKey( | |
| 151 | Schema.Record(Schema.String, EffectCodexSchema.V2TurnStartParams__AdditionalContextEntry), | |
| 152 | ), | |
| 148 | 153 | }), |
| 149 | 154 | ); |
| 150 | 155 | const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( |
| ... | ... | @@ -178,6 +183,8 @@ export interface CodexSessionRuntimeOptions { |
| 178 | 183 | readonly serviceTier?: CodexServiceTier | undefined; |
| 179 | 184 | readonly resumeCursor?: CodexResumeCursor; |
| 180 | 185 | readonly appServerArgs?: ReadonlyArray<string>; |
| 186 | /** The provider's model list; supplies the display name for runtime info. */ | |
| 187 | readonly models?: Effect.Effect<ReadonlyArray<ServerProviderModel>>; | |
| 181 | 188 | /** Capabilities the session's `t3-code` MCP credential grants; drives the prompt blocks. */ |
| 182 | 189 | readonly mcpCapabilities?: ReadonlySet<string>; |
| 183 | 190 | } |
| ... | ... | @@ -579,28 +586,31 @@ function runtimeModeToTurnSandboxPolicy( |
| 579 | 586 | } |
| 580 | 587 | } |
| 581 | 588 | |
| 582 | function buildCodexCollaborationMode(input: { | |
| 589 | function buildCodexTurnInstructions(input: { | |
| 583 | 590 | readonly interactionMode?: ProviderInteractionMode; |
| 584 | 591 | readonly model?: string; |
| 592 | readonly modelName?: string; | |
| 585 | 593 | readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; |
| 586 | 594 | readonly browserToolsAvailable?: boolean | T3CodeToolAvailability; |
| 587 | }): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined { | |
| 595 | }): Pick<CodexTurnStartParamsWithCollaborationMode, "collaborationMode" | "additionalContext"> { | |
| 588 | 596 | if (input.interactionMode === undefined) { |
| 589 | return undefined; | |
| 597 | return {}; | |
| 590 | 598 | } |
| 591 | 599 | const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL; |
| 592 | 600 | const reasoningEffort = input.effort ?? "medium"; |
| 593 | 601 | return { |
| 594 | mode: input.interactionMode, | |
| 595 | settings: { | |
| 596 | model, | |
| 597 | reasoning_effort: reasoningEffort, | |
| 598 | developer_instructions: buildCodexDeveloperInstructions( | |
| 599 | input.interactionMode, | |
| 600 | { model, reasoningEffort }, | |
| 601 | input.browserToolsAvailable ?? true, | |
| 602 | ), | |
| 602 | collaborationMode: { | |
| 603 | mode: input.interactionMode, | |
| 604 | settings: { | |
| 605 | model, | |
| 606 | reasoning_effort: reasoningEffort, | |
| 607 | developer_instructions: buildCodexDeveloperInstructions(input.interactionMode), | |
| 608 | }, | |
| 603 | 609 | }, |
| 610 | additionalContext: buildCodexAdditionalContext( | |
| 611 | { model, modelName: input.modelName, reasoningEffort }, | |
| 612 | input.browserToolsAvailable ?? true, | |
| 613 | ), | |
| 604 | 614 | }; |
| 605 | 615 | } |
| 606 | 616 | |
| ... | ... | @@ -617,6 +627,8 @@ export function buildTurnStartParams(input: { |
| 617 | 627 | readonly path: string; |
| 618 | 628 | }>; |
| 619 | 629 | readonly model?: string; |
| 630 | /** Display name of `model`, for runtime info. */ | |
| 631 | readonly modelName?: string; | |
| 620 | 632 | readonly serviceTier?: CodexServiceTier; |
| 621 | 633 | readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; |
| 622 | 634 | readonly interactionMode?: ProviderInteractionMode; |
| ... | ... | @@ -638,9 +650,10 @@ export function buildTurnStartParams(input: { |
| 638 | 650 | } |
| 639 | 651 | |
| 640 | 652 | const config = runtimeModeToThreadConfig(input.runtimeMode); |
| 641 | const collaborationMode = buildCodexCollaborationMode({ | |
| 653 | const turnInstructions = buildCodexTurnInstructions({ | |
| 642 | 654 | ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), |
| 643 | 655 | ...(input.model ? { model: input.model } : {}), |
| 656 | ...(input.modelName ? { modelName: input.modelName } : {}), | |
| 644 | 657 | ...(input.effort ? { effort: input.effort } : {}), |
| 645 | 658 | browserToolsAvailable: input.browserToolsAvailable ?? true, |
| 646 | 659 | }); |
| ... | ... | @@ -654,7 +667,7 @@ export function buildTurnStartParams(input: { |
| 654 | 667 | ...(input.model ? { model: input.model } : {}), |
| 655 | 668 | ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), |
| 656 | 669 | ...(input.effort ? { effort: input.effort } : {}), |
| 657 | ...(collaborationMode ? { collaborationMode } : {}), | |
| 670 | ...turnInstructions, | |
| 658 | 671 | }).pipe( |
| 659 | 672 | Effect.mapError((cause) => |
| 660 | 673 | CodexErrors.CodexAppServerProtocolParseError.fromSchemaError( |
| ... | ... | @@ -1305,6 +1318,9 @@ export const makeCodexSessionRuntime = ( |
| 1305 | 1318 | const collabChildLiveTurnsRef = yield* Ref.make(new Map<string, string>()); |
| 1306 | 1319 | const suppressMemoryConsolidationNotification = makeMemoryConsolidationNotificationFilter(); |
| 1307 | 1320 | const closedRef = yield* Ref.make(false); |
| 1321 | /** The `additionalContext` of the latest `turn/start`, restored after compaction. */ | |
| 1322 | const lastAdditionalContextRef = | |
| 1323 | yield* Ref.make<CodexTurnStartParamsWithCollaborationMode["additionalContext"]>(undefined); | |
| 1308 | 1324 | |
| 1309 | 1325 | // `~` is not shell-expanded when env vars are set via |
| 1310 | 1326 | // `child_process.spawn`; `expandHomePath` lets a configured |
| ... | ... | @@ -1856,6 +1872,35 @@ export const makeCodexSessionRuntime = ( |
| 1856 | 1872 | } |
| 1857 | 1873 | }); |
| 1858 | 1874 | |
| 1875 | /** | |
| 1876 | * Compaction rebuilds history from user messages and Codex's own context, | |
| 1877 | * which drops our `additionalContext` messages. Codex only resends an | |
| 1878 | * entry when its value changes, so without this the T3 context would stay | |
| 1879 | * lost until the model or effort changed. Awaited so the context is back | |
| 1880 | * before later notifications from the same turn are handled. Drop this if | |
| 1881 | * Codex enables its `retain_client_developer_messages` feature by default. | |
| 1882 | */ | |
| 1883 | const restoreAdditionalContext = (threadId: string) => | |
| 1884 | Effect.gen(function* () { | |
| 1885 | const context = yield* Ref.get(lastAdditionalContextRef); | |
| 1886 | if (!context) return; | |
| 1887 | yield* client.request("thread/inject_items", { | |
| 1888 | threadId, | |
| 1889 | items: Object.entries(context).map(([key, entry]) => ({ | |
| 1890 | type: "message", | |
| 1891 | role: "developer", | |
| 1892 | content: [{ type: "input_text", text: `<${key}>${entry.value}</${key}>` }], | |
| 1893 | })), | |
| 1894 | }); | |
| 1895 | }).pipe( | |
| 1896 | Effect.timeout("10 seconds"), | |
| 1897 | Effect.catch((cause) => | |
| 1898 | Effect.logWarning("Failed to restore Codex additional context after compaction.", { | |
| 1899 | cause, | |
| 1900 | }), | |
| 1901 | ), | |
| 1902 | ); | |
| 1903 | ||
| 1859 | 1904 | const handleRawNotification = (notification: CodexServerNotification) => |
| 1860 | 1905 | Effect.gen(function* () { |
| 1861 | 1906 | const isMemoryConsolidationNotification = |
| ... | ... | @@ -1942,6 +1987,14 @@ export const makeCodexSessionRuntime = ( |
| 1942 | 1987 | return; |
| 1943 | 1988 | } |
| 1944 | 1989 | |
| 1990 | if ( | |
| 1991 | notification.method === "item/completed" && | |
| 1992 | notification.params.item.type === "contextCompaction" && | |
| 1993 | notification.params.threadId === suppressRootId | |
| 1994 | ) { | |
| 1995 | yield* restoreAdditionalContext(notification.params.threadId); | |
| 1996 | } | |
| 1997 | ||
| 1945 | 1998 | let requestId: ApprovalRequestId | undefined; |
| 1946 | 1999 | let requestKind: ProviderRequestKind | undefined; |
| 1947 | 2000 | let turnId = childParentTurnId ?? route.turnId; |
| ... | ... | @@ -2509,12 +2562,15 @@ export const makeCodexSessionRuntime = ( |
| 2509 | 2562 | const normalizedModel = normalizeCodexModelSlug( |
| 2510 | 2563 | input.model ?? (yield* Ref.get(sessionRef)).model, |
| 2511 | 2564 | ); |
| 2565 | const models = options.models ? yield* options.models : []; | |
| 2566 | const modelName = models.find((model) => model.slug === normalizedModel)?.name; | |
| 2512 | 2567 | const params = yield* buildTurnStartParams({ |
| 2513 | 2568 | threadId: providerThreadId, |
| 2514 | 2569 | runtimeMode: options.runtimeMode, |
| 2515 | 2570 | ...(input.input ? { prompt: input.input } : {}), |
| 2516 | 2571 | ...(input.attachments ? { attachments: input.attachments } : {}), |
| 2517 | 2572 | ...(normalizedModel ? { model: normalizedModel } : {}), |
| 2573 | ...(modelName ? { modelName } : {}), | |
| 2518 | 2574 | ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), |
| 2519 | 2575 | ...(input.effort ? { effort: input.effort } : {}), |
| 2520 | 2576 | ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), |
| ... | ... | @@ -2526,6 +2582,7 @@ export const makeCodexSessionRuntime = ( |
| 2526 | 2582 | options.mcpCapabilities, |
| 2527 | 2583 | ), |
| 2528 | 2584 | }); |
| 2585 | yield* Ref.set(lastAdditionalContextRef, params.additionalContext); | |
| 2529 | 2586 | const rawResponse = yield* client.raw.request("turn/start", params); |
| 2530 | 2587 | const response = yield* decodeV2TurnStartResponse(rawResponse).pipe( |
| 2531 | 2588 | Effect.mapError((error) => |
apps/server/src/provider/RuntimeInstructions.test.ts+9| ... | ... | @@ -20,6 +20,15 @@ describe("buildRuntimeInstructions", () => { |
| 20 | 20 | ).toContain("through the Codex harness, as custom model with high reasoning effort."); |
| 21 | 21 | }); |
| 22 | 22 | |
| 23 | it("names the model by display name and slug when they differ", () => { | |
| 24 | expect( | |
| 25 | buildRuntimeInstructions({ harness: "Codex", model: "gpt-5.4", modelName: "GPT-5.4" }), | |
| 26 | ).toContain("through the Codex harness, as GPT-5.4 (model slug: gpt-5.4)."); | |
| 27 | expect( | |
| 28 | buildRuntimeInstructions({ harness: "Codex", model: "my-model", modelName: "my-model" }), | |
| 29 | ).toContain("through the Codex harness, as my-model."); | |
| 30 | }); | |
| 31 | ||
| 23 | 32 | it.each([undefined, "", "auto", "default"])("omits unresolved model %s", (model) => { |
| 24 | 33 | const instructions = buildRuntimeInstructions({ harness: "Cursor", model }); |
| 25 | 34 | expect(instructions).toContain("through the Cursor harness."); |
apps/server/src/provider/RuntimeInstructions.ts+9-2| ... | ... | @@ -2,16 +2,23 @@ const PULL_REQUEST_LINKING_INSTRUCTIONS = `<pull_request_linking> |
| 2 | 2 | When the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked. |
| 3 | 3 | </pull_request_linking>`; |
| 4 | 4 | |
| 5 | /** Shared runtime context; omit model and effort when the harness manages them dynamically. */ | |
| 5 | /** | |
| 6 | * Shared runtime context; omit model and effort when the harness manages them dynamically. | |
| 7 | * `modelName` is the display name users see in the model picker; `model` is the slug. | |
| 8 | */ | |
| 6 | 9 | export function buildRuntimeInstructions(runtime: { |
| 7 | 10 | readonly harness: string; |
| 8 | 11 | readonly model?: string | undefined; |
| 12 | readonly modelName?: string | undefined; | |
| 9 | 13 | readonly reasoningEffort?: string | undefined; |
| 10 | 14 | }): string { |
| 11 | 15 | const harness = toSingleLine(runtime.harness); |
| 12 | 16 | const model = toSingleLine(runtime.model ?? ""); |
| 17 | const modelName = toSingleLine(runtime.modelName ?? ""); | |
| 13 | 18 | const effort = toSingleLine(runtime.reasoningEffort ?? ""); |
| 14 | const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${model}` : ""; | |
| 19 | const modelLabel = | |
| 20 | modelName && modelName !== model ? `${modelName} (model slug: ${model})` : model; | |
| 21 | const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${modelLabel}` : ""; | |
| 15 | 22 | const effortInfo = effort ? ` with ${effort} reasoning effort` : ""; |
| 16 | 23 | return `<runtime_info>In case you're asked: you are running in T3 Code through the ${harness} harness${modelInfo}${effortInfo}. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>\n\n${PULL_REQUEST_LINKING_INSTRUCTIONS}`; |
| 17 | 24 | } |
apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs+8| ... | ... | @@ -106,6 +106,14 @@ rl.on("line", (line) => { |
| 106 | 106 | write({ id, result: fixture.responses.threadStart }); |
| 107 | 107 | return; |
| 108 | 108 | } |
| 109 | if (method === "thread/inject_items" && script.recordRequests) { | |
| 110 | NodeFS.appendFileSync( | |
| 111 | `${process.env.T3_CODEX_COLLAB_SCRIPT}.requests`, | |
| 112 | `${JSON.stringify({ method, params: message.params })}\n`, | |
| 113 | ); | |
| 114 | write({ id, result: {} }); | |
| 115 | return; | |
| 116 | } | |
| 109 | 117 | if (method === "thread/resume") { |
| 110 | 118 | if (script.recordRequests) { |
| 111 | 119 | NodeFS.appendFileSync( |