OpenWriterControle/dev/ai-tools/opencode/building-plugins.md
darrenhinde 2bc97007c1 refactor: simplify agent execution philosophy and reorganize context structure
Major Changes:
- Simplified OpenAgent execution philosophy (universal agent, delegate sparingly)
- Reorganized context into 3-tier structure: standards/, workflows/, system/
- Improved approval gate wording for clarity
- Enhanced .gitignore with comprehensive Node.js patterns
- Added comprehensive plugin development guide

Context Reorganization:
- Consolidated scattered files into organized structure with Quick Reference sections
- Emphasis on lazy loading for token efficiency (60-80% token savings)
- New standards: code.md, docs.md, tests.md, patterns.md, analysis.md
- New workflows: delegation.md, sessions.md, task-breakdown.md, review.md
- New system: context-guide.md (master reference)

Delegation Improvements:
- Removed complex XML-style routing rules
- Simplified to clear when/how structure with 7 delegation criteria
- Added Perspective and Simulation as delegation triggers
- Clearer delegation template and process

Documentation Enhancements:
- Added building-plugins.md (1,274 lines - comprehensive plugin guide)
- Updated how-context-works.md with research-backed architecture
- Enhanced slash-commands-and-subagents.md with path resolution findings

Content Preservation:
- context-discovery.md → system/context-guide.md + workflows/sessions.md
- context-management.md → workflows/sessions.md
- essential-patterns.md → standards/patterns.md
- session-management.md → workflows/sessions.md
2025-11-19 13:13:06 +00:00

1273 lines
33 KiB
Markdown

