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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
.lazyglm/
.clawhip/
*.log
.DS_Store
dist/
Expand Down
70 changes: 70 additions & 0 deletions docs/GLM_NATIVE_PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# GLM-Native Prompt Rationale

LazyGLM now prepends a GLM-native operating block to both the one-shot runtime
and the interactive REPL prompt. The block is intentionally additive: it keeps
the existing LazyGLM working rules/persona, then gives the model GLM-specific
context about the active catalog tier, context window, `reasoning_content`, and
tool-loop expectations.

## z.ai References

- GLM-5.2 model docs: https://docs.z.ai/guides/llm/glm-5.2
- Used for the prompt emphasis on long-horizon coding, project-scale context,
engineering-standards adherence, 1M context, and 128K maximum output.
- Thinking Mode docs: https://docs.z.ai/guides/capabilities/thinking-mode
- Used for `reasoning_content`, interleaved thinking, preserved thinking, and
the need to keep reasoning continuity across tool turns when the runtime
supplies it.
- GLM Coding Plan Quick Start: https://docs.z.ai/devpack/quick-start
- Used for Coding Plan-specific endpoint/protocol framing and the fact that
Coding Plan setup is a coding-tool experience rather than a generic chat
endpoint.
- Tool Integration URL from issue #44:
https://docs.z.ai/guides/capabilities/tool/others
- Checked during implementation, but this URL was unavailable from the docs
fetcher. This PR therefore avoids unsupported claims about that page and
relies only on the reachable GLM-5.2, Thinking Mode, and Coding Plan pages.

## Prompt Shape

The shared prompt builder in `src/prompt.js` creates:

1. `GLM-NATIVE OPERATING CONTRACT`
2. Existing one-shot working rules or REPL persona
3. Environment block
4. Hook-injected project context
5. Optional caller-supplied runtime extra text

The new top block tells the model:

- it is running as LazyGLM on GLM, not as a generic assistant;
- which model and catalog tier are active;
- what the catalog says about the model tier and intended tradeoff;
- to use GLM's long-horizon coding strengths for project context, engineering
constraints, staged execution, and verification;
- to preserve `reasoning_content` continuity when the runtime provides it;
- to inspect with tools, reason over results, and continue with small verified
steps.

## Catalog Coupling

Tier guidance is derived from `config/model-catalog.json` through the active
model's `tier`, `context_window`, and `description`. The prompt does not carry a
parallel model list. Unknown custom models degrade to a generic GLM-native block
without invented tier guidance.

## CLI Surfaces

The REPL banner and `/status` now accept optional `tier` and `tierReason`
fields:

- callers that omit tier data keep the previous output shape;
- REPL startup shows active tier and guidance when the catalog has an entry;
- `/status` shows active tier and guidance alongside existing token telemetry,
including reasoning spend.

## Deferred Scope

This foundational slice does not implement onboarding education or
`lazyglm doctor` GLM-native configuration checks. Those are separate issue #44
acceptance criteria with different user-flow and diagnostic test surfaces.
48 changes: 16 additions & 32 deletions src/agent/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import { join, dirname } from "node:path";
import { appendFile } from "node:fs/promises";
import { chat, resolveProviderConfig, shouldPreserveThinking } from "./provider.js";
import { detectRole, loadCatalog, resolveContextBudget } from "./router.js";
import { detectRole, findCatalogModelEntry, loadCatalog, resolveContextBudget } from "./router.js";
import { TOOL_SPECS, TOOL_HANDLERS } from "./tools.js";
import { Context, assistantMessageFrom } from "./context.js";
import { HookEngine } from "../hooks/engine.js";
import { gitInfo, truncate, ensureDir, nowIso } from "../util.js";
import { abortReason, composeAbortSignals, isDeadlineError, throwIfAborted, withAbort } from "./deadline.js";
import { isToolErrorResult } from "./tool-errors.js";
import { buildRuntimePrompt } from "../prompt.js";

