| author | |
| committer | |
| log | d7950ac153c6fdd788ef63699a5d061243bb4997 |
| tree | 353f22b5b75a2199d4fdeef321ca8886d163c459 |
| parent | 36caf34c68b7bc6c2e967fdec1812fd08f61cdc8 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 73 insertions(+), 22 deletions(-)
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+1-1| ... | ... | @@ -264,7 +264,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { |
| 264 | 264 | ' "Reconnect failures after restart because the session state does not recover" ', |
| 265 | 265 | }, |
| 266 | 266 | }), |
| 267 | stdinMustContain: "You write concise thread titles for coding conversations.", | |
| 267 | stdinMustContain: "Please investigate reconnect failures after restarting the session.", | |
| 268 | 268 | }, |
| 269 | 269 | (textGeneration) => |
| 270 | 270 | Effect.gen(function* () { |
apps/server/src/textGeneration/CodexTextGeneration.ts+7-4| ... | ... | @@ -7,7 +7,12 @@ import * as Scope from "effect/Scope"; |
| 7 | 7 | import * as Stream from "effect/Stream"; |
| 8 | 8 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 9 | 9 | |
| 10 | import { type CodexSettings, type ModelSelection } from "@t3tools/contracts"; | |
| 10 | import { | |
| 11 | type CodexSettings, | |
| 12 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, | |
| 13 | type ModelSelection, | |
| 14 | TextGenerationError, | |
| 15 | } from "@t3tools/contracts"; | |
| 11 | 16 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| 12 | 17 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 13 | 18 | |
| ... | ... | @@ -15,7 +20,6 @@ import { resolveAttachmentPath } from "../attachmentStore.ts"; |
| 15 | 20 | import * as ServerConfig from "../config.ts"; |
| 16 | 21 | import { expandHomePath } from "../pathExpansion.ts"; |
| 17 | 22 | import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; |
| 18 | import { TextGenerationError } from "@t3tools/contracts"; | |
| 19 | 23 | import * as TextGeneration from "./TextGeneration.ts"; |
| 20 | 24 | import { |
| 21 | 25 | buildBranchNamePrompt, |
| ... | ... | @@ -33,7 +37,6 @@ import { |
| 33 | 37 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; |
| 34 | 38 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; |
| 35 | 39 | |
| 36 | const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; | |
| 37 | 40 | const CODEX_TIMEOUT_MS = 180_000; |
| 38 | 41 | const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 39 | 42 | /** |
| ... | ... | @@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 178 | 181 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); |
| 179 | 182 | const reasoningEffort = |
| 180 | 183 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? |
| 181 | CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; | |
| 184 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT; | |
| 182 | 185 | const serviceTier = getCodexServiceTierOptionValue(modelSelection); |
| 183 | 186 | const spawnCommand = yield* resolveSpawnCommand( |
| 184 | 187 | codexConfig.binaryPath || "codex", |
apps/server/src/textGeneration/TextGenerationPrompts.test.ts+14-3| ... | ... | @@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => { |
| 154 | 154 | expect(result.prompt).toContain("User message:"); |
| 155 | 155 | expect(result.prompt).toContain("Investigate reconnect regressions after session restore"); |
| 156 | 156 | expect(result.prompt).not.toContain("Attachment metadata:"); |
| 157 | expect(result.prompt).toContain( | |
| 158 | "Generate a title that will help the user recognize this T3 Code thread weeks later.", | |
| 159 | ); | |
| 160 | expect(result.prompt).toContain( | |
| 161 | "Title the subject and outcome. Discard incidental instructions.", | |
| 162 | ); | |
| 163 | expect(result.prompt).toContain( | |
| 164 | "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", | |
| 165 | ); | |
| 166 | expect(result.prompt).not.toContain( | |
| 167 | "Title should summarize the user's request, not restate it verbatim.", | |
| 168 | ); | |
| 157 | 169 | }); |
| 158 | 170 | |
| 159 | 171 | it("includes attachment metadata when attachments are provided", () => { |
| ... | ... | @@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => { |
| 183 | 195 | }); |
| 184 | 196 | |
| 185 | 197 | expect(result.prompt).toContain( |
| 186 | "The user requested a new title based on the contents of this thread.", | |
| 198 | "Generate a new title that will help the user recognize this T3 Code thread weeks later.", | |
| 187 | 199 | ); |
| 188 | 200 | expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); |
| 189 | expect(result.prompt).toContain("better represents the current state of the thread"); | |
| 190 | 201 | expect(result.prompt).toContain( |
| 191 | "Capture the thread's intent, not a PR number or other superficial detail.", | |
| 202 | "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", | |
| 192 | 203 | ); |
| 193 | 204 | expect(result.prompt).toContain("Thread contents:"); |
| 194 | 205 | expect(result.prompt).toContain("The remaining issue is stale session state"); |
apps/server/src/textGeneration/TextGenerationPrompts.ts+31-13| ... | ... | @@ -150,6 +150,8 @@ export interface BranchNamePromptInput { |
| 150 | 150 | interface PromptFromMessageInput { |
| 151 | 151 | instruction: string; |
| 152 | 152 | responseShape: string; |
| 153 | guidance?: ReadonlyArray<string> | undefined; | |
| 154 | rulesLabel?: string | undefined; | |
| 153 | 155 | rules: ReadonlyArray<string>; |
| 154 | 156 | message: string; |
| 155 | 157 | messageLabel?: string | undefined; |
| ... | ... | @@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { |
| 177 | 179 | const promptSections = [ |
| 178 | 180 | input.instruction, |
| 179 | 181 | input.responseShape, |
| 180 | "Rules:", | |
| 182 | ...(input.guidance ?? []), | |
| 183 | input.rulesLabel ?? "Rules:", | |
| 181 | 184 | ...input.rules.map((rule) => `- ${rule}`), |
| 182 | 185 | "", |
| 183 | 186 | `${input.messageLabel ?? "User message"}:`, |
| ... | ... | @@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { |
| 234 | 237 | const prompt = buildPromptFromMessage({ |
| 235 | 238 | instruction: isRegeneration |
| 236 | 239 | ? [ |
| 237 | "You write concise thread titles for coding conversations.", | |
| 238 | "The user requested a new title based on the contents of this thread.", | |
| 240 | "Generate a new title that will help the user recognize this T3 Code thread weeks later.", | |
| 239 | 241 | `The previous title was ${JSON.stringify(input.previousTitle)}.`, |
| 240 | "Come up with a new title that better represents the current state of the thread.", | |
| 241 | 242 | ].join("\n") |
| 242 | : "You write concise thread titles for coding conversations.", | |
| 243 | responseShape: "Return a JSON object with key: title.", | |
| 243 | : "Generate a title that will help the user recognize this T3 Code thread weeks later.", | |
| 244 | responseShape: "Return JSON with exactly one key: title.", | |
| 245 | guidance: [ | |
| 246 | "", | |
| 247 | "Before answering, silently reduce the request to:", | |
| 248 | "- Subject: What system, feature, or problem is this really about?", | |
| 249 | "- Outcome: What does the user ultimately want to understand or change?", | |
| 250 | "- Incidental instructions: What only describes how the agent should do the work?", | |
| 251 | "", | |
| 252 | "Title the subject and outcome. Discard incidental instructions.", | |
| 253 | "", | |
| 254 | ], | |
| 255 | rulesLabel: "Editorial rules:", | |
| 244 | 256 | rules: [ |
| 245 | isRegeneration | |
| 246 | ? "Title should summarize the thread's current state, not just its initial request." | |
| 247 | : "Title should summarize the user's request, not restate it verbatim.", | |
| 257 | "3-8 words, fewer than 40 characters.", | |
| 258 | "Use a compact noun phrase or clear action phrase.", | |
| 259 | "Capture the umbrella goal when the request lists several symptoms or steps.", | |
| 260 | "Name the product change, not the mock, plan, report, branch, or PR used to produce it.", | |
| 261 | "Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.", | |
| 262 | 'For reviews, name what is being reviewed and the relevant concern. Avoid generic titles such as "Review PR 123" when linked or attached context reveals the subject.', | |
| 263 | "For research, name the question domain rather than the requested research process.", | |
| 264 | "Do not claim the work is complete.", | |
| 265 | "Do not copy and truncate the user's message.", | |
| 266 | "Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.", | |
| 267 | "Use attached images as primary context for UI issues.", | |
| 268 | "When a URL or attachment is the only source of the subject, use available tools to inspect it. If it cannot be resolved, remain accurate rather than guessing.", | |
| 248 | 269 | ...(isRegeneration |
| 249 | 270 | ? [ |
| 250 | "Capture the thread's intent, not a PR number or other superficial detail.", | |
| 271 | "Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.", | |
| 251 | 272 | "Return a different title from the previous title.", |
| 252 | 273 | ] |
| 253 | 274 | : []), |
| 254 | "Keep it short and specific (3-8 words).", | |
| 255 | "Avoid quotes, filler, prefixes, and trailing punctuation.", | |
| 256 | "If images are attached, use them as primary context for visual/UI issues.", | |
| 257 | 275 | ], |
| 258 | 276 | message: input.message, |
| 259 | 277 | ...(isRegeneration |
packages/contracts/src/model.ts+1| ... | ... | @@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [ |
| 145 | 145 | "gpt-5.6-terra", |
| 146 | 146 | ]; |
| 147 | 147 | export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna"; |
| 148 | export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low"; | |
| 148 | 149 | |
| 149 | 150 | export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = { |
| 150 | 151 | [CODEX_DRIVER_KIND]: DEFAULT_MODEL, |
packages/contracts/src/settings.test.ts+8| ... | ... | @@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => { |
| 112 | 112 | }); |
| 113 | 113 | |
| 114 | 114 | describe("ServerSettings.providerInstances (slice-2 invariant)", () => { |
| 115 | it("defaults text generation to Luna at low reasoning effort", () => { | |
| 116 | expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({ | |
| 117 | instanceId: ProviderInstanceId.make("codex"), | |
| 118 | model: "gpt-5.6-luna", | |
| 119 | options: [{ id: "reasoningEffort", value: "low" }], | |
| 120 | }); | |
| 121 | }); | |
| 122 | ||
| 115 | 123 | it("defaults to an empty record so legacy configs without the key still decode", () => { |
| 116 | 124 | expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({}); |
| 117 | 125 | }); |
packages/contracts/src/settings.ts+11-1| ... | ... | @@ -3,7 +3,11 @@ import * as Duration from "effect/Duration"; |
| 3 | 3 | import * as Schema from "effect/Schema"; |
| 4 | 4 | import * as SchemaTransformation from "effect/SchemaTransformation"; |
| 5 | 5 | import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; |
| 6 | import { DEFAULT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts"; | |
| 6 | import { | |
| 7 | DEFAULT_TEXT_GENERATION_MODEL, | |
| 8 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, | |
| 9 | ProviderOptionSelections, | |
| 10 | } from "./model.ts"; | |
| 7 | 11 | import { ModelSelection } from "./orchestration.ts"; |
| 8 | 12 | import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; |
| 9 | 13 | |
| ... | ... | @@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({ |
| 498 | 502 | Effect.succeed({ |
| 499 | 503 | instanceId: ProviderInstanceId.make("codex"), |
| 500 | 504 | model: DEFAULT_TEXT_GENERATION_MODEL, |
| 505 | options: [ | |
| 506 | { | |
| 507 | id: "reasoningEffort", | |
| 508 | value: DEFAULT_TEXT_GENERATION_REASONING_EFFORT, | |
| 509 | }, | |
| 510 | ], | |
| 501 | 511 | }), |
| 502 | 512 | ), |
| 503 | 513 | ), |