- Rename delegate_task tool to task across codebase (100 files) - Update model references: claude-opus-4-6 → 4-5, gpt-5.3-codex → 5.2-codex - Add tool-metadata-store to restore metadata overwritten by fromPlugin() - Add session ID polling for BackgroundManager task sessions - Await async ctx.metadata() calls in tool executors - Add ses_ prefix guard to getMessageDir for performance - Harden BackgroundManager with idle deferral and error handling - Fix duplicate task key in sisyphus-junior test object literals - Fix unawaited showOutputToUser in ast_grep_replace - Fix background=true → run_in_background=true in ultrawork prompt - Fix duplicate task/task references in docs and comments
178 lines
4.8 KiB
TypeScript
178 lines
4.8 KiB
TypeScript
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder"
|
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
|
import { log } from "../../shared"
|
|
|
|
/**
|
|
* Target agents that should receive category+skill reminders.
|
|
* These are orchestrator agents that delegate work to specialized agents.
|
|
*/
|
|
const TARGET_AGENTS = new Set([
|
|
"sisyphus",
|
|
"sisyphus-junior",
|
|
"atlas",
|
|
])
|
|
|
|
/**
|
|
* Tools that indicate the agent is doing work that could potentially be delegated.
|
|
* When these tools are used, we remind the agent about the category+skill system.
|
|
*/
|
|
const DELEGATABLE_WORK_TOOLS = new Set([
|
|
"edit",
|
|
"write",
|
|
"bash",
|
|
"read",
|
|
"grep",
|
|
"glob",
|
|
])
|
|
|
|
/**
|
|
* Tools that indicate the agent is already using delegation properly.
|
|
*/
|
|
const DELEGATION_TOOLS = new Set([
|
|
"task",
|
|
"call_omo_agent",
|
|
])
|
|
|
|
function formatSkillNames(skills: AvailableSkill[], limit: number): string {
|
|
if (skills.length === 0) return "(none)"
|
|
const shown = skills.slice(0, limit).map((s) => s.name)
|
|
const remaining = skills.length - shown.length
|
|
const suffix = remaining > 0 ? ` (+${remaining} more)` : ""
|
|
return shown.join(", ") + suffix
|
|
}
|
|
|
|
function buildReminderMessage(availableSkills: AvailableSkill[]): string {
|
|
const builtinSkills = availableSkills.filter((s) => s.location === "plugin")
|
|
const customSkills = availableSkills.filter((s) => s.location !== "plugin")
|
|
|
|
const builtinText = formatSkillNames(builtinSkills, 8)
|
|
const customText = formatSkillNames(customSkills, 8)
|
|
|
|
const exampleSkillName = customSkills[0]?.name ?? builtinSkills[0]?.name
|
|
const loadSkills = exampleSkillName ? `["${exampleSkillName}"]` : "[]"
|
|
|
|
const lines = [
|
|
"",
|
|
"[Category+Skill Reminder]",
|
|
"",
|
|
`**Built-in**: ${builtinText}`,
|
|
`**⚡ YOUR SKILLS (PRIORITY)**: ${customText}`,
|
|
"",
|
|
"> User-installed skills OVERRIDE built-in defaults. ALWAYS prefer YOUR SKILLS when domain matches.",
|
|
"",
|
|
"```typescript",
|
|
`task(category=\"visual-engineering\", load_skills=${loadSkills}, run_in_background=true)`,
|
|
"```",
|
|
"",
|
|
]
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
interface ToolExecuteInput {
|
|
tool: string
|
|
sessionID: string
|
|
callID: string
|
|
agent?: string
|
|
}
|
|
|
|
interface ToolExecuteOutput {
|
|
title: string
|
|
output: string
|
|
metadata: unknown
|
|
}
|
|
|
|
interface SessionState {
|
|
delegationUsed: boolean
|
|
reminderShown: boolean
|
|
toolCallCount: number
|
|
}
|
|
|
|
export function createCategorySkillReminderHook(
|
|
_ctx: PluginInput,
|
|
availableSkills: AvailableSkill[] = []
|
|
) {
|
|
const sessionStates = new Map<string, SessionState>()
|
|
const reminderMessage = buildReminderMessage(availableSkills)
|
|
|
|
function getOrCreateState(sessionID: string): SessionState {
|
|
if (!sessionStates.has(sessionID)) {
|
|
sessionStates.set(sessionID, {
|
|
delegationUsed: false,
|
|
reminderShown: false,
|
|
toolCallCount: 0,
|
|
})
|
|
}
|
|
return sessionStates.get(sessionID)!
|
|
}
|
|
|
|
function isTargetAgent(sessionID: string, inputAgent?: string): boolean {
|
|
const agent = getSessionAgent(sessionID) ?? inputAgent
|
|
if (!agent) return false
|
|
const agentLower = agent.toLowerCase()
|
|
return TARGET_AGENTS.has(agentLower) ||
|
|
agentLower.includes("sisyphus") ||
|
|
agentLower.includes("atlas")
|
|
}
|
|
|
|
const toolExecuteAfter = async (
|
|
input: ToolExecuteInput,
|
|
output: ToolExecuteOutput,
|
|
) => {
|
|
const { tool, sessionID } = input
|
|
const toolLower = tool.toLowerCase()
|
|
|
|
if (!isTargetAgent(sessionID, input.agent)) {
|
|
return
|
|
}
|
|
|
|
const state = getOrCreateState(sessionID)
|
|
|
|
if (DELEGATION_TOOLS.has(toolLower)) {
|
|
state.delegationUsed = true
|
|
log("[category-skill-reminder] Delegation tool used", { sessionID, tool })
|
|
return
|
|
}
|
|
|
|
if (!DELEGATABLE_WORK_TOOLS.has(toolLower)) {
|
|
return
|
|
}
|
|
|
|
state.toolCallCount++
|
|
|
|
if (state.toolCallCount >= 3 && !state.delegationUsed && !state.reminderShown) {
|
|
output.output += reminderMessage
|
|
state.reminderShown = true
|
|
log("[category-skill-reminder] Reminder injected", {
|
|
sessionID,
|
|
toolCallCount: state.toolCallCount
|
|
})
|
|
}
|
|
}
|
|
|
|
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
const props = event.properties as Record<string, unknown> | undefined
|
|
|
|
if (event.type === "session.deleted") {
|
|
const sessionInfo = props?.info as { id?: string } | undefined
|
|
if (sessionInfo?.id) {
|
|
sessionStates.delete(sessionInfo.id)
|
|
}
|
|
}
|
|
|
|
if (event.type === "session.compacted") {
|
|
const sessionID = (props?.sessionID ??
|
|
(props?.info as { id?: string } | undefined)?.id) as string | undefined
|
|
if (sessionID) {
|
|
sessionStates.delete(sessionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
"tool.execute.after": toolExecuteAfter,
|
|
event: eventHandler,
|
|
}
|
|
}
|