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
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
' "Reconnect failures after restart because the session state does not recover" ',
},
}),
stdinMustContain: "You write concise thread titles for coding conversations.",
stdinMustContain: "Please investigate reconnect failures after restarting the session.",
},
(textGeneration) =>
Effect.gen(function* () {
Expand Down
11 changes: 7 additions & 4 deletions apps/server/src/textGeneration/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { type CodexSettings, type ModelSelection } from "@t3tools/contracts";
import {
type CodexSettings,
DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
type ModelSelection,
TextGenerationError,
} from "@t3tools/contracts";
import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
import { resolveSpawnCommand } from "@t3tools/shared/shell";

import { resolveAttachmentPath } from "../attachmentStore.ts";
import * as ServerConfig from "../config.ts";
import { expandHomePath } from "../pathExpansion.ts";
import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts";
import { TextGenerationError } from "@t3tools/contracts";
import * as TextGeneration from "./TextGeneration.ts";
import {
buildBranchNamePrompt,
Expand All @@ -33,7 +37,6 @@ import {
import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts";

const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
const CODEX_TIMEOUT_MS = 180_000;
const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
/**
Expand Down Expand Up @@ -178,7 +181,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment);
const reasoningEffort =
getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ??
CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT;
DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
const serviceTier = getCodexServiceTierOptionValue(modelSelection);
const spawnCommand = yield* resolveSpawnCommand(
codexConfig.binaryPath || "codex",
Expand Down
17 changes: 14 additions & 3 deletions apps/server/src/textGeneration/TextGenerationPrompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ describe("buildThreadTitlePrompt", () => {
expect(result.prompt).toContain("User message:");
expect(result.prompt).toContain("Investigate reconnect regressions after session restore");
expect(result.prompt).not.toContain("Attachment metadata:");
expect(result.prompt).toContain(
"Generate a title that will help the user recognize this T3 Code thread weeks later.",
);
expect(result.prompt).toContain(
"Title the subject and outcome. Discard incidental instructions.",
);
expect(result.prompt).toContain(
"Name the product change, not the mock, plan, report, branch, or PR used to produce it.",
);
expect(result.prompt).not.toContain(
"Title should summarize the user's request, not restate it verbatim.",
);
});

it("includes attachment metadata when attachments are provided", () => {
Expand Down Expand Up @@ -183,12 +195,11 @@ describe("buildThreadTitlePrompt", () => {
});

expect(result.prompt).toContain(
"The user requested a new title based on the contents of this thread.",
"Generate a new title that will help the user recognize this T3 Code thread weeks later.",
);
expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".');
expect(result.prompt).toContain("better represents the current state of the thread");
expect(result.prompt).toContain(
"Capture the thread's intent, not a PR number or other superficial detail.",
"Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.",
);
expect(result.prompt).toContain("Thread contents:");
expect(result.prompt).toContain("The remaining issue is stale session state");
Expand Down
44 changes: 31 additions & 13 deletions apps/server/src/textGeneration/TextGenerationPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export interface BranchNamePromptInput {
interface PromptFromMessageInput {
instruction: string;
responseShape: string;
guidance?: ReadonlyArray<string> | undefined;
rulesLabel?: string | undefined;
rules: ReadonlyArray<string>;
message: string;
messageLabel?: string | undefined;
Expand Down Expand Up @@ -177,7 +179,8 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string {
const promptSections = [
input.instruction,
input.responseShape,
"Rules:",
...(input.guidance ?? []),
input.rulesLabel ?? "Rules:",
...input.rules.map((rule) => `- ${rule}`),
"",
`${input.messageLabel ?? "User message"}:`,
Expand Down Expand Up @@ -234,26 +237,41 @@ export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) {
const prompt = buildPromptFromMessage({
instruction: isRegeneration
? [
"You write concise thread titles for coding conversations.",
"The user requested a new title based on the contents of this thread.",
"Generate a new title that will help the user recognize this T3 Code thread weeks later.",
`The previous title was ${JSON.stringify(input.previousTitle)}.`,
"Come up with a new title that better represents the current state of the thread.",
].join("\n")
: "You write concise thread titles for coding conversations.",
responseShape: "Return a JSON object with key: title.",
: "Generate a title that will help the user recognize this T3 Code thread weeks later.",
responseShape: "Return JSON with exactly one key: title.",
guidance: [
"",
"Before answering, silently reduce the request to:",
"- Subject: What system, feature, or problem is this really about?",
"- Outcome: What does the user ultimately want to understand or change?",
"- Incidental instructions: What only describes how the agent should do the work?",
"",
"Title the subject and outcome. Discard incidental instructions.",
"",
],
rulesLabel: "Editorial rules:",
rules: [
isRegeneration
? "Title should summarize the thread's current state, not just its initial request."
: "Title should summarize the user's request, not restate it verbatim.",
"3-8 words, fewer than 40 characters.",
"Use a compact noun phrase or clear action phrase.",
"Capture the umbrella goal when the request lists several symptoms or steps.",
"Name the product change, not the mock, plan, report, branch, or PR used to produce it.",
"Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.",
'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.',
"For research, name the question domain rather than the requested research process.",
"Do not claim the work is complete.",
"Do not copy and truncate the user's message.",
"Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.",
"Use attached images as primary context for UI issues.",
"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.",
...(isRegeneration
? [
"Capture the thread's intent, not a PR number or other superficial detail.",
"Capture the current durable subject and outcome across the whole thread, not merely its initial request or latest step.",
"Return a different title from the previous title.",
]
: []),
"Keep it short and specific (3-8 words).",
"Avoid quotes, filler, prefixes, and trailing punctuation.",
"If images are attached, use them as primary context for visual/UI issues.",
],
message: input.message,
...(isRegeneration
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [
"gpt-5.6-terra",
];
export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-5.6-luna";
export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low";

export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
[CODEX_DRIVER_KIND]: DEFAULT_MODEL,
Expand Down
8 changes: 8 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ describe("ClientSettings sidebar v2", () => {
});

describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
it("defaults text generation to Luna at low reasoning effort", () => {
expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.6-luna",
options: [{ id: "reasoningEffort", value: "low" }],
});
});

it("defaults to an empty record so legacy configs without the key still decode", () => {
expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});
});
Expand Down
12 changes: 11 additions & 1 deletion packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import * as Duration from "effect/Duration";
import * as Schema from "effect/Schema";
import * as SchemaTransformation from "effect/SchemaTransformation";
import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
import { DEFAULT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts";
import {
DEFAULT_TEXT_GENERATION_MODEL,
DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
ProviderOptionSelections,
} from "./model.ts";
import { ModelSelection } from "./orchestration.ts";
import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts";

Expand Down Expand Up @@ -498,6 +502,12 @@ export const ServerSettings = Schema.Struct({
Effect.succeed({
instanceId: ProviderInstanceId.make("codex"),
model: DEFAULT_TEXT_GENERATION_MODEL,
options: [
{
id: "reasoningEffort",
value: DEFAULT_TEXT_GENERATION_REASONING_EFFORT,
},
],
}),
),
),
Expand Down
Loading