Skip to content

Commit 2c6dfea

Browse files
committed
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)
1 parent 64b53c0 commit 2c6dfea

File tree

7 files changed

+222
-0
lines changed

7 files changed

+222
-0
lines changed

src/config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const HookNameSchema = z.enum([
4141
"background-notification",
4242
"auto-update-checker",
4343
"ultrawork-mode",
44+
"agent-usage-reminder",
4445
])
4546

4647
export const AgentOverrideConfigSchema = z.object({
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { join } from "node:path";
2+
import { xdgData } from "xdg-basedir";
3+
4+
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
5+
export const AGENT_USAGE_REMINDER_STORAGE = join(
6+
OPENCODE_STORAGE,
7+
"agent-usage-reminder",
8+
);
9+
10+
export const TARGET_TOOLS = [
11+
"Grep",
12+
"safe_grep",
13+
"Glob",
14+
"safe_glob",
15+
"WebFetch",
16+
"context7_resolve-library-id",
17+
"context7_get-library-docs",
18+
"websearch_exa_web_search_exa",
19+
"grep_app_searchGitHub",
20+
] as const;
21+
22+
export const AGENT_TOOLS = [
23+
"Task",
24+
"call_omo_agent",
25+
"background_task",
26+
] as const;
27+
28+
export const REMINDER_MESSAGE = `
29+
[Agent Usage Reminder]
30+
31+
You called a search/fetch tool directly without leveraging specialized agents.
32+
33+
RECOMMENDED: Use background_task with explore/librarian agents for better results:
34+
35+
\`\`\`
36+
// Parallel exploration - fire multiple agents simultaneously
37+
background_task(agent="explore", prompt="Find all files matching pattern X")
38+
background_task(agent="explore", prompt="Search for implementation of Y")
39+
background_task(agent="librarian", prompt="Lookup documentation for Z")
40+
41+
// Then continue your work while they run in background
42+
// System will notify you when each completes
43+
\`\`\`
44+
45+
WHY:
46+
- Agents can perform deeper, more thorough searches
47+
- Background tasks run in parallel, saving time
48+
- Specialized agents have domain expertise
49+
- Reduces context window usage in main session
50+
51+
ALWAYS prefer: Multiple parallel background_task calls > Direct tool calls
52+
`;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { PluginInput } from "@opencode-ai/plugin";
2+
import {
3+
loadAgentUsageState,
4+
saveAgentUsageState,
5+
clearAgentUsageState,
6+
} from "./storage";
7+
import { TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE } from "./constants";
8+
import type { AgentUsageState } from "./types";
9+
10+
interface ToolExecuteInput {
11+
tool: string;
12+
sessionID: string;
13+
callID: string;
14+
parentSessionID?: string;
15+
}
16+
17+
interface ToolExecuteOutput {
18+
title: string;
19+
output: string;
20+
metadata: unknown;
21+
}
22+
23+
interface EventInput {
24+
event: {
25+
type: string;
26+
properties?: unknown;
27+
};
28+
}
29+
30+
export function createAgentUsageReminderHook(_ctx: PluginInput) {
31+
const sessionStates = new Map<string, AgentUsageState>();
32+
33+
function getOrCreateState(sessionID: string): AgentUsageState {
34+
if (!sessionStates.has(sessionID)) {
35+
const persisted = loadAgentUsageState(sessionID);
36+
const state: AgentUsageState = persisted ?? {
37+
sessionID,
38+
agentUsed: false,
39+
reminderCount: 0,
40+
updatedAt: Date.now(),
41+
};
42+
sessionStates.set(sessionID, state);
43+
}
44+
return sessionStates.get(sessionID)!;
45+
}
46+
47+
function markAgentUsed(sessionID: string): void {
48+
const state = getOrCreateState(sessionID);
49+
state.agentUsed = true;
50+
state.updatedAt = Date.now();
51+
saveAgentUsageState(state);
52+
}
53+
54+
function resetState(sessionID: string): void {
55+
sessionStates.delete(sessionID);
56+
clearAgentUsageState(sessionID);
57+
}
58+
59+
const toolExecuteAfter = async (
60+
input: ToolExecuteInput,
61+
output: ToolExecuteOutput,
62+
) => {
63+
const { tool, sessionID, parentSessionID } = input;
64+
65+
// Only run in root sessions (no parent = main session)
66+
if (parentSessionID) {
67+
return;
68+
}
69+
70+
if ((AGENT_TOOLS as readonly string[]).includes(tool)) {
71+
markAgentUsed(sessionID);
72+
return;
73+
}
74+
75+
if (!(TARGET_TOOLS as readonly string[]).includes(tool)) {
76+
return;
77+
}
78+
79+
const state = getOrCreateState(sessionID);
80+
81+
if (state.agentUsed) {
82+
return;
83+
}
84+
85+
output.output += REMINDER_MESSAGE;
86+
state.reminderCount++;
87+
state.updatedAt = Date.now();
88+
saveAgentUsageState(state);
89+
};
90+
91+
const eventHandler = async ({ event }: EventInput) => {
92+
const props = event.properties as Record<string, unknown> | undefined;
93+
94+
if (event.type === "session.deleted") {
95+
const sessionInfo = props?.info as { id?: string } | undefined;
96+
if (sessionInfo?.id) {
97+
resetState(sessionInfo.id);
98+
}
99+
}
100+
101+
if (event.type === "session.compacted") {
102+
const sessionID = (props?.sessionID ??
103+
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
104+
if (sessionID) {
105+
resetState(sessionID);
106+
}
107+
}
108+
};
109+
110+
return {
111+
"tool.execute.after": toolExecuteAfter,
112+
event: eventHandler,
113+
};
114+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
readFileSync,
5+
writeFileSync,
6+
unlinkSync,
7+
} from "node:fs";
8+
import { join } from "node:path";
9+
import { AGENT_USAGE_REMINDER_STORAGE } from "./constants";
10+
import type { AgentUsageState } from "./types";
11+
12+
function getStoragePath(sessionID: string): string {
13+
return join(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
14+
}
15+
16+
export function loadAgentUsageState(sessionID: string): AgentUsageState | null {
17+
const filePath = getStoragePath(sessionID);
18+
if (!existsSync(filePath)) return null;
19+
20+
try {
21+
const content = readFileSync(filePath, "utf-8");
22+
return JSON.parse(content) as AgentUsageState;
23+
} catch {
24+
return null;
25+
}
26+
}
27+
28+
export function saveAgentUsageState(state: AgentUsageState): void {
29+
if (!existsSync(AGENT_USAGE_REMINDER_STORAGE)) {
30+
mkdirSync(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
31+
}
32+
33+
const filePath = getStoragePath(state.sessionID);
34+
writeFileSync(filePath, JSON.stringify(state, null, 2));
35+
}
36+
37+
export function clearAgentUsageState(sessionID: string): void {
38+
const filePath = getStoragePath(sessionID);
39+
if (existsSync(filePath)) {
40+
unlinkSync(filePath);
41+
}
42+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface AgentUsageState {
2+
sessionID: string;
3+
agentUsed: boolean;
4+
reminderCount: number;
5+
updatedAt: number;
6+
}

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export { createRulesInjectorHook } from "./rules-injector";
1414
export { createBackgroundNotificationHook } from "./background-notification"
1515
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
1616
export { createUltraworkModeHook } from "./ultrawork-mode";
17+
export { createAgentUsageReminderHook } from "./agent-usage-reminder";

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
createBackgroundNotificationHook,
1818
createAutoUpdateCheckerHook,
1919
createUltraworkModeHook,
20+
createAgentUsageReminderHook,
2021
} from "./hooks";
2122
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
2223
import {
@@ -207,6 +208,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
207208
const ultraworkMode = isHookEnabled("ultrawork-mode")
208209
? createUltraworkModeHook()
209210
: null;
211+
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
212+
? createAgentUsageReminderHook(ctx)
213+
: null;
210214

211215
updateTerminalTitle({ sessionId: "main" });
212216

@@ -320,6 +324,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
320324
await thinkMode?.event(input);
321325
await anthropicAutoCompact?.event(input);
322326
await ultraworkMode?.event(input);
327+
await agentUsageReminder?.event(input);
323328

324329
const { event } = input;
325330
const props = event.properties as Record<string, unknown> | undefined;
@@ -439,6 +444,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
439444
await directoryReadmeInjector?.["tool.execute.after"](input, output);
440445
await rulesInjector?.["tool.execute.after"](input, output);
441446
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
447+
await agentUsageReminder?.["tool.execute.after"](input, output);
442448

443449
if (input.sessionID === getMainSessionID()) {
444450
updateTerminalTitle({

0 commit comments

Comments
 (0)