feat: support static handoff text
This commit is contained in:
parent
007f4fed6d
commit
2cea3a47b9
2 changed files with 63 additions and 4 deletions
26
README.md
26
README.md
|
|
@ -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 |
|
||||||
|
|
|
||||||
41
index.ts
41
index.ts
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue