diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5c297f8d6..8bbb99a53 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added the GLM 5.2 prompt preset with automatic model detection. + ### Fixed - Fixed inherited Anthropic same-model replay to preserve provider-native server tool blocks around signed thinking, avoiding `thinking` / `redacted_thinking` modification errors on follow-up tool-result requests. diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts new file mode 100644 index 000000000..541dd2d66 --- /dev/null +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/glm-5-2.ts @@ -0,0 +1,15 @@ +import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts"; + +function buildGlm52Tuning(): string { + return `You are running on GLM 5.2: Opus 4.6-class agent behavior tuned toward Fable 5 decisiveness and GPT 5.5 outcome-first coding. Apply literal scopes literally - "every", "all", and "for each" mean the full set. Prefer sufficient context over exhaustive context, pick minor decisions and note them, and use matching tools or skills immediately instead of under-reaching. + +Calibrate deliberation. Use extended reasoning only for genuine multi-step uncertainty; routine classification, file edits, and lookups should be decided directly. A cheap tool call beats long internal debate: act, inspect evidence, and verify. + +Code toward the destination: define the outcome, constraints, and stopping condition, then work without mechanical step-by-step recitation. In ultrawork mode, maintain absolute certainty discipline: preserve the goal, prove completion with evidence, and do not deliver partial work. + +The intent gate routing line is non-optional every turn. For non-trivial tasks, call todowrite with atomic items before starting, keep exactly one item in progress, and complete each item immediately when done.`; +} + +export function buildGlm52Prompt(options: BuildDynamicSystemPromptOptions): string { + return buildDynamicSystemPrompt({ ...options, tuningSection: buildGlm52Tuning() }); +} diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts index 6487a546f..ee27f0d90 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/presets.ts @@ -3,6 +3,7 @@ import type { BuildDynamicSystemPromptOptions } from "../../../dynamic-prompt/bu import { buildClaudeOpus45Prompt } from "./claude-opus-4-5.ts"; import { buildClaudeOpus46Prompt } from "./claude-opus-4-6.ts"; import { buildClaudeOpus47Prompt } from "./claude-opus-4-7.ts"; +import { buildGlm52Prompt } from "./glm-5-2.ts"; import { buildGpt52Prompt } from "./gpt-5.2.ts"; import { buildGpt53CodexPrompt } from "./gpt-5.3-codex.ts"; import { buildGpt54Prompt } from "./gpt-5.4.ts"; @@ -64,6 +65,14 @@ function isKimiK27Model(model: ModelWithPromptPresetMetadata): boolean { return hasKimiK27Signal(model.id) || (model.name !== undefined && hasKimiK27Signal(model.name)); } +function hasGlm52Signal(value: string): boolean { + return /(?:^|[/@._-])glm(?:[._-]|p)5(?:[._-]|p)2(?:$|[/@._:-])/.test(normalizeModelId(value)); +} + +function isGlm52Model(model: ModelWithPromptPresetMetadata): boolean { + return hasGlm52Signal(model.id) || (model.name !== undefined && hasGlm52Signal(model.name)); +} + type ClaudeOpusVersion = "claude-opus-4-7" | "claude-opus-4-6" | "claude-opus-4-5"; function extractClaudeOpusVersion(modelId: string): ClaudeOpusVersion | undefined { @@ -107,6 +116,9 @@ export function resolvePresetName( if (claudeVersion) { return claudeVersion; } + if (isGlm52Model(model)) { + return "glm-5.2"; + } return undefined; } @@ -122,6 +134,8 @@ function buildPreset(name: ResolvedPresetName, options: BuildDynamicSystemPrompt return { name, prompt: buildGpt52Prompt(options) }; case "gpt-5": return { name, prompt: buildGpt5Prompt(options) }; + case "glm-5.2": + return { name, prompt: buildGlm52Prompt(options) }; case "kimi-k2-7": return { name, prompt: buildKimiK27Prompt(options) }; case "kimi-k2-6": diff --git a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts index 997a77d82..8b9b3aabe 100644 --- a/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts +++ b/packages/coding-agent/src/core/extensions/builtin/prompt-preset/settings.ts @@ -5,6 +5,7 @@ export type PromptPresetName = | "claude-opus-4-7" | "claude-opus-4-6" | "claude-opus-4-5" + | "glm-5.2" | "kimi-k2-7" | "kimi-k2-6" | "gpt-5" @@ -24,6 +25,7 @@ const VALID_PRESETS: ReadonlySet = new Set([ "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", + "glm-5.2", "kimi-k2-7", "kimi-k2-6", "gpt-5", diff --git a/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts b/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts new file mode 100644 index 000000000..f9d78716a --- /dev/null +++ b/packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts @@ -0,0 +1,107 @@ +import { type Api, getModels, getProviders, type Model } from "@earendil-works/pi-ai"; +import { describe, expect, it } from "vitest"; +import { + type PromptPresetSettings, + resolvePreset, + resolvePresetName, +} from "../../src/core/extensions/builtin/prompt-preset/presets.ts"; + +function createModel(id: string, provider: string, api: Api = "openai-responses"): Model { + return { + id, + name: id, + api, + provider, + baseUrl: "https://example.com/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; +} + +function hasGlm52CatalogSignal(model: Model): boolean { + const searchable = `${model.id} ${model.name}`.toLowerCase().replace(/\s+/g, "-"); + return /(?:^|[/@._-])glm(?:[._-]|p)5(?:[._-]|p)2(?:$|[/@._:-])/.test(searchable); +} + +function getGlm52CatalogModels(): Model[] { + return getProviders().flatMap((provider) => (getModels(provider) as Model[]).filter(hasGlm52CatalogSignal)); +} + +describe("GLM 5.2 prompt preset", () => { + it.each([ + "zai-org/glm-5.2", + "glm-5.2", + "GLM 5.2", + "zai-org/glm-5p2", + "zai-org/glm_5_2:thinking", + ])("resolves %s to the glm-5.2 preset", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter", "openai-responses"); + + // when + const preset = resolvePreset(model, settings); + + // then + expect(preset?.name).toBe("glm-5.2"); + expect(preset?.prompt).toContain("running on GLM 5.2"); + expect(preset?.prompt).toContain("absolute certainty"); + expect(preset?.prompt).toContain("todowrite"); + expect(preset?.prompt).not.toContain("apply_patch"); + }); + + it.each([ + "glm-4.6", + "zai-org/glm-4.5", + "some-glm-compatible-router", + ])("does not route %s to the glm-5.2 preset", (modelId) => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const model = createModel(modelId, "openrouter", "openai-responses"); + + // when + const preset = resolvePreset(model, settings); + + // then + expect(preset).toBeUndefined(); + }); + + it("allows settings.json to force glm-5.2 regardless of model id", () => { + // given + const settings: PromptPresetSettings = { promptPreset: "glm-5.2" }; + const model = createModel("some-random-model", "custom", "openai-responses"); + + // when + const preset = resolvePreset(model, settings); + + // then + expect(preset?.name).toBe("glm-5.2"); + expect(preset?.prompt).toContain("running on GLM 5.2"); + }); + + it("returns glm-5.2 preset for every GLM 5.2 built-in catalog model", () => { + // given + const settings: PromptPresetSettings = { promptPreset: "auto" }; + const catalogModels = getGlm52CatalogModels(); + const catalogModelIds = catalogModels.map((model) => `${model.provider}/${model.id}`); + + // when + const misses = catalogModels + .filter((model) => resolvePresetName(model, settings) !== "glm-5.2") + .map((model) => `${model.provider}/${model.id}`); + + // then + expect(catalogModelIds).toEqual( + expect.arrayContaining([ + "cloudflare-workers-ai/@cf/zai-org/glm-5.2", + "fireworks/accounts/fireworks/models/glm-5p2", + "openrouter/z-ai/glm-5.2", + "zai/glm-5.2", + ]), + ); + expect(misses).toEqual([]); + }); +});