mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
fix(cli): preserve configured subagent routing (#12652)
* fix(cli): preserve configured subagent routing * fix(cli): preserve markdown agent precedence * ci: rerun checks
This commit is contained in:
@@ -490,6 +490,7 @@ export const layer = Layer.effect(
|
||||
result = mergeConfigConcatArrays(result, { agent: orgModes.agents })
|
||||
}
|
||||
warnings.push(...orgModes.warnings)
|
||||
let configuredAgents = { ...(result.agent ?? {}) }
|
||||
// kilocode_change end
|
||||
|
||||
const authEnv: Record<string, string> = {}
|
||||
@@ -549,6 +550,7 @@ export const layer = Layer.effect(
|
||||
const trusted = sourceTrusted ?? scope === "global"
|
||||
const scoped = KilocodeConfig.scopeIndexing(SandboxConfig.scope(next, scope), scope)
|
||||
result = mergeConfigConcatArrays(result, scoped)
|
||||
if (scoped.agent) configuredAgents = mergeDeep(configuredAgents, scoped.agent)
|
||||
if (next.instructions?.length) {
|
||||
result.instruction_origins = origins(result.instruction_origins, next.instructions, trusted, source)
|
||||
}
|
||||
@@ -765,13 +767,15 @@ export const layer = Layer.effect(
|
||||
result.command ?? {},
|
||||
yield* Effect.promise(() => ConfigCommand.load(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)),
|
||||
)
|
||||
result.agent = mergeDeep(
|
||||
result.agent = KilocodeConfig.mergeAgentMarkdown(
|
||||
result.agent ?? {},
|
||||
yield* Effect.promise(() => ConfigAgent.load(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)),
|
||||
configuredAgents,
|
||||
)
|
||||
result.agent = mergeDeep(
|
||||
result.agent = KilocodeConfig.mergeAgentMarkdown(
|
||||
result.agent ?? {},
|
||||
yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings, dirTrusted, dirFileScope, dirSourceScope)),
|
||||
configuredAgents,
|
||||
)
|
||||
// kilocode_change end
|
||||
// kilocode_change - Auto-discovered plugins under config directories are already local files, so ConfigPlugin.load
|
||||
|
||||
@@ -181,6 +181,35 @@ export namespace KilocodeConfig {
|
||||
return stripGlobalIndexing(info)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge discovered agent markdown while preserving routing explicitly defined
|
||||
* in config. Tracking config entries separately keeps normal directory
|
||||
* precedence between markdown files intact.
|
||||
*/
|
||||
export function mergeAgentMarkdown(
|
||||
existing: Record<string, ConfigAgentV1.Info>,
|
||||
incoming: Record<string, ConfigAgentV1.Info>,
|
||||
configured: Record<string, ConfigAgentV1.Info>,
|
||||
) {
|
||||
const result = { ...existing }
|
||||
for (const [name, agent] of Object.entries(incoming)) {
|
||||
const current = result[name]
|
||||
if (!current) {
|
||||
result[name] = agent
|
||||
continue
|
||||
}
|
||||
|
||||
const config = configured[name]
|
||||
if (agent.mode === "primary" && config && config.mode !== "primary") {
|
||||
result[name] = mergeDeep(mergeDeep(current, agent), { ...config, mode: config.mode ?? "all" })
|
||||
continue
|
||||
}
|
||||
|
||||
result[name] = mergeDeep(current, agent)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function retireIndexingFlag(info: Record<string, unknown>, source: string) {
|
||||
if (!isRecord(info.experimental) || !("semantic_indexing" in info.experimental)) return info
|
||||
const experimental = { ...info.experimental }
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { afterEach, expect, test } from "bun:test"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import {
|
||||
disposeAllInstances,
|
||||
provideInstance,
|
||||
provideTestInstance,
|
||||
testInstanceStoreLayer,
|
||||
tmpdir,
|
||||
} from "../fixture/fixture"
|
||||
|
||||
function load(dir: string) {
|
||||
return Effect.runPromise(
|
||||
provideInstance(dir)(Agent.Service.use((svc) => svc.get("architect"))).pipe(
|
||||
Effect.provide(Agent.defaultLayer),
|
||||
Effect.provide(testInstanceStoreLayer),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
|
||||
test("config subagent routing survives a colliding primary agent markdown file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
config: {
|
||||
agent: {
|
||||
architect: {
|
||||
mode: "subagent",
|
||||
model: "test/configured-subagent",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await Filesystem.write(
|
||||
path.join(tmp.path, ".kilo", "agents", "architect.md"),
|
||||
[
|
||||
"---",
|
||||
"mode: primary",
|
||||
"description: Marketplace architect",
|
||||
"---",
|
||||
"",
|
||||
"You are the marketplace architect.",
|
||||
].join("\n"),
|
||||
)
|
||||
|
||||
const item = await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: () => load(tmp.path),
|
||||
})
|
||||
|
||||
expect(item?.mode).toBe("subagent")
|
||||
expect(String(item?.model?.providerID)).toBe("test")
|
||||
expect(String(item?.model?.modelID)).toBe("configured-subagent")
|
||||
expect(item?.description).toBe("Marketplace architect")
|
||||
})
|
||||
|
||||
test("config-only custom agent keeps its default all mode across a primary collision", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
config: {
|
||||
agent: {
|
||||
architect: {
|
||||
model: "test/configured-subagent",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await Filesystem.write(
|
||||
path.join(tmp.path, ".kilo", "agents", "architect.md"),
|
||||
["---", "mode: primary", "---", "", "You are the marketplace architect."].join("\n"),
|
||||
)
|
||||
|
||||
const item = await provideTestInstance({
|
||||
directory: tmp.path,
|
||||
fn: () => load(tmp.path),
|
||||
})
|
||||
|
||||
expect(item?.mode).toBe("all")
|
||||
expect(String(item?.model?.providerID)).toBe("test")
|
||||
expect(String(item?.model?.modelID)).toBe("configured-subagent")
|
||||
})
|
||||
|
||||
test("higher-priority markdown can override lower-priority markdown routing", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const project = path.join(tmp.path, "project")
|
||||
const global = path.join(tmp.path, "global")
|
||||
await Filesystem.write(
|
||||
path.join(global, "agents", "architect.md"),
|
||||
["---", "mode: subagent", "description: Lower-priority architect", "---"].join("\n"),
|
||||
)
|
||||
await Filesystem.write(
|
||||
path.join(project, ".kilo", "agents", "architect.md"),
|
||||
["---", "mode: primary", "description: Higher-priority architect", "---"].join("\n"),
|
||||
)
|
||||
|
||||
const previous = Global.Path.config
|
||||
;(Global.Path as { config: string }).config = global
|
||||
try {
|
||||
const item = await provideTestInstance({
|
||||
directory: project,
|
||||
fn: () => load(project),
|
||||
})
|
||||
|
||||
expect(item?.mode).toBe("primary")
|
||||
expect(item?.description).toBe("Higher-priority architect")
|
||||
} finally {
|
||||
;(Global.Path as { config: string }).config = previous
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user