Skip to content

Conversation

@ogulcancelik
Copy link
Contributor

adds {{tools}}, {{context}}, {{skills}} template variables for SYSTEM.md.

when present, replaced with dynamic content. no templates = full replacement (current behavior preserved).

also tracks injection metadata through sdk → session → ui, so the welcome screen only shows "loaded context/skills" when actually injected into the system prompt.

@ogulcancelik ogulcancelik changed the title feat: add template variables to custom system prompts feat(coding-agent): add template variables to custom system prompts Jan 22, 2026
@ogulcancelik ogulcancelik force-pushed the feat/system-prompt-template-variables branch from a4676f6 to 7e06aff Compare January 22, 2026 00:09
@badlogic
Copy link
Owner

Thanks! This touches a lot of stuff that's been changed in the great refactor, I will integrate this manually once the refactor is complete.

@badlogic
Copy link
Owner

Also need to integrate this #896

@ogulcancelik
Copy link
Contributor Author

Also need to integrate this #896

let me know if you'd like me to tackle that

@scutifer
Copy link
Contributor

The .replace is simple and robust, but I wonder if a minor extension would allow a lot more functionality without bringing in Moustache or Handlebars or NJK.

Something like this for example:

function render(template: string, context: Record<string, any>): string {
  // 1. Resolve nested paths (e.g., "user.name")
  const resolve = (path: string, ctx: any) => 
    path.trim().split('.').reduce((obj, key) => obj?.[key], ctx);

  // 2. Handle Logic Blocks: {% if ... %} and {% for ... %}
  // This regex matches {% tag condition %} content {% endtag %}
  let output = template.replace(
    /{%\s+(if|for)\s+(.*?)\s+%}([\s\S]*?){%\s+end\1\s+%}/g,
    (_, type, condition, inner) => {
      if (type === "if") {
        return resolve(condition, context) ? render(inner, context) : "";
      } else {
        // For loops: expects syntax "item in list"
        const [itemKey, , listKey] = condition.split(" ");
        const list = resolve(listKey, context);
        if (!Array.isArray(list)) return "";
        return list
          .map((item) => render(inner, { ...context, [itemKey]: item }))
          .join("");
      }
    }
  );

  // 3. Handle Variable Interpolation: {{ variable }}
  return output.replace(/{{(.*?)}}/g, (_, path) => {
    const value = resolve(path, context);
    return value !== undefined && value !== null ? String(value) : "";
  });
}

You would use it like this

const context = {
    cwd: resolvedCwd,
    tools: tools.map(t => ({ name: t, desc: toolDescriptions[t] })),
    hasSkills: skills.length > 0,
    skills: skills,
    contextFiles: contextFiles
};

const template = `
You are an expert assistant working in {{ cwd }}.

Available Tools:
{% for tool in tools %}
- {{ tool.name }}: {{ tool.desc }}
{% endfor %}

{% if hasSkills %}
# Skills
{{ skills }}
{% endif %}

# Project Context
{% for file in contextFiles %}
## {{ file.path }}
{{ file.content }}
{% endfor %}
`;

const systemPrompt = render(template, context);

Then you move the actual string to a file. Users can even bring custom templates in the future via extensions.

adds {{tools}}, {{context}}, {{skills}} template variables for SYSTEM.md.
when present, content is injected at variable location.
no template variables = full replacement (no automatic appending).

also tracks injection state so UI only shows 'Loaded context/skills'
when they were actually injected into the system prompt.
@ogulcancelik ogulcancelik force-pushed the feat/system-prompt-template-variables branch from 7e06aff to ed90a22 Compare January 22, 2026 19:13
@ogulcancelik
Copy link
Contributor Author

@badlogic rebased/refactored implementation after your resourceloader rework.

regarding #896 - i can add that to this pr too, but wanted to check first since it feels like a design choice. currently system prompts aren't stored in sessions at all (no SystemPromptEntry type, nothing in SessionContext). adding persistence would mean storing the prompt when --system-prompt is used and restoring on resume. but then what happens if you want to resume a session with a different prompt? e.g. you updated SYSTEM.md and want existing sessions to pick up the new version. happy to implement either way, just wanted to confirm intended behavior.

@scutifer yes that approach feels a lot comprehensive, though again design choice. i just want my custom system prompt in there for now, either way works for me.

@airtonix
Copy link
Contributor

i'd prefer to have use of nunjucks tbh, means less core churn and more flexibility for end users to extend in their own way

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants