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",
|
"background-notification",
|
||||||
"auto-update-checker",
|
"auto-update-checker",
|
||||||
"ultrawork-mode",
|
"ultrawork-mode",
|
||||||
|
"agent-usage-reminder",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const AgentOverrideConfigSchema = z.object({
|
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 { createBackgroundNotificationHook } from "./background-notification"
|
||||||
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||||
export { createUltraworkModeHook } from "./ultrawork-mode";
|
export { createUltraworkModeHook } from "./ultrawork-mode";
|
||||||
|
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
createBackgroundNotificationHook,
|
createBackgroundNotificationHook,
|
||||||
createAutoUpdateCheckerHook,
|
createAutoUpdateCheckerHook,
|
||||||
createUltraworkModeHook,
|
createUltraworkModeHook,
|
||||||
|
createAgentUsageReminderHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||||
import {
|
import {
|
||||||
@@ -207,6 +208,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||||
? createUltraworkModeHook()
|
? createUltraworkModeHook()
|
||||||
: null;
|
: null;
|
||||||
|
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||||
|
? createAgentUsageReminderHook(ctx)
|
||||||
|
: null;
|
||||||
|
|
||||||
updateTerminalTitle({ sessionId: "main" });
|
updateTerminalTitle({ sessionId: "main" });
|
||||||
|
|
||||||
@@ -320,6 +324,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await thinkMode?.event(input);
|
await thinkMode?.event(input);
|
||||||
await anthropicAutoCompact?.event(input);
|
await anthropicAutoCompact?.event(input);
|
||||||
await ultraworkMode?.event(input);
|
await ultraworkMode?.event(input);
|
||||||
|
await agentUsageReminder?.event(input);
|
||||||
|
|
||||||
const { event } = input;
|
const { event } = input;
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
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 directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||||
await rulesInjector?.["tool.execute.after"](input, output);
|
await rulesInjector?.["tool.execute.after"](input, output);
|
||||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||||
|
await agentUsageReminder?.["tool.execute.after"](input, output);
|
||||||
|
|
||||||
if (input.sessionID === getMainSessionID()) {
|
if (input.sessionID === getMainSessionID()) {
|
||||||
updateTerminalTitle({
|
updateTerminalTitle({
|
||||||
|
|||||||
Reference in New Issue
Block a user