/**
* @typedef {import("../types/index.js").ChatUsage} ChatUsage
Expand All @@ -25,37 +26,8 @@ import { isToolErrorResult } from "./tool-errors.js";
* @typedef {import("../types/index.js").ToolHandler} ToolHandler
*
* @typedef {{ isRepo: boolean, branch?: string, root?: string }} RuntimeGitInfo
* @typedef {{ cwd: string, git: RuntimeGitInfo, model: string, injects?: string[], extra?: string }} SystemPromptOptions
*/

const BASE_SYSTEM_PROMPT = `You are LazyGLM, an autonomous software engineering agent driven by a GLM model. You operate inside a real project directory on the user's machine via tools.

WORKING RULES
- Think in small, verifiable steps. Read before you write. Prefer patch_file for edits, write_file for new files.
- After making changes, run builds/tests with run_shell to verify. Never claim success without verifying.
- Use grep/list_dir/read_file to orient yourself; do not guess file contents.
- When the task is fully done and verified, call the finish tool once with a concise summary and verification instructions. Do not call finish otherwise.
- Do not narrate at length between tool calls. Act, verify, continue.
- Keep file contents complete and correct — never leave placeholders or TODOs in shipped code.

You have these tools: read_file, write_file, patch_file, list_dir, grep, run_shell, finish.`;

/**
* @param {SystemPromptOptions} options
* @returns {string}
*/
function buildSystemPrompt({ cwd, git, model, injects, extra }) {
const parts = [BASE_SYSTEM_PROMPT];
parts.push(
`\nENVIRONMENT\n- cwd: ${cwd}\n- git: ${git.isRepo ? `${git.branch} @ ${git.root}` : "(not a repo)"}\n- model: ${model}\n- date: ${nowIso()}\n- os: ${process.platform}`,
);
if (injects && injects.length) {
parts.push(`\nPROJECT CONTEXT (injected by hooks)\n${injects.join("\n\n")}`);
}
if (extra) parts.push(`\n${extra}`);
return parts.join("\n");
}

