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
5 changes: 5 additions & 0 deletions .changeset/ask-switch-back-reminder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Keep the model's behaviour in sync with the selected agent after switching. Every agent change now adds one reminder that names the previous and current agent and states whether the current agent may modify files, based on its configured permissions. This covers built-in, custom, and organization agents, and fixes Ask still trying to edit files after switching from Code back to Ask.
5 changes: 5 additions & 0 deletions packages/opencode/src/kilocode/session/agent-switch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<system-reminder>
The active agent has changed from ${prior} to ${current}.
This supersedes earlier agent-switch reminders. Your instructions and permissions are those of the ${current} agent as given in the current system prompt; earlier turns reflect the previous agent, not your current capabilities.
${capability}
</system-reminder>
6 changes: 0 additions & 6 deletions packages/opencode/src/kilocode/session/ask-code-switch.txt

This file was deleted.

84 changes: 84 additions & 0 deletions packages/opencode/src/kilocode/session/mode-reminders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Effect } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { PartID } from "@/session/schema"
import { Session } from "@/session/session"
import AGENT_SWITCH from "./agent-switch.txt"
import { KiloSessionPrompt } from "./prompt"

/**
* Single policy for the mode reminders sent to the model.
*
* Every Plan turn re-injects its planning instructions (transient). Every
* agent change adds one persisted switch reminder that names both agents, so
* the newest reminder always supersedes older ones, for built-in, custom, and
* organization agents alike. The capability line is derived from the
* permission ruleset for native agents only; their rulesets are known and a
* catch-all edit rule describes them accurately. Custom and organization
* agents get a neutral line, because scoped allowlists (for example
* `docs/**`) cannot be summarised safely from a catch-all check.
*/
export namespace KiloModeReminders {
export type Target = Pick<Agent.Info, "name" | "displayName" | "permission" | "native">

export const READONLY =
"This agent cannot modify files or run mutating commands. If a request needs implementation, suggest switching to an agent that can."
export const WRITABLE = "This agent may modify files and run commands within its configured permissions."
export const NEUTRAL =
"Use the tools according to this agent's configured permissions; the permission system reports what is allowed."

export function writable(agent: Target) {
return Permission.evaluate("edit", "*", agent.permission).action !== "deny"
}

export function capability(agent: Target) {
if (agent.native !== true) return NEUTRAL
return writable(agent) ? WRITABLE : READONLY
}

/** Switch reminder text for the current turn, or undefined when the agent did not change. */
export function transition(input: { current: Target; prior?: string }) {
const prior = input.prior?.trim()
if (!prior || prior.toLowerCase() === input.current.name.toLowerCase()) return
const text = AGENT_SWITCH.replaceAll("${prior}", () => prior)
.replaceAll("${current}", () => input.current.displayName ?? input.current.name)
.replaceAll("${capability}", () => capability(input.current))
return `\n\n${text}`
}

export const apply = Effect.fn("KiloModeReminders.apply")(function* (input: {
messages: SessionV1.WithParts[]
agent: Agent.Info
session: Session.Info
}) {
const sessions = yield* Session.Service
const user = input.messages.findLast((msg) => msg.info.role === "user")
if (!user) return input.messages

yield* Effect.promise(() =>
KiloSessionPrompt.insertPlanReminders({
agent: input.agent,
session: input.session,
userMessage: user,
messages: input.messages,
}),
)

const prior = input.messages.findLast((msg) => msg.info.id !== user.info.id)
const text = transition({ current: input.agent, prior: prior?.info.agent })
if (!text) return input.messages
if (user.parts.some((part) => part.type === "text" && part.text === text)) return input.messages

const part = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.info.id,
sessionID: user.info.sessionID,
type: "text",
text,
synthetic: true,
})
user.parts.push(part)
return input.messages
})
}
20 changes: 0 additions & 20 deletions packages/opencode/src/kilocode/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { MemoryPaths } from "@kilocode/kilo-memory/effect/paths"
import { MemoryMarker } from "@/kilocode/memory/marker"
import { KilocodeSystemPrompt } from "@/kilocode/system-prompt"
import { KiloToolRegistry } from "@/kilocode/tool/registry"
import ASK_CODE_SWITCH from "./ask-code-switch.txt"
import { consumeAutoTitle, markAutoTitle } from "@/kilo-sessions/rename-adoptions"

