feat(config): add {file:...} syntax support for agent markdown prompts

Enable file content substitution in agent configuration prompts using the
existing `ConfigPaths.substitute` utility. This allows agent markdown files
to reference external file contents via `{file:path}` syntax, with missing
files resolving to empty strings.

Export `substitute` function from ConfigPaths to support agent config
processing.
This commit is contained in:
Maksym Mospanenko
2026-05-12 23:39:07 -03:00
committed by Catriel Müller
parent 55abf13cfa
commit 80edeee58f
2 changed files with 98 additions and 1 deletions
+10 -1
View File
@@ -13,6 +13,7 @@ import * as ConfigMarkdown from "./markdown"
import { ConfigModelID } from "./model-id"
import { ConfigParse } from "./parse"
import { ConfigPermission } from "./permission"
import { ConfigVariable } from "./variable"
// kilocode_change start
import { KilocodeConfig } from "@/kilocode/config/config"
import type { Warning } from "./config"
@@ -162,10 +163,18 @@ export async function load(dir: string, warnings?: Warning[]) {
// kilocode_change end
const name = configEntryNameFromPath(item, patterns)
const prompt = await ConfigVariable.substitute({
text: md.content.trim(),
type: "virtual",
dir: path.dirname(item),
source: item,
missing: "empty",
})
const config = {
name,
...md.data,
prompt: md.content.trim(),
prompt,
}
// kilocode_change start - use Effect schema (propertyOrder: original) + non-fatal handleInvalid
try {
@@ -0,0 +1,88 @@
import { describe, expect, test } from "bun:test"
import path from "node:path"
import fs from "node:fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
describe("agent {file:...} syntax in markdown body", () => {
test("loads file content when {file:...} is used in agent body", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "prompts", "guide.md"), "Guidelines content")
const agentsDir = path.join(dir, ".kilo", "agents")
await fs.mkdir(agentsDir, { recursive: true })
await Bun.write(
path.join(agentsDir, "test.md"),
["---", "description: Test Agent", "---", "Base prompt", "", "{file:../../prompts/guide.md}", ""].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("test")
expect(agent).toBeDefined()
expect(agent?.prompt).toContain("Base prompt")
expect(agent?.prompt).toContain("Guidelines content")
},
})
})
test("replaces missing file reference with empty string", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const agentsDir = path.join(dir, ".kilo", "agents")
await fs.mkdir(agentsDir, { recursive: true })
await Bun.write(
path.join(agentsDir, "test.md"),
["---", "description: Test Agent", "---", "Base prompt", "", "{file:missing.txt}", ""].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("test")
expect(agent).toBeDefined()
expect(agent?.prompt).toContain("Base prompt")
expect(agent?.prompt).not.toContain("{file:")
},
})
})
test("resolves multiple file references in same agent", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "a.md"), "Content A")
await Bun.write(path.join(dir, "b.md"), "Content B")
const agentsDir = path.join(dir, ".kilo", "agents")
await fs.mkdir(agentsDir, { recursive: true })
await Bun.write(
path.join(agentsDir, "test.md"),
["---", "description: Test", "---", "Base", "", "{file:../../a.md}", "{file:../../b.md}", ""].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("test")
expect(agent).toBeDefined()
expect(agent?.prompt).toContain("Content A")
expect(agent?.prompt).toContain("Content B")
},
})
})
})