# Building OpenCode Plugins
## Introduction
OpenCode plugins extend the functionality of the OpenCode AI agent system. Plugins can:
- Monitor system events (messages, sessions, errors)
- Add custom tools for agents to use
- Modify LLM parameters dynamically
- Intercept and transform messages
- Control permissions and behavior
- Track and manage context usage
Plugins are written in TypeScript and registered via `opencode.json`.
---
## Getting Started
### Basic Plugin Structure
Create a TypeScript file (e.g., `my-plugin.ts`):
```typescript
import { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
// Plugin initialization
// ctx provides: client, project, directory, worktree, $
return {
// Your hooks go here
async event({ event }) {
// Handle system events
},
tool: {
// Custom tools
},
async config(config) {
// Load configuration
}
}
}
```
### Plugin Registration
Add to your `opencode.json`:
```json
{
"plugin": [
"file:///absolute/path/to/my-plugin.ts",
"file://./relative/path/to/plugin.ts"
]
}
```
**Path formats:**
- Absolute: `file:///Users/name/plugins/my-plugin.ts`
- Relative to config: `file://./plugins/my-plugin.ts`
- Home directory: `file://~/opencode/plugins/my-plugin.ts`
### Plugin Input Context (`PluginInput`)
When your plugin initializes, you receive:
```typescript
{
client: OpencodeClient, // Full SDK client for OpenCode APIs
project: Project, // Current project metadata
directory: string, // Working directory path
worktree: string, // Git worktree root
$: BunShell // Bun shell for executing commands
}
```
---
## Core Plugin Hooks
### 1. `event` Hook - System Event Monitoring
Monitor all system events in real-time:
```typescript
async event({ event }) {
// Available event types:
if (event.type === "session.created") {
const session = event.properties.info
console.log(`New session: ${session.id}`)
}
if (event.type === "session.updated") {
// Session metadata changed
}
if (event.type === "session.deleted") {
// Clean up tracking state
}
if (event.type === "message.updated") {
const msg = event.properties.info
console.log(`Message tokens: ${msg.tokens.input}`)
}
if (event.type === "message.removed") {
// Message was deleted
}
if (event.type === "session.compacted") {
// Session context was compacted
}
if (event.type === "session.diff") {
// File changes detected
}
if (event.type === "session.error") {
// Error occurred
}
}
```
**Key Events:**
- `session.*` - Session lifecycle
- `message.*` - Message lifecycle
- `message.part.*` - Message part updates
- `session.compacted` - Context compaction
- `session.diff` - File changes
- `session.error` - Errors
---
### 2. `chat.message` Hook - Message Interception
Intercept user messages before they're sent to the agent:
```typescript
async "chat.message"(input, output) {
// input: { message: UserMessage }
// output: { message: UserMessage, parts: MessagePart[] }
// Example: Add metadata to messages
output.message.metadata = {
timestamp: Date.now(),
source: "plugin"
}
// Example: Modify message parts
output.parts.forEach(part => {
if (part.type === "text") {
// Transform text content
}
})
// Example: Validate before sending
if (output.message.content.length > 10000) {
console.warn("Large message detected")
}
}
```
---
### 3. `chat.params` Hook - LLM Parameter Modification
Modify LLM parameters dynamically:
```typescript
async "chat.params"(input, output) {
// input.model = Model being used
// input.provider = Provider info
// input.message = Current message
// output.temperature = 0-1
// output.topP = 0-1
// output.options = Record<string, any>
// Example: Adjust temperature based on task
if (input.message.content.includes("creative")) {
output.temperature = 0.8
} else {
output.temperature = 0.3
}
// Example: Add custom model parameters
output.options = {
top_k: 40,
repetition_penalty: 1.1
}
// Example: Model-specific settings
if (input.model.id.includes("claude")) {
output.options.thinking = {
type: "enabled",
budget_tokens: 2000
}
}
}
```
---
### 4. `tool` Hook - Custom Tool Creation
Add custom tools that agents can use:
```typescript
import { tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
// Simple tool with no arguments
get_timestamp: tool({
description: "Get current timestamp",
args: {},
async execute() {
return new Date().toISOString()
}
}),
// Tool with arguments
search_database: tool({
description: "Search the project database",
args: {
query: {
type: "string",
description: "Search query"
},
limit: {
type: "number",
description: "Maximum results",
default: 10
}
},
async execute({ query, limit }) {
const results = await searchDB(query, limit)
return JSON.stringify(results, null, 2)
}
}),
// Tool with access to context
analyze_project: tool({
description: "Analyze current project structure",
args: {},
async execute() {
// Access plugin context
const files = await ctx.$`find ${ctx.worktree} -type f`.text()
return `Project has ${files.split('\n').length} files`
}
})
}
}
}
```
**Tool Best Practices:**
- Clear, descriptive names
- Detailed descriptions (agent uses these to decide when to call)
- Type-safe arguments with descriptions
- Return strings or JSON-serializable data
- Handle errors gracefully
---
### 5. `tool.execute.before` / `tool.execute.after` - Tool Execution Hooks
Hook into tool execution to track or modify behavior:
```typescript
async "tool.execute.before"(input, output) {
// input.tool = Tool name
// input.sessionID = Current session
// input.callID = Unique call ID
// output.args = Tool arguments (mutable)
console.log(`Executing tool: ${input.tool}`)
// Example: Modify arguments
if (input.tool === "bash" && output.args.command.includes("rm -rf")) {
throw new Error("Dangerous command blocked")
}
// Example: Track usage
trackToolUsage(input.tool, input.sessionID)
}
async "tool.execute.after"(input, output) {
// output.title = Tool result title
// output.output = Tool result output
// output.metadata = Tool metadata
console.log(`Tool ${input.tool} completed`)
// Example: Log results
if (output.output.length > 5000) {
console.warn(`Tool ${input.tool} returned large output`)
}
// Example: Track performance
const duration = output.metadata?.duration
if (duration > 5000) {
console.warn(`Tool ${input.tool} took ${duration}ms`)
}
}
```
---
### 6. `config` Hook - Configuration Loading
Load plugin configuration from `opencode.json`:
```typescript
async config(config) {
// Access user's opencode.json config
const mySettings = config.myPlugin || {}
// Example: Load settings with defaults
const settings = {
enabled: mySettings.enabled ?? true,
threshold: mySettings.threshold ?? 100,
apiKey: mySettings.apiKey || process.env.MY_API_KEY
}
console.log("Plugin configured:", settings)
return settings
}
```
**In `opencode.json`:**
```json
{
"plugin": ["file://./my-plugin.ts"],
"myPlugin": {
"enabled": true,
"threshold": 150,
"apiKey": "secret"
}
}
```
---
### 7. `permission.ask` Hook - Permission Control
Control what the agent can do:
```typescript
async "permission.ask"(input, output) {
// input = Permission request details
// output.status = "ask" | "deny" | "allow"
// Example: Auto-approve safe operations
if (input.tool === "read" || input.tool === "list") {
output.status = "allow"
return
}
// Example: Block dangerous operations
if (input.tool === "bash" && input.args?.command?.includes("rm -rf /")) {
output.status = "deny"
return
}
// Example: Conditional approval
if (input.tool === "write") {
const path = input.args?.filePath
if (path?.startsWith("/tmp/")) {
output.status = "allow"
} else {
output.status = "ask" // Prompt user
}
}
}
```
---
## SDK Client Methods
Available via `ctx.client`:
### Session Management
```typescript
// Get session info
const session = await ctx.client.session.get({
path: { id: sessionID }
})
// List all sessions
const sessions = await ctx.client.session.list()
// Get session messages
const messages = await ctx.client.session.messages.list({
path: { id: sessionID }
})
// Fork session (preserve context)
const newSession = await ctx.client.session.fork({
path: { id: sessionID }
})
// Delete session
await ctx.client.session.delete({
path: { id: sessionID }
})
// Execute command in session
await ctx.client.session.command({
path: { id: sessionID },
body: {
command: "compact",
arguments: "",
agent: "build",
model: "anthropic/claude-3.5-sonnet"
}
})
```
### Event Publishing
```typescript
// Publish UI events (toasts)
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Alert",
message: "Something happened",
variant: "info" | "warning" | "error" | "success",
duration: 5000
}
}
})
```
### Shell Commands
```typescript
// Execute shell commands via Bun
const output = await ctx.$`ls -la ${ctx.worktree}`.text()
// With error handling
try {
const result = await ctx.$`git status`.text()
console.log(result)
} catch (error) {
console.error("Command failed:", error)
}
```
---
## Best Practices
### Error Handling
Always handle errors gracefully - never let exceptions crash OpenCode:
```typescript
async event({ event }) {
try {
// Your logic here
await processEvent(event)
} catch (error) {
console.error("Plugin error:", error)
// Log but don't throw - let OpenCode continue
}
}
```
### State Management
Use Maps for efficient state tracking:
```typescript
const MyPlugin: Plugin = async (ctx) => {
// State persists across hook calls
const sessionState = new Map<string, {
created: number
messageCount: number
}>()
return {
async event({ event }) {
if (event.type === "session.created") {
sessionState.set(event.properties.info.id, {
created: Date.now(),
messageCount: 0
})
}
if (event.type === "session.deleted") {
// Always clean up!
sessionState.delete(event.properties.info.id)
}
}
}
}
```
### Performance Considerations
Keep hooks fast and non-blocking:
```typescript
async event({ event }) {
// ✅ Good: Fast checks first
if (event.type !== "message.updated") return
if (!shouldProcess(event)) return
// ✅ Good: Async operations don't block
processEventAsync(event).catch(console.error)
// ❌ Bad: Heavy computation blocks event loop
// await expensiveAnalysis() // Don't do this
}
```
### Cleanup Patterns
Always clean up resources:
```typescript
const MyPlugin: Plugin = async (ctx) => {
const resources = new Map()
return {
async event({ event }) {
if (event.type === "session.created") {
resources.set(event.properties.info.id, createResource())
}
if (event.type === "session.deleted") {
const resource = resources.get(event.properties.info.id)
if (resource) {
await resource.cleanup()
resources.delete(event.properties.info.id)
}
}
}
}
}
```
---
## Context-Aware Plugins
Context-aware plugins monitor and manage the agent's context window to optimize token usage and prevent context overflow.
### Understanding Context Data
#### Token Tracking
From `message.updated` events:
```typescript
msg.tokens = {
input: number, // Context window tokens
output: number, // Generated tokens
reasoning: number, // Reasoning tokens (if applicable)
cache: {
read: number, // Prompt cache hits
write: number // Prompt cache writes
}
}
```
**Key calculation:**
```typescript
// Actual context window usage
const contextSize = msg.tokens.input + msg.tokens.cache.read
```
#### Message Parts
Track different types of content:
```typescript
part.type =
| "text" // Text content
| "file" // File attachments
| "tool" // Tool calls & results
| "step-start" // Agent thinking step
| "step-finish" // Step completion with token usage
| "snapshot" // Code snapshot
| "patch" // Code changes
| "reasoning" // Model reasoning (if enabled)
```
**Tool results can be large:**
```typescript
if (part.type === "tool" && part.state.status === "completed") {
const outputSize = estimateTokens(part.state.output)
// Track this for context management
}
```
---
### Example: Context Monitor Plugin
A complete plugin that monitors context usage and warns when approaching limits:
```typescript
import { Plugin } from "@opencode-ai/plugin"
export const ContextMonitor: Plugin = async (ctx) => {
// Track state per session
const sessionWarnings = new Map<string, {
warned80k: boolean
warned100k: boolean
lastTokenCount: number
history: Array<{ timestamp: number, tokens: number }>
}>()
// Configuration
const config = {
warn80k: 80_000,
warn100k: 100_000,
autoCompact: false
}
return {
async config(userConfig) {
// Load user settings
Object.assign(config, userConfig.contextMonitor || {})
},
async event({ event }) {
// Monitor message updates for token usage
if (event.type === "message.updated") {
const msg = event.properties.info
// Only process assistant messages with token data
if (msg.role === "assistant" && msg.tokens) {
const totalTokens = msg.tokens.input + msg.tokens.cache.read
const sessionID = msg.sessionID
// Initialize tracking if needed
if (!sessionWarnings.has(sessionID)) {
sessionWarnings.set(sessionID, {
warned80k: false,
warned100k: false,
lastTokenCount: 0,
history: []
})
}
const state = sessionWarnings.get(sessionID)!
// Track history
state.history.push({
timestamp: Date.now(),
tokens: totalTokens
})
// Keep only last 10 entries
if (state.history.length > 10) {
state.history.shift()
}
// Calculate growth rate
const growthRate = calculateGrowthRate(state.history)
// Warning at 80k tokens
if (totalTokens >= config.warn80k && !state.warned80k) {
state.warned80k = true
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Context Warning",
message: `Context at ${totalTokens.toLocaleString()} tokens (${Math.round(totalTokens / 2000)}% of 200k limit)`,
variant: "warning",
duration: 8000
}
}
})
console.log(`⚠️ Context at ${totalTokens} tokens - consider compacting soon`)
}
// Critical warning at 100k tokens
if (totalTokens >= config.warn100k && !state.warned100k) {
state.warned100k = true
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Context Critical",
message: `Context at ${totalTokens.toLocaleString()} tokens - compact recommended`,
variant: "error",
duration: 10000
}
}
})
console.log(`🚨 Context at ${totalTokens} tokens - compact now!`)
// Auto-compact if enabled
if (config.autoCompact) {
await ctx.client.session.command({
path: { id: sessionID },
body: {
command: "compact",
arguments: "",
agent: "build",
model: msg.model
}
})
console.log("✅ Auto-compaction triggered")
}
}
// Log growth rate if concerning
if (growthRate > 10000) {
console.log(`📈 Rapid context growth: ${growthRate} tokens/min`)
}
state.lastTokenCount = totalTokens
}
}
// Reset warnings after compaction
if (event.type === "session.compacted") {
const sessionID = event.properties.info.id
const state = sessionWarnings.get(sessionID)
if (state) {
state.warned80k = false
state.warned100k = false
console.log("✅ Context compacted - warnings reset")
}
}
// Clean up when session deleted
if (event.type === "session.deleted") {
sessionWarnings.delete(event.properties.info.id)
}
},
// Add custom tool for checking context
tool: {
check_context: tool({
description: "Check current session context size and health",
args: {},
async execute() {
// Get current session (would need session ID in real implementation)
const sessions = await ctx.client.session.list()
const currentSession = sessions[0] // Simplified
if (!currentSession) {
return "No active session"
}
const state = sessionWarnings.get(currentSession.id)
if (!state) {
return "No context data available yet"
}
const tokens = state.lastTokenCount
const percentage = Math.round((tokens / 200000) * 100)
const growthRate = calculateGrowthRate(state.history)
return `Context Health Report:
- Current tokens: ${tokens.toLocaleString()}
- Percentage of 200k limit: ${percentage}%
- Growth rate: ${growthRate.toLocaleString()} tokens/min
- Status: ${tokens < 80000 ? "✅ Healthy" : tokens < 100000 ? "⚠️ Warning" : "🚨 Critical"}
${tokens >= 80000 ? "\nRecommendation: Consider compacting context" : ""}`
}
})
}
}
}
// Helper function to calculate growth rate
function calculateGrowthRate(history: Array<{ timestamp: number, tokens: number }>): number {
if (history.length < 2) return 0
const first = history[0]
const last = history[history.length - 1]
const tokenDiff = last.tokens - first.tokens
const timeDiff = (last.timestamp - first.timestamp) / 1000 / 60 // minutes
if (timeDiff === 0) return 0
return Math.round(tokenDiff / timeDiff)
}
// Helper function for token estimation
function estimateTokens(text: string): number {
// Rough estimation: ~4 characters per token
return Math.ceil(text.length / 4)
}
```
**Configuration in `opencode.json`:**
```json
{
"plugin": ["file://./context-monitor.ts"],
"contextMonitor": {
"warn80k": 80000,
"warn100k": 100000,
"autoCompact": false
}
}
```
---
### Context Optimization Strategies
#### 1. Monitor Context Growth Rate
Track how fast context is growing to predict when compaction will be needed:
```typescript
const history: Array<{ timestamp: number, tokens: number }> = []
async event({ event }) {
if (event.type === "message.updated" && event.properties.info.tokens) {
const tokens = event.properties.info.tokens.input +
event.properties.info.tokens.cache.read
history.push({ timestamp: Date.now(), tokens })
// Keep last 10 measurements
if (history.length > 10) history.shift()
// Calculate tokens per minute
const rate = calculateGrowthRate(history)
if (rate > 10000) {
console.warn(`⚠️ Rapid context growth: ${rate} tokens/min`)
}
}
}
```
#### 2. Smart Compaction Triggers
Don't just check total tokens - check token efficiency:
```typescript
const efficiency = outputTokens / inputTokens
if (efficiency < 0.01 && inputTokens > 100_000) {
// Too much input for little output = should compact
console.warn("Low token efficiency - compaction recommended")
}
```
#### 3. Predictive Warnings
Warn before hitting the limit:
```typescript
async "chat.params"(input, output) {
const model = input.model
const modelLimit = model.limit.context
const currentTokens = await getCurrentContextSize(input.message.sessionID)
// Estimate next turn will add ~5k tokens
const projectedGrowth = 5000
if (currentTokens + projectedGrowth > modelLimit * 0.9) {
console.warn("⚠️ Will exceed limit soon - compact before next turn!")
}
}
```
#### 4. Model-Specific Context Limits
Adjust behavior based on model limits:
```typescript
async "chat.params"(input, output) {
const model = input.model
// model.limit.context = Total context window
// model.limit.output = Max output tokens
const available = model.limit.context - model.limit.output
const used = await getCurrentContextSize(input.message.sessionID)
const remaining = available - used
if (remaining < 10_000) {
console.warn(`⚠️ Only ${remaining} tokens remaining!`)
// Reduce temperature for more focused responses
output.temperature = 0.2
}
}
```
#### 5. Adaptive Context Configuration
Adjust agent behavior based on context size:
```typescript
async "chat.params"(input, output) {
const contextTokens = await getContextSize(input.message.sessionID)
if (contextTokens > 100_000) {
// High context - optimize for focus
output.temperature = 0.3
output.topP = 0.9
console.log("High context mode: reduced temperature for focus")
} else {
// Normal context - allow creativity
output.temperature = 0.7
output.topP = 0.95
}
}
```
#### 6. Track Tool Impact on Context
Monitor which tools add the most context:
```typescript
const toolImpact = new Map<string, number>()
async "tool.execute.after"(input, output) {
const tokensAdded = estimateTokens(output.output)
const current = toolImpact.get(input.tool) || 0
toolImpact.set(input.tool, current + tokensAdded)
// Log top offenders
if (tokensAdded > 5000) {
console.log(`Tool ${input.tool} added ${tokensAdded} tokens`)
}
}
```
---
### Understanding Compaction
#### How Compaction Works
OpenCode automatically compacts when:
```typescript
// System checks if overflow
SessionCompaction.isOverflow({
tokens: msg.tokens,
model: currentModel
})
// Returns true when:
// (input + cache.read + output) > (context_limit - output_limit)
```
**Key constants:**
- `PRUNE_MINIMUM = 20_000` - Minimum tokens to prune
- `PRUNE_PROTECT = 40_000` - Protect recent 40k tokens
#### Compaction Process
1. Keeps last 2 user turns fully
2. Summarizes older messages
3. Prunes old tool call outputs (marks as `compacted`)
4. Emits `session.compacted` event
#### Detecting Compacted State
```typescript
async event({ event }) {
if (event.type === "session.compacted") {
console.log("✅ Context was compacted")
// Reset your warnings/tracking
}
}
// Check if session has been compacted
const messages = await ctx.client.session.messages.list({
path: { id: sessionID }
})
const hasCompacted = messages.some(m =>
m.info.role === "assistant" && m.info.summary === true
)
```
---
### Complete Context-Aware Plugin Example
Here's a production-ready context monitoring plugin with all optimizations:
```typescript
import { Plugin, tool } from "@opencode-ai/plugin"
interface SessionState {
warned80k: boolean
warned100k: boolean
warned150k: boolean
lastTokenCount: number
history: Array<{ timestamp: number, tokens: number }>
toolImpact: Map<string, number>
compactionCount: number
}
export const AdvancedContextMonitor: Plugin = async (ctx) => {
const sessions = new Map<string, SessionState>()
const config = {
warn80k: 80_000,
warn100k: 100_000,
warn150k: 150_000,
autoCompact: false,
adaptiveTemperature: true,
trackToolImpact: true
}
function getOrCreateState(sessionID: string): SessionState {
if (!sessions.has(sessionID)) {
sessions.set(sessionID, {
warned80k: false,
warned100k: false,
warned150k: false,
lastTokenCount: 0,
history: [],
toolImpact: new Map(),
compactionCount: 0
})
}
return sessions.get(sessionID)!
}
function calculateGrowthRate(history: Array<{ timestamp: number, tokens: number }>): number {
if (history.length < 2) return 0
const first = history[0]
const last = history[history.length - 1]
const tokenDiff = last.tokens - first.tokens
const timeDiff = (last.timestamp - first.timestamp) / 1000 / 60
return timeDiff === 0 ? 0 : Math.round(tokenDiff / timeDiff)
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4)
}
return {
async config(userConfig) {
Object.assign(config, userConfig.advancedContextMonitor || {})
console.log("Advanced Context Monitor configured:", config)
},
async event({ event }) {
try {
// Track message updates
if (event.type === "message.updated") {
const msg = event.properties.info
if (msg.role === "assistant" && msg.tokens) {
const totalTokens = msg.tokens.input + msg.tokens.cache.read
const state = getOrCreateState(msg.sessionID)
// Update history
state.history.push({ timestamp: Date.now(), tokens: totalTokens })
if (state.history.length > 10) state.history.shift()
const growthRate = calculateGrowthRate(state.history)
// Progressive warnings
if (totalTokens >= config.warn80k && !state.warned80k) {
state.warned80k = true
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Context Notice",
message: `${totalTokens.toLocaleString()} tokens (${Math.round(totalTokens/2000)}%)`,
variant: "info",
duration: 5000
}
}
})
}
if (totalTokens >= config.warn100k && !state.warned100k) {
state.warned100k = true
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Context Warning",
message: `${totalTokens.toLocaleString()} tokens - consider compacting`,
variant: "warning",
duration: 8000
}
}
})
}
if (totalTokens >= config.warn150k && !state.warned150k) {
state.warned150k = true
await ctx.client.events.publish({
body: {
type: "tui.toast.show",
properties: {
title: "Context Critical",
message: `${totalTokens.toLocaleString()} tokens - compact now!`,
variant: "error",
duration: 10000
}
}
})
if (config.autoCompact) {
await ctx.client.session.command({
path: { id: msg.sessionID },
body: {
command: "compact",
arguments: "",
agent: "build",
model: msg.model
}
})
console.log("✅ Auto-compaction triggered")
}
}
state.lastTokenCount = totalTokens
}
}
// Track compaction
if (event.type === "session.compacted") {
const state = sessions.get(event.properties.info.id)
if (state) {
state.warned80k = false
state.warned100k = false
state.warned150k = false
state.compactionCount++
console.log(`✅ Compaction #${state.compactionCount} completed`)
}
}
// Cleanup
if (event.type === "session.deleted") {
sessions.delete(event.properties.info.id)
}
} catch (error) {
console.error("Context monitor error:", error)
}
},
async "tool.execute.after"(input, output) {
if (!config.trackToolImpact) return
try {
const tokensAdded = estimateTokens(output.output || "")
const state = getOrCreateState(input.sessionID)
const current = state.toolImpact.get(input.tool) || 0
state.toolImpact.set(input.tool, current + tokensAdded)
if (tokensAdded > 5000) {
console.log(`📊 Tool ${input.tool} added ${tokensAdded.toLocaleString()} tokens`)
}
} catch (error) {
console.error("Tool tracking error:", error)
}
},
async "chat.params"(input, output) {
if (!config.adaptiveTemperature) return
try {
const msg = input.message
const state = sessions.get(msg.sessionID)
if (state && state.lastTokenCount > 100_000) {
// High context - reduce temperature for focus
output.temperature = 0.3
console.log("🎯 Adaptive mode: reduced temperature (high context)")
}
} catch (error) {
console.error("Adaptive params error:", error)
}
},
tool: {
context_health: tool({
description: "Get detailed context health report for current session",
args: {},
async execute() {
try {
const sessions_list = await ctx.client.session.list()
if (!sessions_list.length) return "No active sessions"
const currentSession = sessions_list[0]
const state = sessions.get(currentSession.id)
if (!state) return "No context data available yet"
const tokens = state.lastTokenCount
const percentage = Math.round((tokens / 200000) * 100)
const growthRate = calculateGrowthRate(state.history)
let report = `📊 Context Health Report\n\n`
report += `Current tokens: ${tokens.toLocaleString()}\n`
report += `Percentage of 200k limit: ${percentage}%\n`
report += `Growth rate: ${growthRate.toLocaleString()} tokens/min\n`
report += `Compactions: ${state.compactionCount}\n`
report += `Status: ${tokens < 80000 ? "✅ Healthy" : tokens < 100000 ? "⚠️ Warning" : "🚨 Critical"}\n`
if (config.trackToolImpact && state.toolImpact.size > 0) {
report += `\n📈 Top Tools by Token Impact:\n`
const sorted = Array.from(state.toolImpact.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
sorted.forEach(([tool, tokens]) => {
report += ` - ${tool}: ${tokens.toLocaleString()} tokens\n`
})
}
if (tokens >= 80000) {
report += `\n💡 Recommendation: Consider compacting context`
}
return report
} catch (error) {
return `Error: ${error}`
}
}
})
}
}
}
```
---
## Key Takeaways
### For All Plugins:
1. **Always handle errors** - Never crash OpenCode
2. **Clean up state** - Delete tracking data when sessions end
3. **Keep hooks fast** - Don't block the event loop
4. **Use TypeScript** - Type safety prevents bugs
5. **Test thoroughly** - Plugins run in production
### For Context-Aware Plugins:
1. **Monitor `message.updated` events** for token tracking
2. **Calculate context as** `input + cache.read` tokens
3. **Warn at 80-100k tokens** even if model supports more
4. **Track `session.compacted` events** to reset warnings
5. **Use adaptive strategies** - adjust behavior based on context size
6. **Track tool impact** - identify which tools add most context
7. **Provide tools** - let agents check their own context health
8. **Test with real sessions** - generate lots of context to verify
The plugin system provides complete visibility and control over OpenCode's behavior, enabling sophisticated context management and optimization strategies.