export namespace KiloSessionPrompt {
Expand Down Expand Up @@ -563,25 +562,6 @@ export namespace KiloSessionPrompt {
add(`\n\n<system-reminder>\n${body}\n</system-reminder>`)
}

export function insertAgentSwitchReminder(input: {
agent: { name: string }
userMessage: MessageV2.WithParts
messages: MessageV2.WithParts[]
}) {
if (mode(input.agent.name) !== "code") return
const prior = input.messages.findLast((msg) => msg.info.id !== input.userMessage.info.id)
if (!prior || mode(prior.info.agent) !== "ask") return
if (input.userMessage.parts.some((part) => part.type === "text" && part.text === ASK_CODE_SWITCH)) return
return {
id: PartID.ascending(),
messageID: input.userMessage.info.id,
sessionID: input.userMessage.info.sessionID,
type: "text" as const,
text: ASK_CODE_SWITCH,
synthetic: true,
}
}

/**
* Determines the close reason for a session turn.
* Checks for an explicit reason first (e.g. set on error during runLoop),
Expand Down
5 changes: 0 additions & 5 deletions packages/opencode/src/session/prompt/code-switch.txt

This file was deleted.

78 changes: 4 additions & 74 deletions packages/opencode/src/session/reminders.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,16 @@

import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect } from "effect"
import { KiloSessionPrompt } from "@/kilocode/session/prompt" // kilocode_change
import { Agent } from "@/agent/agent"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { PartID } from "./schema"
import { MessageV2 } from "./message-v2"
import { KiloModeReminders } from "@/kilocode/session/mode-reminders" // kilocode_change
import { Session } from "./session"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import CODE_SWITCH from "./prompt/code-switch.txt" // kilocode_change
import ASK_CODE_SWITCH from "@/kilocode/session/ask-code-switch.txt" // kilocode_change

export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
messages: SessionV1.WithParts[]
agent: Agent.Info
session: Session.Info
}) {
const flags = yield* RuntimeFlags.Service
const fsys = yield* FSUtil.Service
const sessions = yield* Session.Service
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
if (!userMessage) return input.messages

// kilocode_change start - shared planning / agent-switch reminder path
yield* Effect.promise(() =>
KiloSessionPrompt.insertPlanReminders({
agent: input.agent,
session: input.session,
userMessage,
messages: input.messages,
}),
)
const switched = KiloSessionPrompt.insertAgentSwitchReminder({
agent: input.agent,
userMessage,
messages: input.messages,
})
if (switched) userMessage.parts.push(yield* sessions.updatePart(switched))
// kilocode_change end

if (!flags.experimentalPlanMode) {
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
if (
wasPlan &&
input.agent.name === "code" &&
!userMessage.parts.some((part) => part.type === "text" && part.text === ASK_CODE_SWITCH)
) {
// kilocode_change - renamed from "build" to "code"
userMessage.parts.push({
id: PartID.ascending(),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: `\n\n${CODE_SWITCH}`, // kilocode_change - separated from user content
synthetic: true,
})
}
return input.messages
}

const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
const ctx = yield* InstanceState.context
const plan = Session.plan(input.session, ctx)
const exists = yield* fsys.existsSafe(plan)
const part = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID,
type: "text",
text: exists
? `\n\n${CODE_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it` // kilocode_change - renamed from BUILD_SWITCH to CODE_SWITCH
: `\n\n${CODE_SWITCH}`, // kilocode_change - renamed from BUILD_SWITCH to CODE_SWITCH
synthetic: true,
})
userMessage.parts.push(part)
return input.messages
}

// kilocode_change start - replace native Plan's separate prompt with the shared reminder above
return input.messages
// kilocode_change start - mode reminder policy (plan, ask, code switches) lives in kilocode/session/mode-reminders.ts
return yield* KiloModeReminders.apply(input)
// kilocode_change end
})

Expand Down
Loading
Loading