/**
* Run the GLM agent on a task.
* @param {RunAgentOptions} opts
Expand Down Expand Up @@ -96,7 +68,10 @@ export async function runAgent(opts) {
if (!resolvedModel) {
throw new Error("No GLM model resolved. Set LAZYGLM_MODEL, pass --model, or configure config/model-catalog.json.");
}
const contextBudget = budget ?? resolveContextBudget(providerConfig.model || model || resolvedModel, await loadCatalog());
const catalog = await loadCatalog();
const catalogModel = providerConfig.model || model || resolvedModel;
const catalogEntry = findCatalogModelEntry(catalogModel, catalog);
const contextBudget = budget ?? resolveContextBudget(catalogModel, catalog);

/** @param {string} message */
const hookLog = (message) => onEvent({ type: "log", message });
Expand Down Expand Up @@ -176,7 +151,16 @@ export async function runAgent(opts) {
// 1. SessionStart
const startRes = await fireRunHook("SessionStart", {});
const gi = gitInfo(cwd);
const system = buildSystemPrompt({ cwd, git: gi, model: resolvedModel, injects: startRes.injects, extra: systemPromptExtra });
const system = buildRuntimePrompt({
cwd,
git: gi,
model: resolvedModel,
injects: startRes.injects,
extra: systemPromptExtra,
tier: catalogEntry?.tier,
contextWindow: catalogEntry?.context_window ?? catalogEntry?.context,
description: catalogEntry?.description,
});
ctx.setSystem(system);
await log({ type: "system_prompt_chars", chars: system.length });

Expand Down
15 changes: 14 additions & 1 deletion src/banner.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ function centerVisible(s, width) {
return " ".repeat(left) + value + " ".repeat(right);
}

function cleanSegment(value) {
return String(value ?? "").replace(/[|\r\n]+/g, " ").replace(/\s+/g, " ").trim();
}

/** One `label value` row, padded to exactly `width` rendered columns. */
function fieldRow(label, value, width) {
const labelField = label ? String(label).padEnd(LABEL_WIDTH) : "";
Expand All @@ -121,6 +125,8 @@ function fieldRow(label, value, width) {
* @param {object} [opts.git] - { isRepo, branch, root } from gitInfo()
* @param {object} [opts.session] - { id, ... } from createSession(); omitted => no session line
* @param {boolean} [opts.yolo] - render the yolo line when true
* @param {string} [opts.tier] - catalog tier for the active model
* @param {string} [opts.tierReason] - catalog-derived tier guidance
* @param {boolean} [opts.isTTY] - true => art + panel; false/undefined => one clean line
* @returns {string} the full banner (no console writes, no process reads)
*/
Expand All @@ -131,6 +137,8 @@ export function renderBanner({
git,
session,
yolo,
tier,
tierReason,
isTTY,
} = {}) {
const m = model ?? "?";
Expand All @@ -141,7 +149,10 @@ export function renderBanner({

// Non-TTY: one machine-readable line, no ANSI, no art.
if (!tty) {
return `LazyGLM | ${m} | ${p} | ${dir}\n`;
const parts = ["LazyGLM", m, p, dir];
if (tier) parts.push(`tier=${cleanSegment(tier)}`);
if (tierReason) parts.push(`guidance=${cleanSegment(tierReason)}`);
return `${parts.join(" | ")}\n`;
}

const out = [];
Expand All @@ -158,6 +169,8 @@ export function renderBanner({
const b = BOX.tty;
const rows = [];
rows.push(fieldRow("model", m, PANEL_WIDTH));
if (tier) rows.push(fieldRow("tier", tier, PANEL_WIDTH));
if (tierReason) rows.push(fieldRow("guidance", tierReason, PANEL_WIDTH));
rows.push(fieldRow("provider", p, PANEL_WIDTH));
rows.push(fieldRow("cwd", dir, PANEL_WIDTH));
if (g.isRepo && g.branch) rows.push(fieldRow("git", g.branch, PANEL_WIDTH));
Expand Down
141 changes: 141 additions & 0 deletions src/prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// @ts-check

// Side-effect-free prompt composition for the one-shot runtime and interactive
// REPL. Model/tier facts are supplied by callers from config/model-catalog.json.
import { nowIso } from "./util.js";

/**
* @typedef {{ isRepo: boolean, branch?: string, root?: string }} PromptGitInfo
* @typedef {object} PromptOptions
* @property {string} cwd
* @property {PromptGitInfo} git
* @property {string} model
* @property {string[]} [injects]
* @property {string} [extra]
* @property {string} [tier]
* @property {number|string} [contextWindow]
* @property {string} [description]
*/

export const RUNTIME_WORKING_PROMPT = `You are LazyGLM, an autonomous software engineering agent driven by a GLM model. You operate inside a real project directory on the user's machine via tools.

WORKING RULES
- Think in small, verifiable steps. Read before you write. Prefer patch_file for edits, write_file for new files.
- After making changes, run builds/tests with run_shell to verify. Never claim success without verifying.
- Use grep/list_dir/read_file to orient yourself; do not guess file contents.
- When the task is fully done and verified, call the finish tool once with a concise summary and verification instructions. Do not call finish otherwise.
- Do not narrate at length between tool calls. Act, verify, continue.
- Keep file contents complete and correct - never leave placeholders or TODOs in shipped code.

You have these tools: read_file, write_file, patch_file, list_dir, grep, run_shell, finish.`;

export const REPL_PERSONA_PROMPT = `You are LazyGLM, a terminal-based AI coding agent connected directly to the user's file system via a CLI.

PERSONALITY:
You are a brilliant but "lazy" pragmatic developer. You hate writing unnecessary text, explanations, or filler. You believe code speaks louder than words. You do exactly what is asked, make the edit, and stop talking. Never say "Certainly!" or "I'd be happy to help." Just do the work. Be extremely concise. If the user didn't ask for an explanation, don't give one.

HOW YOU OPERATE (agentic - you have tools):
- To edit a file, use the patch_file tool (SEARCH/REPLACE: old_string -> new_string). Never output whole files. Never paste SEARCH/REPLACE blocks into chat - invoke the tool.
- To see a file, use read_file / list_dir / grep autonomously. Do NOT ask the user to @mention or paste files - go look yourself.
- After making changes, verify with run_shell (build/test). Never claim success without verifying.
- Keep your terminal output clean and readable.
- When the user's request is fully done, call the finish tool with a one-line summary.`;

/** @type {Record<string, string>} */
const TIER_GUIDANCE = {
"high-end": "Use this tier for long-horizon coding, architecture, complex debugging, and work that benefits from the largest GLM context.",
"high-end-fast": "Use this tier when the task still needs strong coding ability but should spend fewer latency/cost resources than the flagship route.",
strong: "Use this tier for general implementation and review where full flagship context is not required.",
balanced: "Use this tier for verification, medium-complexity changes, and routine coding turns.",
fast: "Use this tier for quick edits, listings, simple lookups, and low-latency helper work.",
};

/**
* @param {{ tier?: string, description?: string }} [info]
* @returns {string}
*/
export function modelTierGuidance(info = {}) {
const tier = info.tier || "";
const tierText = TIER_GUIDANCE[tier] || "";
const description = info.description ? String(info.description).trim() : "";
if (tierText && description) return `${tierText} Catalog note: ${description}`;
return tierText || description;
}

/**
* @param {number|string|undefined} value
* @returns {string}
*/
function formatContextWindow(value) {
const n = Number(value);
if (Number.isFinite(n) && n > 0) return n.toLocaleString("en-US");
return value ? String(value) : "catalog entry unavailable";
}

/**
* @param {{ model?: string, tier?: string, contextWindow?: number|string, description?: string }} [options]
* @returns {string}
*/
export function buildGlmNativeBlock({ model, tier, contextWindow, description } = {}) {
const guidance = modelTierGuidance({ tier, description });
const lines = [
"GLM-NATIVE OPERATING CONTRACT",
`- You are running on GLM through LazyGLM. Treat this as a GLM-native coding-agent session, not a generic assistant session.`,
`- Active model: ${model || "unknown"}${tier ? ` (${tier})` : ""}. Context window: ${formatContextWindow(contextWindow)} tokens.`,
];
if (guidance) lines.push(`- Tier guidance: ${guidance}`);
lines.push(
"- Play to GLM's documented strengths: preserve long-horizon project context, carry forward engineering constraints, and close tasks through implementation plus verification.",
"- GLM thinking may arrive as reasoning_content. Preserve reasoning continuity across tool turns when the runtime supplies it, but keep user-facing summaries concise and avoid exposing raw hidden reasoning unless the UI explicitly streams it.",
"- Follow z.ai Coding Plan tool-loop conventions: inspect with tools, reason over tool results before the next action, then continue with the smallest verified step.",
);
return lines.join("\n");
}

/**
* @param {PromptOptions} options
* @returns {string}
*/
function buildEnvironmentBlock({ cwd, git, model }) {
return [
"ENVIRONMENT",
`- cwd: ${cwd}`,
`- git: ${git?.isRepo ? `${git.branch} @ ${git.root}` : "(not a repo)"}`,
`- model: ${model}`,
`- date: ${nowIso()}`,
`- os: ${process.platform}`,
].join("\n");
}

/**
* @param {PromptOptions} options
* @returns {string}
*/
export function buildRuntimePrompt(options) {
const parts = [
buildGlmNativeBlock(options),
RUNTIME_WORKING_PROMPT,
buildEnvironmentBlock(options),
];
if (options.injects && options.injects.length) {
parts.push(`PROJECT CONTEXT (injected by hooks)\n${options.injects.join("\n\n")}`);
}
if (options.extra) parts.push(options.extra);
return parts.join("\n\n");
}

/**
* @param {PromptOptions} options
* @returns {string}
*/
export function buildReplPrompt(options) {
const parts = [
buildGlmNativeBlock(options),
REPL_PERSONA_PROMPT,
buildEnvironmentBlock(options),
];
if (options.injects && options.injects.length) {
parts.push(`PROJECT CONTEXT (injected by hooks)\n${options.injects.join("\n\n")}`);
}
return parts.join("\n\n");
}
Loading
Loading