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:
@@ -41,6 +41,7 @@ export const HookNameSchema = z.enum([
|
||||
"background-notification",
|
||||
"auto-update-checker",
|
||||
"ultrawork-mode",
|
||||
"agent-usage-reminder",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
|
||||
52
src/hooks/agent-usage-reminder/constants.ts
Normal file
52
src/hooks/agent-usage-reminder/constants.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { join } from "node:path";
|
||||
import { xdgData } from "xdg-basedir";
|
||||
|
||||
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
||||
export const AGENT_USAGE_REMINDER_STORAGE = join(
|
||||
OPENCODE_STORAGE,
|
||||
"agent-usage-reminder",
|
||||
);
|
||||
|
||||
export const TARGET_TOOLS = [
|
||||
"Grep",
|
||||
"safe_grep",
|
||||
"Glob",
|
||||
"safe_glob",
|
||||
"WebFetch",
|
||||
"context7_resolve-library-id",
|
||||
"context7_get-library-docs",
|
||||
"websearch_exa_web_search_exa",
|
||||
"grep_app_searchGitHub",
|
||||
] as const;
|
||||
|
||||
export const AGENT_TOOLS = [
|
||||
"Task",
|
||||
"call_omo_agent",
|
||||
"background_task",
|
||||
] as const;
|
||||
|
||||
export const REMINDER_MESSAGE = `
|
||||
[Agent Usage Reminder]
|
||||
|
||||
You called a search/fetch tool directly without leveraging specialized agents.
|
||||
|
||||
RECOMMENDED: Use background_task with explore/librarian agents for better results:
|
||||
|
||||
\`\`\`
|
||||
// Parallel exploration - fire multiple agents simultaneously
|
||||
background_task(agent="explore", prompt="Find all files matching pattern X")
|
||||
background_task(agent="explore", prompt="Search for implementation of Y")
|
||||
background_task(agent="librarian", prompt="Lookup documentation for Z")
|
||||
|
||||
// Then continue your work while they run in background
|
||||
// System will notify you when each completes
|
||||
\`\`\`
|
||||
|
||||
WHY:
|
||||
- Agents can perform deeper, more thorough searches
|
||||
- Background tasks run in parallel, saving time
|
||||
- Specialized agents have domain expertise
|
||||
- Reduces context window usage in main session
|
||||
|
||||
ALWAYS prefer: Multiple parallel background_task calls > Direct tool calls
|
||||
`;
|
||||
114
src/hooks/agent-usage-reminder/index.ts
Normal file
114
src/hooks/agent-usage-reminder/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
42
src/hooks/agent-usage-reminder/storage.ts
Normal file
42
src/hooks/agent-usage-reminder/storage.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { AGENT_USAGE_REMINDER_STORAGE } from "./constants";
|
||||
import type { AgentUsageState } from "./types";
|
||||
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadAgentUsageState(sessionID: string): AgentUsageState | null {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return null;
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(content) as AgentUsageState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAgentUsageState(state: AgentUsageState): void {
|
||||
if (!existsSync(AGENT_USAGE_REMINDER_STORAGE)) {
|
||||
mkdirSync(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = getStoragePath(state.sessionID);
|
||||
writeFileSync(filePath, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
export function clearAgentUsageState(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
6
src/hooks/agent-usage-reminder/types.ts
Normal file
6
src/hooks/agent-usage-reminder/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface AgentUsageState {
|
||||
sessionID: string;
|
||||
agentUsed: boolean;
|
||||
reminderCount: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -14,3 +14,4 @@ export { createRulesInjectorHook } from "./rules-injector";
|
||||
export { createBackgroundNotificationHook } from "./background-notification"
|
||||
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||
export { createUltraworkModeHook } from "./ultrawork-mode";
|
||||
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
createAgentUsageReminderHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -207,6 +208,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
: null;
|
||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||
? createAgentUsageReminderHook(ctx)
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
@@ -320,6 +324,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await thinkMode?.event(input);
|
||||
await anthropicAutoCompact?.event(input);
|
||||
await ultraworkMode?.event(input);
|
||||
await agentUsageReminder?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -439,6 +444,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||
await rulesInjector?.["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||
await agentUsageReminder?.["tool.execute.after"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
|
||||
Reference in New Issue
Block a user