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
1273 lines
33 KiB
Markdown
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.
|