| 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,7 +264,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => { |
| 264 | ' "Reconnect failures after restart because the session state does not recover" ', | 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 | (textGeneration) => | 269 | (textGeneration) => |
| 270 | Effect.gen(function* () { | 270 | Effect.gen(function* () { |
apps/server/src/textGeneration/CodexTextGeneration.ts+7-4| ... | @@ -7,7 +7,12 @@ import * as Scope from "effect/Scope"; | ... | @@ -7,7 +7,12 @@ import * as Scope from "effect/Scope"; |
| 7 | import * as Stream from "effect/Stream"; | 7 | import * as Stream from "effect/Stream"; |
| 8 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 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 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; | 16 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| 12 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; | 17 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 13 | 18 | ||
| ... | @@ -15,7 +20,6 @@ import { resolveAttachmentPath } from "../attachmentStore.ts"; | ... | @@ -15,7 +20,6 @@ import { resolveAttachmentPath } from "../attachmentStore.ts"; |
| 15 | import * as ServerConfig from "../config.ts"; | 20 | import * as ServerConfig from "../config.ts"; |
| 16 | import { expandHomePath } from "../pathExpansion.ts"; | 21 | import { expandHomePath } from "../pathExpansion.ts"; |
| 17 | import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; | 22 | import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; |
| 18 | import { TextGenerationError } from "@t3tools/contracts"; | ||
| 19 | import * as TextGeneration from "./TextGeneration.ts"; | 23 | import * as TextGeneration from "./TextGeneration.ts"; |
| 20 | import { | 24 | import { |
| 21 | buildBranchNamePrompt, | 25 | buildBranchNamePrompt, |
| ... | @@ -33,7 +37,6 @@ import { | ... | @@ -33,7 +37,6 @@ import { |
| 33 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; | 37 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; |
| 34 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; | 38 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; |
| 35 | 39 | ||
| 36 | const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low"; | ||
| 37 | const CODEX_TIMEOUT_MS = 180_000; | 40 | const CODEX_TIMEOUT_MS = 180_000; |
| 38 | const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); | 41 | const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 39 | /** | 42 | /** |
| ... | @@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func | ... | @@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 178 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); | 181 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); |
| 179 | const reasoningEffort = | 182 | const reasoningEffort = |
| 180 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? | 183 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? |
| 181 | CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; | 184 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT; |
| 182 | const serviceTier = getCodexServiceTierOptionValue(modelSelection); | 185 | const serviceTier = getCodexServiceTierOptionValue(modelSelection); |
| 183 | const spawnCommand = yield* resolveSpawnCommand( | 186 | const spawnCommand = yield* resolveSpawnCommand( |
| 184 | codexConfig.binaryPath || "codex", | 187 | codexConfig.binaryPath || "codex", |
apps/server/src/textGeneration/TextGenerationPrompts.test.ts+14-3| ... | @@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => { | ... | @@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => { |
| 154 | expect(result.prompt).toContain("User message:"); | 154 | expect(result.prompt).toContain("User message:"); |
| 155 | expect(result.prompt).toContain("Investigate reconnect regressions after session restore"); | 155 | expect(result.prompt).toContain("Investigate reconnect regressions after session restore"); |
| 156 | expect(result.prompt).not.toContain("Attachment metadata:"); | 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 | it("includes attachment metadata when attachments are provided", () => { | 171 | it("includes attachment metadata when attachments are provided", () => { |
| ... | @@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => { | ... | @@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => { |
| 183 | }); | 195 | }); |
| 184 | 196 | ||
| 185 | expect(result.prompt).toContain( | 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 | expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); | 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 | expect(result.prompt).toContain( | 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 | expect(result.prompt).toContain("Thread contents:"); | 204 | expect(result.prompt).toContain("Thread contents:"); |
| 194 | expect(result.prompt).toContain("The remaining issue is stale session state"); | 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,6 +150,8 @@ export interface BranchNamePromptInput { |
| 150 | interface PromptFromMessageInput { | 150 | interface PromptFromMessageInput { |
| 151 | instruction: string; | 151 | instruction: string; |
| 152 | responseShape: string; | 152 | responseShape: string; |
| 153 | guidance?: ReadonlyArray<string> | undefined; | ||
| 154 | rulesLabel?: string | undefined; | ||
| 153 | rules: ReadonlyArray<string>; | 155 | rules: ReadonlyArray<string>; |
| 154 | message: string; | 156 | message: string; |
| 155 | messageLabel?: string | undefined; | 157 | messageLabel?: string | undefined; |
| ... | @@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { | ... | @@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { |
| 177 | const promptSections = [ | 179 | const promptSections = [ |
| 178 | input.instruction, | 180 | input.instruction, |
| 179 | input.responseShape, | 181 | input.responseShape, |
| 180 | "Rules:", | 182 | ...(input.guidance ?? []), |
| 183 | input.rulesLabel ?? "Rules:", | ||
| 181 | ...input.rules.map((rule) => `- ${rule}`), | 184 | ...input.rules.map((rule) => `- ${rule}`), |
| 182 | "", | 185 | "", |
| 183 | `${input.messageLabel ?? "User message"}:`, | 186 | `${input.messageLabel ?? "User message"}:`, |
| ... | @@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { | ... | @@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { |
| 234 | const prompt = buildPromptFromMessage({ | 237 | const prompt = buildPromptFromMessage({ |
| 235 | instruction: isRegeneration | 238 | instruction: isRegeneration |
| 236 | ? [ | 239 | ? [ |
| 237 | "You write concise thread titles for coding conversations.", | 240 | "Generate a new title that will help the user recognize this T3 Code thread weeks later.", |
| 238 | "The user requested a new title based on the contents of this thread.", | ||
| 239 | `The previous title was ${JSON.stringify(input.previousTitle)}.`, | 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 | ].join("\n") | 242 | ].join("\n") |
| 242 | : "You write concise thread titles for coding conversations.", | 243 | : "Generate a title that will help the user recognize this T3 Code thread weeks later.", |
| 243 | responseShape: "Return a JSON object with key: title.", | 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 | rules: [ | 256 | rules: [ |
| 245 | isRegeneration | 257 | "3-8 words, fewer than 40 characters.", |
| 246 | ? "Title should summarize the thread's current state, not just its initial request." | 258 | "Use a compact noun phrase or clear action phrase.", |
| 247 | : "Title should summarize the user's request, not restate it verbatim.", | 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 | ...(isRegeneration | 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 | "Return a different title from the previous title.", | 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 | message: input.message, | 276 | message: input.message, |
| 259 | ...(isRegeneration | 277 | ...(isRegeneration |
packages/contracts/src/model.ts+1| ... | @@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [ | ... | @@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [ |
| 145 | "gpt-5.6-terra", | 145 | "gpt-5.6-terra", |
| 146 | ]; | 146 | ]; |
| 147 | export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna"; | 147 | export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna"; |
| 148 | export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low"; | ||
| 148 | 149 | ||
| 149 | export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = { | 150 | export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = { |
| 150 | [CODEX_DRIVER_KIND]: DEFAULT_MODEL, | 151 | [CODEX_DRIVER_KIND]: DEFAULT_MODEL, |
packages/contracts/src/settings.test.ts+8| ... | @@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => { | ... | @@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => { |
| 112 | }); | 112 | }); |
| 113 | 113 | ||
| 114 | describe("ServerSettings.providerInstances (slice-2 invariant)", () => { | 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 | it("defaults to an empty record so legacy configs without the key still decode", () => { | 123 | it("defaults to an empty record so legacy configs without the key still decode", () => { |
| 116 | expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({}); | 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,7 +3,11 @@ import * as Duration from "effect/Duration"; |
| 3 | import * as Schema from "effect/Schema"; | 3 | import * as Schema from "effect/Schema"; |
| 4 | import * as SchemaTransformation from "effect/SchemaTransformation"; | 4 | import * as SchemaTransformation from "effect/SchemaTransformation"; |
| 5 | import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; | 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 | import { ModelSelection } from "./orchestration.ts"; | 11 | import { ModelSelection } from "./orchestration.ts"; |
| 8 | import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; | 12 | import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; |
| 9 | 13 | ||
| ... | @@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({ | ... | @@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({ |
| 498 | Effect.succeed({ | 502 | Effect.succeed({ |
| 499 | instanceId: ProviderInstanceId.make("codex"), | 503 | instanceId: ProviderInstanceId.make("codex"), |
| 500 | model: DEFAULT_TEXT_GENERATION_MODEL, | 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 | ), |