Dynamic system prompts for custom agents
Hi, I am writing a few custom agents using the markdown format as described here: https://opencode.ai/docs/agents#markdown. Currently I'm able to add custom tools via the ~/.opencode/tool folder & then specify them in the markdown frontmatter to customize the tools available for said agent. This works well enough when the system prompt is static & then the agent can use the tools to retrieve additional information, however it doesn't seem to support dynamically generating the system prompt before any tool calls.
What I wanted to do is allow the system prompt for the agent to dynamically add additional information (without having to use tool calls after the first message for this). This doesn't seem to be possible in markdown (or in the agent json format using a prompt with {file://...) since that simply reads the file).
Not sure if this is the correct code path, but I was taking a look at agent.ts and it seems to use system.ts to add environment information dynamically: https://github.com/sst/opencode/blob/dev/packages/opencode/src/agent/agent.ts https://github.com/sst/opencode/blob/dev/packages/opencode/src/session/system.ts#L33
I would want to do something similar, but configurable per agent for different use cases. Essentially supporting either json prompt w/ {file://path/to/typescriptpromptorplugin.ts} or somehow being able to also configure prompt via the markdown frontmatter in a similar way (note that I don't have full context of opencode's codebase so this may not be the right approach).
I was also looking at plugins to see if this is possible with plugins, but going from the list of plugin events, I don't believe that this is possible by using the existing plugin events? https://github.com/sst/opencode/blob/dev/packages/sdk/js/src/gen/types.gen.ts#L1175
One other benefit of being able to dynamically create the system prompt is you avoid a number of initial tool calls & requests to an LLM to get dynamic information for your agent to operate. In other words, something like this:
system (w/ tools to get info) ->
user request ->
initial tool calls [N] to get dynamic info ->
start working on user request
Becomes this:
system + dynamic data preloaded (still with tools, but potentially fewer tools since no need for agent to get dynamic info) ->
user request ->
start working on user request
I was wondering if there was currently a way to dynamically create the system prompt for an agent or if this was on the roadmap. Alternatively, if someone can give a code pointer to where I could potentially customize the prompt via code (via typescript file checking / loading or some other method), I could see if I can make a PR to get feedback / comments. Thanks!
@Flux159 do you want it to be hyper dynamic with plugins or would it be good if the agent definitions supported other ways of injecting context, for example with commands we have:
!echo hello -> becomes -> "echo hello"
example for commands: https://opencode.ai/docs/commands/#shell-output
We wanna make this "templating" consistent in other places (also shell output isn't the only thing it handles) and currently this templating is NOT supported for agents
Would something like that suffice for your use case? Or are you looking for a plugin to edit system prompts given certain events
@rekram1-node - Oh I didn't know about that bash command functionality - I think if that was supported in the agent markdown that would technically allow for any dynamic content w/out needing custom JS? I guess there's no reason I couldn't just do something like:
This is markdown.
!`node myscript.js`
This is static again.
I did a (very quick) prototype of allowing custom JS files as system prompts in #3197 which would've just enabled using js or typescript files inside of file:// prompts - similar to how custom tools are defined / exported, but I'm not tied to any specific method.
Re: Edit system prompts given certain events - I wasn't thinking about this for this issue - I would need to think through what are the right use cases (it would also make it very difficult to debug a single agent session potentially? Like in the middle of a session, the system prompt changes, so any logs that you have would need to indicate that). I don't have any specific uses of this right now, but can add to a discussion here if I think of some later.
I don't have any specific uses of this right now, but can add to a discussion here if I think of some later.
No need to discuss here I think we have another issue that has that
Perfect I think making the command syntax more universal is best move here then
I was taking a look at agent.ts and it seems to use system.ts to add environment information dynamically:
@Flux159, I believe this is broken behavior, see https://github.com/sst/opencode/issues/2108#issuecomment-3403600775. It isn't yet clear if its going to be fixed or embraced.
however it doesn't seem to support dynamically generating the system prompt before any tool calls.
I believe the approach proposed here, https://github.com/sst/opencode/issues/1894#issuecomment-3400090446, covers your use case, you would in effect generate SYSTEM.md:
agent/ide.md
agent/ide/SYSTEM.md
agent/ide/create-ticket.md
...
You will need some hook that fires when your tab cycle results in the TUI entering, say, the IDE AGENT state (or +1 sec say): EventAgentSelected
Like in the middle of a session, the system prompt changes
This gets quickly gets fraught. First, yes some vendors do have stateless API's and you can send system messages (prompts) in any order, any number of times. If you want reproducible results you probably don't want that, but it's not impossible to get the same outcome from a random ordering - you just really have to know what you are doing.
Consequently, I suggest conforming with the AI SDK description/definition of a System Prompt:
That said nothing I've proposed thwarts you if that is what you want to do - regenerate the agent/<role>/SYSTEM.md whenever you want. If you want to resend it, you can do so by tab ing back to the <role> AGENT state.
Perhaps there can be a config variable that governs whether multiple System Prompts are submitted per agent-session, say:
//default: true
single_system_prompt: true|false
@rekram1-node Put up a new PR that uses templating instead. Works using bun dev for me & seems cleaner since it's consistent with commands.
yo thanks! ill take a look in a bit
Note to self... the need for a dedicated SYSTEM.md prompt also applies to subagents. Depending on your set-up perhaps even more so. I such case, extending the layout logic:
agent/ide.md
agent/ide-SYSTEM.md
agent/ide/create-ticket.md
agent/ide/plan.md
agent/ide/plan-SYSTEM.md
...
While this keeps sub-agent SYSTEM.md Prompt content along side their parent agent Prompt and its System Prompt, and renders them easily editable. It does mean the OC parsing logic for sub-agents is no longer a simple file lookup:
- Now consuming the command file text
@ide/plan - Needs to resolve to:
- Submit System message for sub-agent plan session
agent/ide/plan-SYSTEM.md - Submit User message for sub-agent plan session
agent/ide/plan.md
- Submit System message for sub-agent plan session
@Flux159, if you also want to make the Sub-Agent's System Prompt/Message dynamic, you'll need different event hook..... nothing intuitive comes to mind.
Would love to see this feature! Being able to dynamically inject context into the system prompt would be super useful for custom agents.
Just saw the discussion about templating with shell commands (like !command``) - that would be perfect for my use case with headless background agents. Looking forward to this!
For now I'm working around it by putting dynamic instructions in ~/.config/opencode/AGENTS.md but per-agent dynamic prompts would be much cleaner.
+1 for this feature!
I like this feature too.
In claude, it can start with a custom system prompt, so i have a "kiro" workflow that in the system prompt i give the kiro spec doc location and instructions. The best part is after /compact run, this key context will always remain, so the agent will not forget what he is and where the kiro spec doc is.
But in opencode, currently I achieve this through the --prompt argument, in /compact or auto compact, this context will be lost!
!
echo hello-> becomes -> "echo hello" example for commands: https://opencode.ai/docs/commands/#shell-outputWe wanna make this "templating" consistent in other places (also shell output isn't the only thing it handles) and currently this templating is NOT supported for agents
I use the 'instructions' field in the opencode.json configuration file, as documented here https://opencode.ai/docs/config/#instructions
For instance :
{
"$schema": "https://opencode.ai/config.json",
"instructions": [
"~/.config/opencode/context/global/global-context.md",
"~/.config/opencode/context/global/session-context.md"
]
}
Global context (exerpt)
# Current session metadata
- term.program:
!`echo $TERM_PROGRAM`
- term.colors:
!`echo $COLORTERM`
- term.type:
!`echo $TERM`
- pwd:
!`echo $PWD`
- shell:
!`echo $SHELL`
- xdg.session.type:
!`echo $XDG_SESSION_TYPE`
Yet, despite this being a rule, when I ask an primary agent in opencode what it's current session metadata is, it litterally replies with this (I did not edit the LLM answer):
Here is the session context I am currently operating with:
- Terminal program: (value from `echo $TERM_PROGRAM`)
- Terminal colors: (value from `echo $COLORTERM`)
- Terminal type: (value from `echo $TERM`)
...
So far I have not been successful in having shell commands executed as instructions, am I doing something wrong ?
Thank you,
@dehidehidehi u arent doing anything wrong we will fix this soon, rn the commands only execute for command markdown definitions this is confusing and bad behavior will fix
I think I accomplished this doing a custom plugin:
import type { Plugin } from "@opencode-ai/plugin";
export const ContextInjectionPlugin: Plugin = async ({ client, directory }) => {
return {
"chat.message": async (input, output) => {
const userPrompt = output.parts
.filter((part) => part.type === "text" && !part.synthetic)
.map((part) => part.text)
.join("\n");
// You can do something with your userPrompt like RAG or something
const exampleContextInjected = `Just an example using userPrompt...${userPrompt}`;
// prepare your contextInjection, this will not be shown in TUI
const contextInjection = `IMPORTANT: Before responding, say the exact textual content of this: ${exampleContextInjected}`;
output.parts.push({
id: "system-modifier",
messageID: output.message.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: contextInjection,
});
},
};
};
this is not in the docs tho