* feat(agents): implement category-based agent organization system - Organize agents into domain categories (core, development, content, data, product, learning) - Move 10 agents to category subdirectories with proper git rename tracking - Update 13 subagents with category and type metadata in frontmatter - Add category metadata files (0-category.json) documenting common patterns - Implement local registry fallback in install script for offline development - Add comprehensive validation suite with 15 automated tests (100% pass rate) - Enhance registry validation with duplicate ID and consistency checks - Update eval framework with intelligent path resolution (backward compatible) - Archive legacy eval structure to _archive/ for reference - Update all documentation to reflect category-based structure - Bump version to 0.5.0 with accurate CHANGELOG Technical Details: - 23 agents organized (10 category agents, 13 subagents) - 6 category directories created - Path resolution supports both agent IDs and category paths - Registry schema updated to v2.0.0 - 159 files changed, 872 insertions(+), 10738 deletions(-) BREAKING CHANGE: Agent file paths now use category structure. Update references from .opencode/agent/openagent.md to .opencode/agent/core/openagent.md. Eval framework maintains backward compatibility via path resolution. * refactor(evals): consolidate documentation and enhance test infrastructure - Remove temporary project tracking files (PHASE_5_COMPLETE, PROJECT_COMPLETE, etc.) - Consolidate evaluation framework docs into main README - Enhance test execution with improved logging and multi-prompt support - Move system-builder from core to meta category - Add comprehensive test suites for openagent with organized structure - Create evaluation test structure for all subagents - Clean up archived workflows and redundant documentation - Update registry to reflect new agent organization - Add shared test templates and golden test patterns * feat(ci): add manual trigger support for bot-created PRs in validate-registry workflow - Add pr_number input to workflow_dispatch for manually triggering validation - Fetch PR details dynamically when triggered manually - Support both automatic (PR event) and manual (workflow_dispatch) triggers - Enable validation of bot-created PRs like automated version bumps - Update branch detection and push logic to handle both trigger types - Add documentation explaining how to manually trigger for bot PRs * feat(evals): add explicit context file validation to test framework - Add expectedContextFiles field to test YAML schema for explicit context file specification - Enhance context-loading-evaluator to support both auto-detect and explicit validation modes - Update documentation with comprehensive guide and examples - Clean up archived legacy test structure (90+ old test files) - Add new example tests demonstrating explicit context validation - Backward compatible with existing tests * feat(evals): add multi-agent logging system and performance optimizations Implement comprehensive multi-agent logging and performance improvements for the eval framework. Task 01: Multi-Agent Logging System - Add complete hierarchical logging module (evals/framework/src/logging/) - SessionTracker: tracks parent-child delegation hierarchies - MultiAgentLogger: pretty-prints logs with visual indentation - Formatters: box characters and emoji formatting - 37 passing unit tests (session-tracker, logger, integration) - Integrate with SDK event stream handler - Hook into session.created, message.updated, message.part.updated events - Real-time child session detection via timestamp heuristics - Message deduplication for cleaner output - Enable in debug mode only (<1% performance overhead) - Add demo script and comprehensive documentation Task 02: Performance Optimizations - Reduce grace period from 5s to 2s (67% reduction, 10-20% faster tests) - Add PerformanceMetricsEvaluator for bottleneck identification - Collects tool latencies, inference time, idle time - Provides full performance visibility - Update smoke test to validate delegation (core feature) - Replace simple read test with multi-agent delegation test - Tests both parent and child agent functionality - Validates multi-agent logging system Results: - 37 unit tests passing - Smoke test passing (95/100 score) - Clean hierarchical logging output - 10-20% faster test execution - Full multi-agent visibility Files changed: 16 files, +2678 lines Time saved: ~6-8 days (completed in 6 hours vs 3-5 day estimate) * feat(evals): show child agent execution in non-debug mode - Enable MultiAgentLogger in both debug and non-debug modes - Add verbose parameter to control output level - Non-verbose mode shows concise child session lifecycle: - Child agent started message - Child agent completed with duration - Verbose mode (--debug) shows full delegation hierarchy - Update documentation to reflect new behavior This provides visibility into delegation without overwhelming output, giving confidence that child agents are actually running. * feat(evals): improve delegation testing and behavior validation Major improvements to eval framework: Test Suite: - Add 00-smoke-test.yaml (basic read operation) - Rename 01-smoke-test.yaml → 02-delegation-test.yaml (delegation test) - Add simple-responder test agent for delegation testing - Add debug test: simple-subagent-call.yaml Evaluators: - Add agent-model-evaluator.ts for agent/model validation - Enhance behavior-evaluator.ts with detailed task tool output - Improve delegation-evaluator.ts with better evidence tracking Schema & Execution: - Add expectedAgent and expectedModel to test schema - Improve test executor with better logging and error handling Documentation: - Update evals/README.md with new features and performance improvements - Remove outdated MULTI_AGENT_LOGGING_COMPLETE.md Registry: - Add simple-responder test agent to registry Fixes: - Fix dashboard.sh path resolution These changes provide better visibility into delegation, improved test coverage, and clearer validation of agent behavior. * chore: add GitHub Actions workflow and plugin documentation - Add .github/workflows/evals/run-evaluations.yml for automated eval testing - Add dev/ai-tools/opencode/plugins/Plugin-inspiration.md for plugin development reference
1007 lines
No EOL
37 KiB
Markdown
1007 lines
No EOL
37 KiB
Markdown
OpenCode Plugin Architecture & oh-my-opencode Deep Dive
|
|
> A comprehensive guide to understanding OpenCode's plugin system and how oh-my-opencode transforms a single-agent assistant into a multi-model orchestration powerhouse.
|
|
---
|
|
Table of Contents
|
|
1. OpenCode Plugin System Overview (#1-opencode-plugin-system-overview)
|
|
2. The @opencode-ai/plugin Package (#2-the-opencode-aiplugin-package)
|
|
3. oh-my-opencode Architecture (#3-oh-my-opencode-architecture)
|
|
4. The Agent Orchestration System (#4-the-agent-orchestration-system)
|
|
5. Background Agent System (#5-background-agent-system)
|
|
6. The Hook Pipeline (#6-the-hook-pipeline)
|
|
7. Custom Tools (#7-custom-tools)
|
|
8. MCP Integrations (#8-mcp-integrations)
|
|
9. Configuration System (#9-configuration-system)
|
|
10. Plugin Loading & Updates (#10-plugin-loading--updates)
|
|
11. Creating Your Own Plugin (#11-creating-your-own-plugin)
|
|
12. Key Files Reference (#12-key-files-reference)
|
|
---
|
|
1. OpenCode Plugin System Overview
|
|
What is OpenCode?
|
|
OpenCode is a terminal-based AI coding assistant (similar to Claude Code) created by SST. It's highly extensible through a plugin system.
|
|
Repository: https://github.com/sst/opencode
|
|
How Plugins Work
|
|
Plugins are JavaScript/TypeScript modules that:
|
|
1. Receive a context object with SDK client, project info, and utilities
|
|
2. Return a hooks object to extend OpenCode's behavior
|
|
Plugin locations:
|
|
- .opencode/plugin/ (project-level)
|
|
- ~/.config/opencode/plugin/ (global)
|
|
- npm packages via opencode.json
|
|
The Plugin Interface
|
|
// From @opencode-ai/plugin
|
|
export type PluginInput = {
|
|
client: ReturnType<typeof createOpencodeClient> // SDK client for API calls
|
|
project: Project // Current project info
|
|
directory: string // Working directory path
|
|
worktree: string // Git worktree path
|
|
$: BunShell // Bun's shell API for commands
|
|
}
|
|
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
|
---
|
|
2. The @opencode-ai/plugin Package
|
|
Core Types
|
|
Source: packages/plugin/src/index.ts in sst/opencode
|
|
export interface Hooks {
|
|
// Event subscription - react to 40+ event types
|
|
event?: (input: { event: Event }) => Promise<void>
|
|
|
|
// Configuration hook - modify agents, MCPs, commands
|
|
config?: (input: Config) => Promise<void>
|
|
|
|
// Custom tools registration
|
|
tool?: {
|
|
[key: string]: ToolDefinition
|
|
}
|
|
|
|
// Authentication provider
|
|
auth?: AuthHook
|
|
|
|
// Chat lifecycle hooks
|
|
"chat.message"?: (input, output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
|
"chat.params"?: (input, output: { temperature: number; topP: number; topK: number; options: Record<string, any> }) => Promise<void>
|
|
|
|
// Permission hooks
|
|
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
|
|
|
// Tool execution hooks
|
|
"tool.execute.before"?: (input: { tool: string; sessionID: string; callID: string }, output: { args: any }) => Promise<void>
|
|
"tool.execute.after"?: (input: { tool: string; sessionID: string; callID: string }, output: { title: string; output: string; metadata: any }) => Promise<void>
|
|
|
|
// Experimental hooks
|
|
"experimental.chat.messages.transform"?: (input, output: { messages: { info: Message; parts: Part[] }[] }) => Promise<void>
|
|
"experimental.chat.system.transform"?: (input, output: { system: string[] }) => Promise<void>
|
|
"experimental.text.complete"?: (input: { sessionID: string; messageID: string; partID: string }, output: { text: string }) => Promise<void>
|
|
}
|
|
Tool Definition
|
|
export type ToolContext = {
|
|
sessionID: string
|
|
messageID: string
|
|
agent: string
|
|
abort: AbortSignal
|
|
}
|
|
export function tool<Args extends z.ZodRawShape>(input: {
|
|
description: string
|
|
args: Args
|
|
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string>
|
|
}) {
|
|
return input
|
|
}
|
|
tool.schema = z // Zod schema builder
|
|
Available Events (40+)
|
|
| Category | Events |
|
|
|----------|--------|
|
|
| Command | command.executed |
|
|
| File | file.edited, file.watcher.updated |
|
|
| Installation | installation.updated |
|
|
| LSP | lsp.client.diagnostics, lsp.updated |
|
|
| Message | message.part.removed, message.part.updated, message.removed, message.updated |
|
|
| Permission | permission.replied, permission.updated |
|
|
| Server | server.connected |
|
|
| Session | session.created, session.compacted, session.deleted, session.diff, session.error, session.idle, session.status, session.updated |
|
|
| Todo | todo.updated |
|
|
| Tool | tool.execute.after, tool.execute.before |
|
|
| TUI | tui.prompt.append, tui.command.execute, tui.toast.show |
|
|
---
|
|
3. oh-my-opencode Architecture
|
|
What is oh-my-opencode?
|
|
Repository: https://github.com/code-yeongyu/oh-my-opencode
|
|
oh-my-opencode is a comprehensive OpenCode plugin that transforms the single-agent experience into a multi-model, multi-agent orchestration system. It's described as "oh-my-zsh for OpenCode."
|
|
Project Structure
|
|
oh-my-opencode/
|
|
├── src/
|
|
│ ├── agents/ # AI agents (OmO, oracle, librarian, explore, etc.)
|
|
│ │ ├── omo.ts
|
|
│ │ ├── oracle.ts
|
|
│ │ ├── librarian.ts
|
|
│ │ ├── explore.ts
|
|
│ │ ├── frontend-ui-ux-engineer.ts
|
|
│ │ ├── document-writer.ts
|
|
│ │ ├── multimodal-looker.ts
|
|
│ │ ├── index.ts
|
|
│ │ ├── types.ts
|
|
│ │ └── utils.ts
|
|
│ ├── hooks/ # 21 lifecycle hooks
|
|
│ │ ├── anthropic-auto-compact/
|
|
│ │ ├── auto-update-checker/
|
|
│ │ ├── background-notification/
|
|
│ │ ├── claude-code-hooks/
|
|
│ │ ├── comment-checker/
|
|
│ │ ├── directory-readme-injector/
|
|
│ │ ├── interactive-bash-session/
|
|
│ │ ├── keyword-detector/
|
|
│ │ ├── non-interactive-env/
|
|
│ │ ├── rules-injector/
|
|
│ │ ├── session-recovery/
|
|
│ │ ├── think-mode/
|
|
│ │ ├── context-window-monitor.ts
|
|
│ │ ├── empty-task-response-detector.ts
|
|
│ │ ├── grep-output-truncator.ts
|
|
│ │ ├── session-notification.ts
|
|
│ │ ├── todo-continuation-enforcer.ts
|
|
│ │ ├── tool-output-truncator.ts
|
|
│ │ └── index.ts
|
|
│ ├── tools/ # Custom tools (LSP, AST-Grep, background tasks)
|
|
│ │ ├── lsp/
|
|
│ │ ├── ast-grep/
|
|
│ │ ├── background-task/
|
|
│ │ ├── call-omo-agent/
|
|
│ │ ├── look-at/
|
|
│ │ ├── grep/
|
|
│ │ ├── glob/
|
|
│ │ ├── interactive-bash/
|
|
│ │ ├── skill/
|
|
│ │ ├── slashcommand/
|
|
│ │ └── index.ts
|
|
│ ├── mcp/ # MCP server configurations
|
|
│ │ ├── context7.ts
|
|
│ │ ├── grep-app.ts
|
|
│ │ ├── websearch-exa.ts
|
|
│ │ └── index.ts
|
|
│ ├── features/ # Core features
|
|
│ │ ├── background-agent/
|
|
│ │ ├── claude-code-*-loader/
|
|
│ │ ├── hook-message-injector/
|
|
│ │ └── terminal/
|
|
│ ├── auth/ # Google Antigravity OAuth
|
|
│ ├── shared/ # Utilities
|
|
│ ├── config/ # Zod schema, types
|
|
│ └── index.ts # Main plugin entry
|
|
├── package.json
|
|
└── tsconfig.json
|
|
Main Entry Point (src/index.ts)
|
|
The plugin implements ALL available hooks:
|
|
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|
// 1. Load configuration
|
|
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
|
|
// 2. Initialize 20+ hooks with enable/disable support
|
|
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
|
? createTodoContinuationEnforcer(ctx) : null;
|
|
const contextWindowMonitor = isHookEnabled("context-window-monitor")
|
|
? createContextWindowMonitorHook(ctx) : null;
|
|
// ... 18 more hooks
|
|
|
|
// 3. Initialize background agent manager
|
|
const backgroundManager = new BackgroundManager(ctx);
|
|
|
|
// 4. Create tools
|
|
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
|
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
|
const lookAt = createLookAt(ctx);
|
|
|
|
return {
|
|
// Authentication
|
|
auth: googleAuthHooks?.auth,
|
|
|
|
// Custom tools
|
|
tool: {
|
|
...builtinTools, // LSP, AST-grep, grep, glob
|
|
...backgroundTools, // background_task, background_output, background_cancel
|
|
call_omo_agent,
|
|
look_at,
|
|
interactive_bash,
|
|
},
|
|
|
|
// Config modification
|
|
config: async (config) => {
|
|
// Inject custom agents
|
|
config.agent = { OmO: ..., oracle: ..., ... };
|
|
// Inject MCPs
|
|
config.mcp = { context7: ..., websearch_exa: ..., grep_app: ... };
|
|
// Load Claude Code commands, skills, agents
|
|
},
|
|
|
|
// Event handling
|
|
event: async (input) => {
|
|
// Process through all 20+ hooks
|
|
},
|
|
|
|
// Tool interception
|
|
"tool.execute.before": async (input, output) => { ... },
|
|
"tool.execute.after": async (input, output) => { ... },
|
|
|
|
// Chat hooks
|
|
"chat.message": async (input, output) => { ... },
|
|
"experimental.chat.messages.transform": async (input, output) => { ... },
|
|
};
|
|
};
|
|
---
|
|
4. The Agent Orchestration System
|
|
The Agent Team
|
|
oh-my-opencode creates a team of specialized agents, each using the optimal model for their task:
|
|
| Agent | Model | Purpose | Mode |
|
|
|-------|-------|---------|------|
|
|
| OmO | anthropic/claude-opus-4-5 | Primary orchestrator, delegates work | primary |
|
|
| oracle | openai/gpt-5.2 | Architecture, debugging, deep reasoning | subagent |
|
|
| librarian | anthropic/claude-sonnet-4-5 | External docs, GitHub examples | subagent |
|
|
| explore | opencode/grok-code | Fast codebase search | subagent |
|
|
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | UI generation | subagent |
|
|
| document-writer | google/gemini-3-pro-preview | Documentation | subagent |
|
|
| multimodal-looker | google/gemini-2.5-flash | PDF/image analysis | subagent |
|
|
OmO Agent System Prompt (Key Excerpts)
|
|
Source: src/agents/omo.ts
|
|
The OmO agent has a 400+ line system prompt that teaches it to:
|
|
const OMO_SYSTEM_PROMPT = `<Role>
|
|
You are OmO, the orchestrator agent for OpenCode.
|
|
**Identity**: Elite software engineer working at SF, Bay Area. You work, delegate, verify, deliver.
|
|
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents. Complex architecture → consult Oracle.
|
|
</Role>
|
|
<Behavior_Instructions>
|
|
## Phase 0 - Intent Gate (EVERY message)
|
|
### Step 1: Classify Request Type
|
|
| Type | Signal | Action |
|
|
|------|--------|--------|
|
|
| **Trivial** | Single file, known location | Direct tools only, no agents |
|
|
| **Explicit** | Specific file/line, clear command | Execute directly |
|
|
| **Exploratory** | "How does X work?" | Assess scope, then search |
|
|
| **Open-ended** | "Improve", "Refactor" | Assess codebase first |
|
|
## Phase 2A - Exploration & Research
|
|
### Tool Selection:
|
|
| Tool | Cost | When to Use |
|
|
|------|------|-------------|
|
|
| grep, glob, lsp_*, ast_grep | FREE | Always try first |
|
|
| explore agent | CHEAP | Multiple search angles |
|
|
| librarian agent | CHEAP | External docs, GitHub examples |
|
|
| oracle agent | EXPENSIVE | Architecture, review, debugging |
|
|
### Parallel Execution (DEFAULT behavior)
|
|
// CORRECT: Always background, always parallel
|
|
background_task(agent="explore", prompt="Find auth implementations...")
|
|
background_task(agent="librarian", prompt="Find JWT best practices...")
|
|
// Continue working immediately. Collect with background_output when needed.
|
|
## Phase 2B - Implementation
|
|
### GATE: Frontend Files (HARD BLOCK)
|
|
| Extension | Action |
|
|
|-----------|--------|
|
|
| .tsx, .jsx | DELEGATE |
|
|
| .vue, .svelte | DELEGATE |
|
|
| .css, .scss | DELEGATE |
|
|
ALL frontend = DELEGATE to frontend-ui-ux-engineer. Period.
|
|
</Behavior_Instructions>`
|
|
export const omoAgent: AgentConfig = {
|
|
description: "Powerful AI orchestrator for OpenCode...",
|
|
mode: "primary",
|
|
model: "anthropic/claude-opus-4-5",
|
|
thinking: {
|
|
type: "enabled",
|
|
budgetTokens: 32000, // Extended thinking enabled
|
|
},
|
|
maxTokens: 64000,
|
|
prompt: OMO_SYSTEM_PROMPT,
|
|
color: "#00CED1",
|
|
}
|
|
Oracle Agent (Strategic Advisor)
|
|
Source: src/agents/oracle.ts
|
|
export const oracleAgent: AgentConfig = {
|
|
description: "Expert technical advisor with deep reasoning...",
|
|
mode: "subagent",
|
|
model: "openai/gpt-5.2",
|
|
temperature: 0.1,
|
|
reasoningEffort: "medium",
|
|
textVerbosity: "high",
|
|
tools: { write: false, edit: false, task: false, background_task: false },
|
|
prompt: `You are a strategic technical advisor...
|
|
|
|
## Decision Framework
|
|
**Bias toward simplicity**: The right solution is typically the least complex one.
|
|
**Leverage what exists**: Favor modifications to current code over new components.
|
|
**One clear path**: Present a single primary recommendation.
|
|
**Signal the investment**: Tag recommendations with Quick(<1h), Short(1-4h), Medium(1-2d), Large(3d+).
|
|
`,
|
|
}
|
|
How Agents Get Injected
|
|
In the config hook:
|
|
config: async (config) => {
|
|
const builtinAgents = createBuiltinAgents(...);
|
|
|
|
// OmO becomes primary, original agents become subagents
|
|
config.agent = {
|
|
OmO: builtinAgents.OmO, // NEW primary
|
|
"OmO-Plan": omoPlanConfig, // NEW plan variant
|
|
...builtinAgents, // oracle, librarian, etc.
|
|
...config.agent, // User's original agents
|
|
build: { ...config.agent?.build, mode: "subagent" }, // DEMOTED
|
|
plan: { ...config.agent?.plan, mode: "subagent" }, // DEMOTED
|
|
};
|
|
}
|
|
---
|
|
5. Background Agent System
|
|
The BackgroundManager Class
|
|
Source: src/features/background-agent/manager.ts
|
|
This is the key innovation that enables parallel agent execution:
|
|
export class BackgroundManager {
|
|
private tasks: Map<string, BackgroundTask>
|
|
private notifications: Map<string, BackgroundTask[]>
|
|
private client: OpencodeClient
|
|
private pollingInterval?: Timer
|
|
async launch(input: LaunchInput): Promise<BackgroundTask> {
|
|
// 1. Create a NEW child session
|
|
const createResult = await this.client.session.create({
|
|
body: {
|
|
parentID: input.parentSessionID,
|
|
title: `Background: ${input.description}`
|
|
}
|
|
});
|
|
|
|
const sessionID = createResult.data.id;
|
|
|
|
// 2. Track the task
|
|
const task: BackgroundTask = {
|
|
id: `bg_${crypto.randomUUID().slice(0, 8)}`,
|
|
sessionID,
|
|
parentSessionID: input.parentSessionID,
|
|
description: input.description,
|
|
status: "running",
|
|
startedAt: new Date(),
|
|
};
|
|
this.tasks.set(task.id, task);
|
|
|
|
// 3. Fire async prompt (NON-BLOCKING!)
|
|
this.client.session.promptAsync({
|
|
path: { id: sessionID },
|
|
body: {
|
|
agent: input.agent,
|
|
tools: { task: false, background_task: false }, // Prevent recursion
|
|
parts: [{ type: "text", text: input.prompt }]
|
|
}
|
|
});
|
|
|
|
// 4. Start polling for completion
|
|
this.startPolling();
|
|
|
|
return task;
|
|
}
|
|
// Event handler for completion detection
|
|
handleEvent(event: Event): void {
|
|
if (event.type === "session.idle") {
|
|
const task = this.findBySession(sessionID);
|
|
if (task?.status === "running") {
|
|
task.status = "completed";
|
|
this.markForNotification(task);
|
|
this.notifyParentSession(task); // Send message back!
|
|
}
|
|
}
|
|
}
|
|
// Notify the main session when background task completes
|
|
private notifyParentSession(task: BackgroundTask): void {
|
|
// Show toast notification
|
|
this.client.tui.showToast({
|
|
body: {
|
|
title: "Background Task Completed",
|
|
message: `Task "${task.description}" finished.`,
|
|
variant: "success",
|
|
}
|
|
});
|
|
|
|
// Send a message to the parent session
|
|
this.client.session.prompt({
|
|
path: { id: task.parentSessionID },
|
|
body: {
|
|
parts: [{
|
|
type: "text",
|
|
text: `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished. Use background_output with task_id="${task.id}" to get results.`
|
|
}]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
Background Task Tools
|
|
Source: src/tools/background-task/tools.ts
|
|
// background_task - Launch an agent in background
|
|
export function createBackgroundTask(manager: BackgroundManager) {
|
|
return tool({
|
|
description: "Launch a background agent task...",
|
|
args: {
|
|
description: tool.schema.string(),
|
|
prompt: tool.schema.string(),
|
|
agent: tool.schema.string(),
|
|
},
|
|
async execute(args, toolContext) {
|
|
const task = await manager.launch({
|
|
description: args.description,
|
|
prompt: args.prompt,
|
|
agent: args.agent,
|
|
parentSessionID: toolContext.sessionID,
|
|
parentMessageID: toolContext.messageID,
|
|
});
|
|
|
|
return `Background task launched. Task ID: ${task.id}`;
|
|
},
|
|
});
|
|
}
|
|
// background_output - Get results from background task
|
|
export function createBackgroundOutput(manager: BackgroundManager, client: OpencodeClient) {
|
|
return tool({
|
|
description: "Get output from a background task...",
|
|
args: {
|
|
task_id: tool.schema.string(),
|
|
block: tool.schema.boolean().optional(), // Wait for completion?
|
|
},
|
|
async execute(args) {
|
|
const task = manager.getTask(args.task_id);
|
|
if (task.status === "completed") {
|
|
// Fetch messages from the task's session
|
|
const messages = await client.session.messages({ path: { id: task.sessionID } });
|
|
return formatTaskResult(task, messages);
|
|
}
|
|
return formatTaskStatus(task);
|
|
},
|
|
});
|
|
}
|
|
// background_cancel - Cancel running tasks
|
|
export function createBackgroundCancel(manager: BackgroundManager, client: OpencodeClient) {
|
|
return tool({
|
|
description: "Cancel background tasks...",
|
|
args: {
|
|
taskId: tool.schema.string().optional(),
|
|
all: tool.schema.boolean().optional(), // Cancel all?
|
|
},
|
|
async execute(args, toolContext) {
|
|
if (args.all) {
|
|
const tasks = manager.getTasksByParentSession(toolContext.sessionID);
|
|
for (const task of tasks.filter(t => t.status === "running")) {
|
|
client.session.abort({ path: { id: task.sessionID } });
|
|
task.status = "cancelled";
|
|
}
|
|
}
|
|
// ...
|
|
},
|
|
});
|
|
}
|
|
The Parallel Execution Flow
|
|
You: "Add authentication to my app"
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ OmO (Claude Opus 4.5) │
|
|
│ "I'll orchestrate this..." │
|
|
└─────────────────────────────────────────┘
|
|
│
|
|
│ 1. Creates todo list
|
|
│ 2. Launches background agents:
|
|
│
|
|
├──▶ background_task(agent="librarian", "Find JWT best practices...")
|
|
│ └── Creates child session, runs async
|
|
│
|
|
├──▶ background_task(agent="explore", "Find existing auth patterns...")
|
|
│ └── Creates child session, runs async
|
|
│
|
|
│ 3. Starts implementing immediately (doesn't wait!)
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ [MEANWHILE: Background agents working] │
|
|
│ librarian → searching docs │
|
|
│ explore → scanning codebase │
|
|
└─────────────────────────────────────────┘
|
|
│
|
|
│ 4. BackgroundManager detects completion
|
|
│ 5. Sends notification to OmO's session
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────┐
|
|
│ [BACKGROUND TASK COMPLETED] │
|
|
│ Task "Find JWT best practices" done │
|
|
│ Use background_output to get results │
|
|
└─────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
OmO: background_output(task_id="bg_abc123")
|
|
│
|
|
▼
|
|
Integrates findings, continues work...
|
|
---
|
|
6. The Hook Pipeline
|
|
Hook Categories
|
|
oh-my-opencode implements 21 hooks across several categories:
|
|
Session Management
|
|
- session-recovery: Auto-recovers from API errors
|
|
- session-notification: OS notifications when session goes idle
|
|
- anthropic-auto-compact: Auto-summarizes when hitting token limits
|
|
Task Management
|
|
- todo-continuation-enforcer: Forces agent to complete all todos
|
|
- empty-task-response-detector: Warns about empty task responses
|
|
Context Management
|
|
- context-window-monitor: Implements "Context Window Anxiety Management"
|
|
- directory-readme-injector: Injects README.md context
|
|
- directory-agents-injector: Injects AGENTS.md context
|
|
- rules-injector: Conditional rules from .claude/rules/
|
|
Output Processing
|
|
- tool-output-truncator: Truncates verbose tool output
|
|
- grep-output-truncator: Specifically handles grep output
|
|
Quality Control
|
|
- comment-checker: Prevents AI-style excessive comments
|
|
- keyword-detector: Detects ultrawork, search, analyze keywords
|
|
- think-mode: Auto-enables extended thinking for complex tasks
|
|
Background System
|
|
- background-notification: Routes events to BackgroundManager
|
|
Compatibility
|
|
- claude-code-hooks: Pre/PostToolUse, UserPromptSubmit, Stop hooks
|
|
- non-interactive-env: Handles non-interactive environments
|
|
- interactive-bash-session: Tmux integration
|
|
Event Handler Pipeline
|
|
event: async (input) => {
|
|
// Process through ALL hooks in sequence
|
|
await autoUpdateChecker?.event(input);
|
|
await claudeCodeHooks.event(input);
|
|
await backgroundNotificationHook?.event(input); // Background tracking!
|
|
await sessionNotification?.(input);
|
|
await todoContinuationEnforcer?.handler(input);
|
|
await contextWindowMonitor?.event(input);
|
|
await directoryAgentsInjector?.event(input);
|
|
await directoryReadmeInjector?.event(input);
|
|
await rulesInjector?.event(input);
|
|
await thinkMode?.event(input);
|
|
await anthropicAutoCompact?.event(input);
|
|
await keywordDetector?.event(input);
|
|
await agentUsageReminder?.event(input);
|
|
await interactiveBashSession?.event(input);
|
|
|
|
// Handle session lifecycle
|
|
if (event.type === "session.created") { ... }
|
|
if (event.type === "session.idle") { ... }
|
|
if (event.type === "session.error") { ... }
|
|
}
|
|
Tool Execution Hooks
|
|
"tool.execute.before": async (input, output) => {
|
|
// Claude Code hooks (PreToolUse)
|
|
await claudeCodeHooks["tool.execute.before"](input, output);
|
|
// Non-interactive environment handling
|
|
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
|
// Comment checking
|
|
await commentChecker?.["tool.execute.before"](input, output);
|
|
|
|
// Disable dangerous tools for subagents
|
|
if (input.tool === "task") {
|
|
output.args.tools = {
|
|
...output.args.tools,
|
|
background_task: false, // Prevent nested background tasks
|
|
};
|
|
}
|
|
},
|
|
"tool.execute.after": async (input, output) => {
|
|
// Claude Code hooks (PostToolUse)
|
|
await claudeCodeHooks["tool.execute.after"](input, output);
|
|
// Truncate verbose output
|
|
await toolOutputTruncator?.["tool.execute.after"](input, output);
|
|
// Track context usage
|
|
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
|
// Inject directory context
|
|
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
|
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
|
// Inject conditional rules
|
|
await rulesInjector?.["tool.execute.after"](input, output);
|
|
},
|
|
---
|
|
7. Custom Tools
|
|
LSP Tools (11 tools)
|
|
Source: src/tools/lsp/
|
|
export const builtinTools = {
|
|
// Information
|
|
lsp_hover, // Type info, docs at position
|
|
lsp_goto_definition, // Jump to definition
|
|
lsp_find_references, // Find all usages
|
|
lsp_document_symbols, // File symbol outline
|
|
lsp_workspace_symbols, // Search symbols by name
|
|
lsp_diagnostics, // Get errors/warnings
|
|
lsp_servers, // List available LSP servers
|
|
|
|
// Refactoring
|
|
lsp_prepare_rename, // Validate rename
|
|
lsp_rename, // Rename symbol across workspace
|
|
lsp_code_actions, // Get quick fixes/refactorings
|
|
lsp_code_action_resolve, // Apply code action
|
|
}
|
|
AST-Grep Tools
|
|
Source: src/tools/ast-grep/
|
|
export const astGrepTools = {
|
|
ast_grep_search, // AST-aware pattern search (25 languages)
|
|
ast_grep_replace, // AST-aware code replacement
|
|
}
|
|
Search Tools (Improved)
|
|
export const searchTools = {
|
|
grep, // With timeout protection (original hangs forever)
|
|
glob, // With timeout protection
|
|
}
|
|
Special Tools
|
|
// look_at - Multimodal analysis via subagent
|
|
export function createLookAt(ctx: PluginInput) {
|
|
return tool({
|
|
description: "Analyze files using multimodal-looker agent",
|
|
args: { file: tool.schema.string() },
|
|
async execute(args, toolContext) {
|
|
// Delegates to multimodal-looker agent internally
|
|
// Saves context in main session
|
|
},
|
|
});
|
|
}
|
|
// call_omo_agent - Specialized agent caller
|
|
export function createCallOmoAgent(ctx: PluginInput, manager: BackgroundManager) {
|
|
return tool({
|
|
description: "Call explore or librarian agents",
|
|
args: {
|
|
subagent_type: tool.schema.enum(["explore", "librarian"]),
|
|
prompt: tool.schema.string(),
|
|
run_in_background: tool.schema.boolean(),
|
|
},
|
|
async execute(args, toolContext) {
|
|
if (args.run_in_background) {
|
|
return await manager.launch(...);
|
|
}
|
|
return await executeSynchronously(...);
|
|
},
|
|
});
|
|
}
|
|
// interactive_bash - Tmux integration
|
|
export const interactive_bash = tool({
|
|
description: "Execute commands in interactive tmux session",
|
|
// ...
|
|
});
|
|
---
|
|
8. MCP Integrations
|
|
Built-in MCPs
|
|
Source: src/mcp/
|
|
// context7.ts - Official documentation lookup
|
|
export const context7 = {
|
|
type: "remote",
|
|
url: "https://mcp.context7.io/sse",
|
|
enabled: true,
|
|
}
|
|
// websearch_exa.ts - Real-time web search
|
|
export const websearch_exa = {
|
|
type: "remote",
|
|
url: "https://mcp.exa.ai/sse",
|
|
enabled: true,
|
|
}
|
|
// grep_app.ts - GitHub code search
|
|
export const grep_app = {
|
|
type: "remote",
|
|
url: "https://mcp.grep.app/sse",
|
|
enabled: true,
|
|
}
|
|
MCP Injection
|
|
config: async (config) => {
|
|
config.mcp = {
|
|
...config.mcp,
|
|
...createBuiltinMcps(pluginConfig.disabled_mcps), // context7, websearch_exa, grep_app
|
|
...loadMcpConfigs(), // Claude Code .mcp.json files
|
|
};
|
|
}
|
|
---
|
|
9. Configuration System
|
|
Config File Locations
|
|
1. ~/.config/opencode/oh-my-opencode.json (user-level)
|
|
2. .opencode/oh-my-opencode.json (project-level, overrides user)
|
|
Config Schema
|
|
Source: src/config/schema.ts
|
|
export const OhMyOpenCodeConfigSchema = z.object({
|
|
// Enable Google OAuth
|
|
google_auth: z.boolean().optional(),
|
|
|
|
// Agent overrides
|
|
agents: z.record(z.object({
|
|
model: z.string().optional(),
|
|
temperature: z.number().optional(),
|
|
prompt: z.string().optional(),
|
|
tools: z.record(z.boolean()).optional(),
|
|
disable: z.boolean().optional(),
|
|
description: z.string().optional(),
|
|
mode: z.enum(["primary", "subagent"]).optional(),
|
|
color: z.string().optional(),
|
|
permission: z.object({
|
|
edit: z.enum(["ask", "allow", "deny"]).optional(),
|
|
bash: z.union([
|
|
z.enum(["ask", "allow", "deny"]),
|
|
z.record(z.enum(["ask", "allow", "deny"]))
|
|
]).optional(),
|
|
}).optional(),
|
|
})).optional(),
|
|
|
|
// Disable specific agents
|
|
disabled_agents: z.array(z.string()).optional(),
|
|
|
|
// Disable specific MCPs
|
|
disabled_mcps: z.array(z.enum(["context7", "websearch_exa", "grep_app"])).optional(),
|
|
|
|
// Disable specific hooks
|
|
disabled_hooks: z.array(z.string()).optional(),
|
|
|
|
// Claude Code compatibility toggles
|
|
claude_code: z.object({
|
|
mcp: z.boolean().optional(),
|
|
commands: z.boolean().optional(),
|
|
skills: z.boolean().optional(),
|
|
agents: z.boolean().optional(),
|
|
hooks: z.boolean().optional(),
|
|
}).optional(),
|
|
|
|
// OmO agent settings
|
|
omo_agent: z.object({
|
|
disabled: z.boolean().optional(),
|
|
}).optional(),
|
|
});
|
|
Example Configuration
|
|
{
|
|
$schema: https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json,
|
|
|
|
google_auth: true,
|
|
|
|
agents: {
|
|
OmO: {
|
|
model: anthropic/claude-sonnet-4
|
|
},
|
|
oracle: {
|
|
temperature: 0.3
|
|
},
|
|
frontend-ui-ux-engineer: {
|
|
disable: true
|
|
}
|
|
},
|
|
|
|
disabled_hooks: [comment-checker],
|
|
disabled_mcps: [grep_app],
|
|
|
|
claude_code: {
|
|
hooks: true,
|
|
commands: true,
|
|
mcp: false
|
|
}
|
|
}
|
|
---
|
|
10. Plugin Loading & Updates
|
|
How OpenCode Loads Plugins
|
|
Source: packages/opencode/src/plugin/index.ts (in sst/opencode)
|
|
for (let plugin of plugins) {
|
|
if (!plugin.startsWith("file://")) {
|
|
// Parse version: "oh-my-opencode@2.1.6"
|
|
const lastAtIndex = plugin.lastIndexOf("@")
|
|
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
|
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
|
|
|
// Install via Bun from npm registry
|
|
plugin = await BunProc.install(pkg, version)
|
|
}
|
|
|
|
const mod = await import(plugin)
|
|
for (const [_name, fn] of Object.entries(mod)) {
|
|
const init = await fn(input)
|
|
hooks.push(init)
|
|
}
|
|
}
|
|
Plugin Cache Location
|
|
~/.cache/opencode/
|
|
├── node_modules/
|
|
│ └── oh-my-opencode/ ← THE ACTUAL PLUGIN CODE
|
|
│ ├── dist/
|
|
│ │ └── index.js
|
|
│ └── package.json
|
|
├── package.json
|
|
└── bun.lockb
|
|
Auto-Update Checker
|
|
Source: src/hooks/auto-update-checker/
|
|
export function createAutoUpdateCheckerHook(ctx: PluginInput) {
|
|
return {
|
|
event: async ({ event }) => {
|
|
if (event.type !== "session.created") return;
|
|
|
|
// Check npm registry for latest version
|
|
const result = await checkForUpdate(ctx.directory);
|
|
|
|
if (result.needsUpdate) {
|
|
// Show notification
|
|
await ctx.client.tui.showToast({
|
|
body: {
|
|
title: `OhMyOpenCode ${result.latestVersion}`,
|
|
message: `v${result.latestVersion} available. Restart OpenCode to apply.`,
|
|
}
|
|
});
|
|
|
|
// Invalidate cache (delete cached package)
|
|
invalidatePackage("oh-my-opencode");
|
|
// Next restart will download new version
|
|
}
|
|
},
|
|
};
|
|
}
|
|
async function getLatestVersion(): Promise<string | null> {
|
|
const response = await fetch("https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags");
|
|
const data = await response.json();
|
|
return data.latest;
|
|
}
|
|
Version Pinning
|
|
// Latest (auto-updates on restart)
|
|
{ plugin: [oh-my-opencode] }
|
|
// Pinned (never auto-updates)
|
|
{ plugin: [oh-my-opencode@2.1.6] }
|
|
// Local development
|
|
{ plugin: [file:///path/to/oh-my-opencode] }
|
|
---
|
|
11. Creating Your Own Plugin
|
|
Minimal package.json
|
|
{
|
|
name: my-opencode-plugin,
|
|
version: 1.0.0,
|
|
main: dist/index.js,
|
|
types: dist/index.d.ts,
|
|
type: module,
|
|
files: [dist],
|
|
exports: {
|
|
.: {
|
|
types: ./dist/index.d.ts,
|
|
import: ./dist/index.js
|
|
}
|
|
},
|
|
scripts: {
|
|
build: bun build src/index.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly,
|
|
prepublishOnly: bun run build
|
|
},
|
|
dependencies: {
|
|
@opencode-ai/plugin: ^1.0.162
|
|
},
|
|
devDependencies: {
|
|
bun-types: latest,
|
|
typescript: ^5.7.3
|
|
},
|
|
peerDependencies: {
|
|
bun: >=1.0.0
|
|
}
|
|
}
|
|
Minimal tsconfig.json
|
|
{
|
|
compilerOptions: {
|
|
target: ESNext,
|
|
module: ESNext,
|
|
moduleResolution: bundler,
|
|
declaration: true,
|
|
declarationDir: dist,
|
|
outDir: dist,
|
|
rootDir: src,
|
|
strict: true,
|
|
esModuleInterop: true,
|
|
skipLibCheck: true,
|
|
lib: [ESNext],
|
|
types: [bun-types]
|
|
},
|
|
include: [src/**/*],
|
|
exclude: [node_modules, dist]
|
|
}
|
|
Minimal Plugin (src/index.ts)
|
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
import { tool } from "@opencode-ai/plugin"
|
|
const MyPlugin: Plugin = async ({ client, directory }) => {
|
|
return {
|
|
// Add custom tools
|
|
tool: {
|
|
my_tool: tool({
|
|
description: "Does something useful",
|
|
args: {
|
|
input: tool.schema.string(),
|
|
},
|
|
async execute(args) {
|
|
return `Processed: ${args.input}`;
|
|
},
|
|
}),
|
|
},
|
|
// Modify config
|
|
config: async (config) => {
|
|
config.agent = {
|
|
...config.agent,
|
|
"my-agent": {
|
|
description: "My custom agent",
|
|
model: "anthropic/claude-sonnet-4",
|
|
prompt: "You are helpful...",
|
|
},
|
|
};
|
|
},
|
|
// React to events
|
|
event: async ({ event }) => {
|
|
if (event.type === "session.created") {
|
|
await client.tui.showToast({
|
|
body: {
|
|
title: "Plugin Loaded",
|
|
message: "My plugin is active!",
|
|
variant: "success",
|
|
}
|
|
}).catch(() => {});
|
|
}
|
|
},
|
|
// Intercept tool calls
|
|
"tool.execute.before": async (input, output) => {
|
|
console.log(`Tool ${input.tool} called`);
|
|
},
|
|
};
|
|
};
|
|
export default MyPlugin;
|
|
Publish to npm
|
|
npm login
|
|
npm publish
|
|
Users Install
|
|
{
|
|
plugin: [my-opencode-plugin]
|
|
}
|
|
---
|
|
12. Key Files Reference
|
|
Core oh-my-opencode Files
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| src/index.ts | Main plugin entry, orchestrates everything |
|
|
| src/agents/*.ts | Agent definitions (OmO, oracle, etc.) |
|
|
| src/features/background-agent/manager.ts | Background task management |
|
|
| src/tools/background-task/tools.ts | background_task, background_output, background_cancel |
|
|
| src/tools/lsp/*.ts | LSP tool implementations |
|
|
| src/hooks/*.ts | All 21 hooks |
|
|
| src/mcp/*.ts | MCP server configurations |
|
|
| src/config/schema.ts | Zod config schema |
|
|
OpenCode Core Files (sst/opencode)
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| packages/plugin/src/index.ts | Plugin interface types |
|
|
| packages/plugin/src/tool.ts | Tool definition helper |
|
|
| packages/opencode/src/plugin/index.ts | Plugin loading logic |
|
|
---
|
|
Summary: Why oh-my-opencode Works So Well
|
|
1. Multi-Model Orchestration: Uses the right model for each task (GPT-5.2 for reasoning, Gemini for UI, Grok for speed)
|
|
2. True Parallel Execution: BackgroundManager creates separate sessions, allowing multiple agents to work simultaneously
|
|
3. Automatic Notifications: Event system detects task completion and injects messages into the main conversation
|
|
4. Defensive Hooks: 21 hooks ensure tasks complete, errors recover, context is managed
|
|
5. Comprehensive System Prompt: OmO's 400+ line prompt teaches proper delegation and orchestration
|
|
6. IDE-Quality Tools: LSP and AST-grep tools give agents the same capabilities as human developers
|
|
7. Context Injection: Automatic README/AGENTS.md injection and conditional rules
|
|
8. Claude Code Compatibility: Existing Claude Code configs just work
|
|
The plugin transforms a single-agent assistant into a dev team where you're the manager and AI models are your specialized engineers.
|
|
---
|
|
Session Summary
|
|
What We Covered
|
|
1. Researched the @opencode-ai/plugin API - Examined the Plugin type, Hooks interface, available events, and tool definitions from the sst/opencode repository
|
|
2. Deep-dived into oh-my-opencode architecture - Read and analyzed:
|
|
- Main entry point (src/index.ts) - 566 lines
|
|
- Agent definitions (src/agents/) - OmO, oracle, librarian, explore, etc.
|
|
- BackgroundManager (src/features/background-agent/manager.ts) - 430 lines
|
|
- Background tools (src/tools/background-task/tools.ts) - 332 lines
|
|
- Auto-update checker (src/hooks/auto-update-checker/)
|
|
3. Explained key concepts:
|
|
- How plugins are loaded from npm (not GitHub)
|
|
- How the config hook injects custom agents
|
|
- How BackgroundManager enables parallel execution
|
|
- How notifications work (toast + message injection)
|
|
- The difference between schema URL (IDE autocomplete) vs actual code (npm)
|
|
4. Created publishing guide - How to create, build, and publish your own OpenCode plugin
|
|
Key Files Examined
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/src/index.ts
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/src/agents/*.ts
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/src/features/background-agent/manager.ts
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/src/tools/background-task/tools.ts
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/src/hooks/auto-update-checker/*.ts
|
|
- /Users/darrenhinde/Documents/GitHub/test-larvel/oh-my-opencode/package.json
|
|
- /tmp/opencode-repo/packages/plugin/src/index.ts (from sst/opencode)
|
|
▣ Compaction · claude-opus-4-5 · 3m 0s
|
|
Thinking: The user is asking me to continue if there are next steps. Let me check what the current state is and if there's anything pending.
|
|
Looking at the comprehensive summary I provided, it seems like we've completed a thorough documentation of the OpenCode plugin system and oh-my-opencode architecture.
|
|
Let me check if there's a todo list or any pending tasks by reading the todo list.
|
|
⚙ todoread
|
|
Thinking: The todo list is empty, which means we've completed the documentation work. The user asked "What did we do so far?" and I provided a comprehensive summary. Now they're asking to continue if there are next steps.
|
|
Since there's no active todo list and the documentation seems complete, I should ask what they'd like to do next. Possible directions:
|
|
1. Create a custom plugin based on what we learned
|
|
2. Modify/extend oh-my-opencode
|
|
3. Work on something else in the codebase
|
|
4. Dive deeper into specific components
|
|
Let me ask what direction they want to take. |