feat(hooks): add task-reminder hook for task tool usage tracking
Injects a reminder after 10 tool turns without task tool usage. Tracks per-session counters and cleans up on session deletion. 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
parent
865ced72e4
commit
6a31e911d8
59
src/hooks/task-reminder/hook.ts
Normal file
59
src/hooks/task-reminder/hook.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
const TASK_TOOLS = new Set([
|
||||
"task",
|
||||
"task_create",
|
||||
"task_list",
|
||||
"task_get",
|
||||
"task_update",
|
||||
"task_delete",
|
||||
])
|
||||
const TURN_THRESHOLD = 10
|
||||
const REMINDER_MESSAGE = `
|
||||
|
||||
The task tools haven't been used recently. If you're tracking work, use task with action=create/update (or task_create/task_update) to record progress.`
|
||||
|
||||
interface ToolExecuteInput {
|
||||
tool: string
|
||||
sessionID: string
|
||||
callID: string
|
||||
}
|
||||
|
||||
interface ToolExecuteOutput {
|
||||
output: string
|
||||
}
|
||||
|
||||
export function createTaskReminderHook(_ctx: PluginInput) {
|
||||
const sessionCounters = new Map<string, number>()
|
||||
|
||||
const toolExecuteAfter = async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
|
||||
const { tool, sessionID } = input
|
||||
const toolLower = tool.toLowerCase()
|
||||
|
||||
if (TASK_TOOLS.has(toolLower)) {
|
||||
sessionCounters.set(sessionID, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const currentCount = sessionCounters.get(sessionID) ?? 0
|
||||
const newCount = currentCount + 1
|
||||
|
||||
if (newCount >= TURN_THRESHOLD) {
|
||||
output.output += REMINDER_MESSAGE
|
||||
sessionCounters.set(sessionID, 0)
|
||||
} else {
|
||||
sessionCounters.set(sessionID, newCount)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||
if (event.type !== "session.deleted") return
|
||||
const props = event.properties as { info?: { id?: string } } | undefined
|
||||
const sessionId = props?.info?.id
|
||||
if (!sessionId) return
|
||||
sessionCounters.delete(sessionId)
|
||||
},
|
||||
}
|
||||
}
|
||||
150
src/hooks/task-reminder/index.test.ts
Normal file
150
src/hooks/task-reminder/index.test.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test"
|
||||
import { createTaskReminderHook } from "./index"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
const mockCtx = {} as PluginInput
|
||||
|
||||
describe("TaskReminderHook", () => {
|
||||
let hook: ReturnType<typeof createTaskReminderHook>
|
||||
|
||||
beforeEach(() => {
|
||||
hook = createTaskReminderHook(mockCtx)
|
||||
})
|
||||
|
||||
test("does not inject reminder before 10 turns", async () => {
|
||||
//#given
|
||||
const sessionID = "test-session"
|
||||
const output = { output: "Result" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-${i}` },
|
||||
output
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(output.output).not.toContain("task tools haven't been used")
|
||||
})
|
||||
|
||||
test("injects reminder after 10 turns without task tool usage", async () => {
|
||||
//#given
|
||||
const sessionID = "test-session"
|
||||
const output = { output: "Result" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-${i}` },
|
||||
output
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(output.output).toContain("task tools haven't been used")
|
||||
})
|
||||
|
||||
test("resets counter when task tool is used", async () => {
|
||||
//#given
|
||||
const sessionID = "test-session"
|
||||
const output = { output: "Result" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-${i}` },
|
||||
output
|
||||
)
|
||||
}
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "task", sessionID, callID: "call-task" },
|
||||
output
|
||||
)
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-after-${i}` },
|
||||
output
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(output.output).not.toContain("task tools haven't been used")
|
||||
})
|
||||
|
||||
test("resets counter after injecting reminder", async () => {
|
||||
//#given
|
||||
const sessionID = "test-session"
|
||||
const output1 = { output: "Result 1" }
|
||||
const output2 = { output: "Result 2" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-1-${i}` },
|
||||
output1
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-2-${i}` },
|
||||
output2
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(output1.output).toContain("task tools haven't been used")
|
||||
expect(output2.output).not.toContain("task tools haven't been used")
|
||||
})
|
||||
|
||||
test("tracks separate counters per session", async () => {
|
||||
//#given
|
||||
const session1 = "session-1"
|
||||
const session2 = "session-2"
|
||||
const output1 = { output: "Result 1" }
|
||||
const output2 = { output: "Result 2" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID: session1, callID: `call-${i}` },
|
||||
output1
|
||||
)
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID: session2, callID: `call-${i}` },
|
||||
output2
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(output1.output).toContain("task tools haven't been used")
|
||||
expect(output2.output).not.toContain("task tools haven't been used")
|
||||
})
|
||||
|
||||
test("cleans up counters on session.deleted", async () => {
|
||||
//#given
|
||||
const sessionID = "test-session"
|
||||
const output = { output: "Result" }
|
||||
|
||||
//#when
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-${i}` },
|
||||
output
|
||||
)
|
||||
}
|
||||
await hook.event?.({ event: { type: "session.deleted", properties: { info: { id: sessionID } } } })
|
||||
const outputAfterDelete = { output: "Result" }
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await hook["tool.execute.after"]?.(
|
||||
{ tool: "bash", sessionID, callID: `call-after-${i}` },
|
||||
outputAfterDelete
|
||||
)
|
||||
}
|
||||
|
||||
//#then
|
||||
expect(outputAfterDelete.output).not.toContain("task tools haven't been used")
|
||||
})
|
||||
})
|
||||
1
src/hooks/task-reminder/index.ts
Normal file
1
src/hooks/task-reminder/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createTaskReminderHook } from "./hook";
|
||||
Loading…
x
Reference in New Issue
Block a user