feat: support static handoff text

This commit is contained in:
emidab 2026-09-16 06:42:03 +00:00
parent 007f4fed6d
commit 2cea3a47b9
2 changed files with 63 additions and 4 deletions

View file

@ -31,6 +31,32 @@ The plugin registers one tool, `session_restart`, that the agent can call when t
- Aborts the current session by default, so only the restarted session stays active. - Aborts the current session by default, so only the restarted session stays active.
- Returns the new session ID so the agent can tell you where to switch. - Returns the new session ID so the agent can tell you where to switch.
## Static handoff text
A static text that is always included in the restart prompt, when defined:
- **Per agent (overrides the global default):** add a `handoff_text` key to any agent in `opencode.json` (or any frontmatter key in `agent/*.md` — unknown keys fold into the agent's `options` and are read from there). The text of the agent the restarted session runs under is used.
```json
{
"agent": {
"build": {
"handoff_text": "Always start by reviewing the open PRs on git.jilits.se before touching any code."
}
}
}
```
- **Global fallback:** set the `static_text` plugin option. It applies to every restart whenever the agent defines no `handoff_text` of its own.
```json
{
"plugin": [["/path/to/opencode-session-restart", { "static_text": "..." }]]
}
```
Either way the text is prepended to the handoff prompt under a `STANDING INSTRUCTIONS` section. Precedence: agent `handoff_text` (direct key, or md-frontmatter `handoff_text`) → `static_text` plugin option → nothing.
### Tool arguments ### Tool arguments
| argument | type | default | description | | argument | type | default | description |

View file

@ -1,8 +1,10 @@
import { type Plugin, type PluginOptions, tool } from "@opencode-ai/plugin" import { type Plugin, type PluginInput, type PluginOptions, tool } from "@opencode-ai/plugin"
import type { TextPartInput } from "@opencode-ai/sdk" import type { TextPartInput } from "@opencode-ai/sdk"
export const id = "opencode-session-restart" export const id = "opencode-session-restart"
type PluginClient = PluginInput["client"]
export type RestartPluginOptions = { export type RestartPluginOptions = {
/** /**
* Abort the current session once the new session has been started. * Abort the current session once the new session has been started.
@ -16,6 +18,12 @@ export type RestartPluginOptions = {
* Default: true. * Default: true.
*/ */
disable_compaction_continue: boolean disable_compaction_continue: boolean
/**
* Static text always prepended to every restart prompt, for agents that do
* not define their own `handoff_text`. An agent-defined `handoff_text`
* overrides this. Default: none.
*/
static_text?: string
} }
const DEFAULT_OPTIONS: RestartPluginOptions = { const DEFAULT_OPTIONS: RestartPluginOptions = {
@ -25,12 +33,15 @@ const DEFAULT_OPTIONS: RestartPluginOptions = {
const MAX_TITLE_LENGTH = 80 const MAX_TITLE_LENGTH = 80
function buildHandoff(task: string, notes: string | undefined) { function buildHandoff(task: string, notes: string | undefined, staticText: string | undefined) {
const state = notes?.trim() const state = notes?.trim()
return [ return [
"You are continuing work that was restarted into this fresh session with a clean context window.", "You are continuing work that was restarted into this fresh session with a clean context window.",
"", "",
"Do not reconstruct or repeat the earlier conversation. Only the notes below and the current on-disk state (working tree, git status, staged or uncommitted changes) are available context.", "Do not reconstruct or repeat the earlier conversation. Only the standing instructions and state notes below, plus the current on-disk state (working tree, git status, staged or uncommitted changes), are available context.",
...(staticText
? ["", "STANDING INSTRUCTIONS (always apply):", staticText]
: []),
"", "",
"STATE NOTES:", "STATE NOTES:",
state && state.length > 0 ? state : "None provided — orient yourself by inspecting git status, the diff, and open files.", state && state.length > 0 ? state : "None provided — orient yourself by inspecting git status, the diff, and open files.",
@ -40,6 +51,27 @@ function buildHandoff(task: string, notes: string | undefined) {
].join("\n") ].join("\n")
} }
async function resolveStaticText(
client: PluginClient,
currentAgent: string,
targetAgent: string | undefined,
fallback: string | undefined,
) {
const agentName = targetAgent ?? currentAgent
try {
const cfg = await client.config.get()
const agent = cfg.data?.agent?.[agentName]
const direct = agent?.handoff_text
if (typeof direct === "string" && direct.trim()) return direct
const opts = agent?.options as Record<string, unknown> | undefined
const folded = opts?.handoff_text
if (typeof folded === "string" && folded.trim()) return folded
} catch {
// config lookup failed — fall through to the plugin-level static_text
}
return fallback
}
function normalizeModel(value: string) { function normalizeModel(value: string) {
const slash = value.indexOf("/") const slash = value.indexOf("/")
if (slash <= 0 || slash === value.length - 1) { if (slash <= 0 || slash === value.length - 1) {
@ -91,7 +123,8 @@ export const server: Plugin = async ({ client }, rawOptions = {}) => {
const restartedID = created.data?.id const restartedID = created.data?.id
if (!restartedID) throw new Error("session.create returned no session id") if (!restartedID) throw new Error("session.create returned no session id")
const parts: TextPartInput[] = [{ type: "text", text: buildHandoff(args.task, args.notes) }] const staticText = await resolveStaticText(client, ctx.agent, args.agent, options.static_text)
const parts: TextPartInput[] = [{ type: "text", text: buildHandoff(args.task, args.notes, staticText) }]
await client.session.promptAsync({ await client.session.promptAsync({
path: { id: restartedID }, path: { id: restartedID },
body: { body: {