feat(hooks): add agent-usage-reminder hook for background agent recommendations

Implements hook that tracks whether explore/librarian agents have been used in a session.
When target tools (Grep, Glob, WebFetch, context7, websearch_exa, grep_app) are called
without prior agent usage, appends reminder message recommending parallel background_task calls.

State persists across tool calls and resets on session compaction, allowing fresh reminders
after context compaction - similar to directory-readme-injector pattern.

Files:
- src/hooks/agent-usage-reminder/: New hook implementation
  - types.ts: AgentUsageState interface
  - constants.ts: TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE
  - storage.ts: File-based state persistence with compaction handling
  - index.ts: Hook implementation with tool.execute.after and event handlers
- src/config/schema.ts: Add 'agent-usage-reminder' to HookNameSchema
- src/hooks/index.ts: Export createAgentUsageReminderHook
- src/index.ts: Instantiate and register hook handlers

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-13 21:42:05 +09:00
parent 64b53c0e1c
commit 2c6dfeadce
7 changed files with 222 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
import type { PluginInput } from "@opencode-ai/plugin";
import {
loadAgentUsageState,
saveAgentUsageState,
clearAgentUsageState,
} from "./storage";
import { TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE } from "./constants";
import type { AgentUsageState } from "./types";
interface ToolExecuteInput {
tool: string;
sessionID: string;
callID: string;
parentSessionID?: string;
}
interface ToolExecuteOutput {
title: string;
output: string;
metadata: unknown;
}
interface EventInput {
event: {
type: string;
properties?: unknown;
};
}
export function createAgentUsageReminderHook(_ctx: PluginInput) {
const sessionStates = new Map<string, AgentUsageState>();
function getOrCreateState(sessionID: string): AgentUsageState {
if (!sessionStates.has(sessionID)) {
const persisted = loadAgentUsageState(sessionID);
const state: AgentUsageState = persisted ?? {
sessionID,
agentUsed: false,
reminderCount: 0,
updatedAt: Date.now(),
};
sessionStates.set(sessionID, state);
}
return sessionStates.get(sessionID)!;
}
function markAgentUsed(sessionID: string): void {
const state = getOrCreateState(sessionID);
state.agentUsed = true;
state.updatedAt = Date.now();
saveAgentUsageState(state);
}
function resetState(sessionID: string): void {
sessionStates.delete(sessionID);
clearAgentUsageState(sessionID);
}
const toolExecuteAfter = async (
input: ToolExecuteInput,
output: ToolExecuteOutput,
) => {
const { tool, sessionID, parentSessionID } = input;
// Only run in root sessions (no parent = main session)
if (parentSessionID) {
return;
}
if ((AGENT_TOOLS as readonly string[]).includes(tool)) {
markAgentUsed(sessionID);
return;
}
if (!(TARGET_TOOLS as readonly string[]).includes(tool)) {
return;
}
const state = getOrCreateState(sessionID);
if (state.agentUsed) {
return;
}
output.output += REMINDER_MESSAGE;
state.reminderCount++;
state.updatedAt = Date.now();
saveAgentUsageState(state);
};
const eventHandler = async ({ event }: EventInput) => {
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) {
resetState(sessionInfo.id);
}
}
if (event.type === "session.compacted") {
const sessionID = (props?.sessionID ??
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
if (sessionID) {
resetState(sessionID);
}
}
};
return {
"tool.execute.after": toolExecuteAfter,
event: eventHandler,
};
}