refactor(omo-task): rename to call_omo_agent with run_in_background parameter

- Rename omo-task to call-omo-agent with mandatory run_in_background parameter
- Implement background mode using BackgroundManager (fire-and-forget abort)
- Implement sync mode with existing subagent logic
- Fix background_cancel: use fire-and-forget abort to prevent parent session interruption
- Add call_omo_agent to tool disable list in explore/librarian agents
- Add call_omo_agent to tool disable list in BackgroundManager

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-12 18:39:53 +09:00
parent f007437991
commit 12c0b7b6c0
13 changed files with 262 additions and 136 deletions

1
src/codex-auth.ts Normal file
View File

@@ -0,0 +1 @@
export * from "opencode-openai-codex-auth";

View File

@@ -81,6 +81,7 @@ export class BackgroundManager {
background_task: false,
background_output: false,
background_cancel: false,
call_omo_agent: false,
},
parts: [{ type: "text", text: input.prompt }],
},
@@ -248,7 +249,7 @@ export class BackgroundManager {
setTimeout(async () => {
try {
await this.client.session.promptAsync({
await this.client.session.prompt({
path: { id: mainSessionID },
body: {
parts: [{ type: "text", text: message }],
@@ -256,9 +257,9 @@ export class BackgroundManager {
query: { directory: this.directory },
})
this.clearNotificationsForTask(task.id)
log("[background-agent] Successfully sent promptAsync to main session")
log("[background-agent] Successfully sent prompt to main session")
} catch (error) {
log("[background-agent] promptAsync failed:", String(error))
log("[background-agent] prompt failed:", String(error))
}
}, 200)
}

View File

@@ -37,7 +37,7 @@ import {
getCurrentSessionTitle,
} from "./features/claude-code-session-state";
import { updateTerminalTitle } from "./features/terminal";
import { builtinTools, createOmoTask, createBackgroundTools } from "./tools";
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
import { BackgroundManager } from "./features/background-agent";
import { createBuiltinMcps } from "./mcp";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
@@ -169,13 +169,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
const omoTask = createOmoTask(ctx);
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
return {
tool: {
...builtinTools,
...backgroundTools,
omo_task: omoTask,
call_omo_agent: callOmoAgent,
},
"chat.message": async (input, output) => {
@@ -205,13 +205,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
if (config.agent.explore) {
config.agent.explore.tools = {
...config.agent.explore.tools,
omo_task: false,
call_omo_agent: false,
};
}
if (config.agent.librarian) {
config.agent.librarian.tools = {
...config.agent.librarian.tools,
omo_task: false,
call_omo_agent: false,
};
}

View File

@@ -196,13 +196,11 @@ export function createBackgroundCancel(manager: BackgroundManager, client: Openc
Only running tasks can be cancelled.`
}
const abortResult = await client.session.abort({
// Fire-and-forget: abort 요청을 보내고 await 하지 않음
// await 하면 메인 세션까지 abort 되는 문제 발생
client.session.abort({
path: { id: task.sessionID },
})
if (abortResult.error) {
return `❌ Failed to abort session: ${(abortResult.error as any).message || String(abortResult.error)}`
}
}).catch(() => {})
task.status = "cancelled"
task.completedAt = new Date()

View File

@@ -1,6 +1,6 @@
export const ALLOWED_AGENTS = ["explore", "librarian"] as const
export const TASK_TOOL_DESCRIPTION_TEMPLATE = `Launch a new agent to handle complex, multi-step tasks autonomously.
export const CALL_OMO_AGENT_DESCRIPTION = `Launch a new agent to handle complex, multi-step tasks autonomously.
This is a restricted version of the Task tool that only allows spawning explore and librarian agents.
@@ -9,9 +9,15 @@ Available agent types:
When using this tool, you must specify a subagent_type parameter to select which agent type to use.
**IMPORTANT: run_in_background parameter is REQUIRED**
- \`run_in_background=true\`: Task runs asynchronously in background. Returns immediately with task_id.
Use \`background_output\` tool with the returned task_id to check progress or retrieve results.
- \`run_in_background=false\`: Task runs synchronously. Waits for completion and returns full result.
Usage notes:
1. Launch multiple agents concurrently whenever possible, to maximize performance
2. When the agent is done, it will return a single message back to you
3. Each agent invocation is stateless unless you provide a session_id
4. Your prompt should contain a highly detailed task description for the agent to perform autonomously
5. Clearly tell the agent whether you expect it to write code or just to do research`
5. Clearly tell the agent whether you expect it to write code or just to do research
6. For long-running research tasks, use run_in_background=true to avoid blocking`

View File

@@ -1,3 +1,3 @@
export * from "./types"
export * from "./constants"
export { createOmoTask } from "./tools"
export { createCallOmoAgent } from "./tools"

View File

@@ -0,0 +1,167 @@
import { tool, type PluginInput } from "@opencode-ai/plugin"
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
import type { CallOmoAgentArgs } from "./types"
import type { BackgroundManager } from "../../features/background-agent"
import { log } from "../../shared/logger"
export function createCallOmoAgent(
ctx: PluginInput,
backgroundManager: BackgroundManager
) {
const agentDescriptions = ALLOWED_AGENTS.map(
(name) => `- ${name}: Specialized agent for ${name} tasks`
).join("\n")
const description = CALL_OMO_AGENT_DESCRIPTION.replace("{agents}", agentDescriptions)
return tool({
description,
args: {
description: tool.schema.string().describe("A short (3-5 words) description of the task"),
prompt: tool.schema.string().describe("The task for the agent to perform"),
subagent_type: tool.schema
.enum(ALLOWED_AGENTS)
.describe("The type of specialized agent to use for this task (explore or librarian only)"),
run_in_background: tool.schema
.boolean()
.describe("REQUIRED. true: run asynchronously (use background_output to get results), false: run synchronously and wait for completion"),
session_id: tool.schema.string().describe("Existing Task session to continue").optional(),
},
async execute(args: CallOmoAgentArgs, toolContext) {
log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`)
if (!ALLOWED_AGENTS.includes(args.subagent_type as typeof ALLOWED_AGENTS[number])) {
return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`
}
if (args.run_in_background) {
if (args.session_id) {
return `Error: session_id is not supported in background mode. Use run_in_background=false to continue an existing session.`
}
return await executeBackground(args, toolContext, backgroundManager)
}
return await executeSync(args, toolContext, ctx)
},
})
}
async function executeBackground(
args: CallOmoAgentArgs,
toolContext: { sessionID: string; messageID: string },
manager: BackgroundManager
): Promise<string> {
try {
const task = await manager.launch({
description: args.description,
prompt: args.prompt,
agent: args.subagent_type,
parentSessionID: toolContext.sessionID,
parentMessageID: toolContext.messageID,
})
return `Background agent task launched successfully.
Task ID: ${task.id}
Session ID: ${task.sessionID}
Description: ${task.description}
Agent: ${task.agent} (subagent)
Status: ${task.status}
Use \`background_output\` tool with task_id="${task.id}" to check progress or retrieve results.
- block=false: Check status without waiting
- block=true (default): Wait for completion and get result`
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return `Failed to launch background agent task: ${message}`
}
}
async function executeSync(
args: CallOmoAgentArgs,
toolContext: { sessionID: string },
ctx: PluginInput
): Promise<string> {
let sessionID: string
if (args.session_id) {
log(`[call_omo_agent] Using existing session: ${args.session_id}`)
const sessionResult = await ctx.client.session.get({
path: { id: args.session_id },
})
if (sessionResult.error) {
log(`[call_omo_agent] Session get error:`, sessionResult.error)
return `Error: Failed to get existing session: ${sessionResult.error}`
}
sessionID = args.session_id
} else {
log(`[call_omo_agent] Creating new session with parent: ${toolContext.sessionID}`)
const createResult = await ctx.client.session.create({
body: {
parentID: toolContext.sessionID,
title: `${args.description} (@${args.subagent_type} subagent)`,
},
})
if (createResult.error) {
log(`[call_omo_agent] Session create error:`, createResult.error)
return `Error: Failed to create session: ${createResult.error}`
}
sessionID = createResult.data.id
log(`[call_omo_agent] Created session: ${sessionID}`)
}
log(`[call_omo_agent] Sending prompt to session ${sessionID}`)
log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100))
await ctx.client.session.prompt({
path: { id: sessionID },
body: {
agent: args.subagent_type,
tools: {
task: false,
call_omo_agent: false,
},
parts: [{ type: "text", text: args.prompt }],
},
})
log(`[call_omo_agent] Prompt sent, fetching messages...`)
const messagesResult = await ctx.client.session.messages({
path: { id: sessionID },
})
if (messagesResult.error) {
log(`[call_omo_agent] Messages error:`, messagesResult.error)
return `Error: Failed to get messages: ${messagesResult.error}`
}
const messages = messagesResult.data
log(`[call_omo_agent] Got ${messages.length} messages`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const lastAssistantMessage = messages
.filter((m: any) => m.info.role === "assistant")
.sort((a: any, b: any) => (b.info.time?.created || 0) - (a.info.time?.created || 0))[0]
if (!lastAssistantMessage) {
log(`[call_omo_agent] No assistant message found`)
log(`[call_omo_agent] All messages:`, JSON.stringify(messages, null, 2))
return `Error: No assistant response found\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
}
log(`[call_omo_agent] Found assistant message with ${lastAssistantMessage.parts.length} parts`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const textParts = lastAssistantMessage.parts.filter((p: any) => p.type === "text")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseText = textParts.map((p: any) => p.text).join("\n")
log(`[call_omo_agent] Got response, length: ${responseText.length}`)
const output =
responseText + "\n\n" + ["<task_metadata>", `session_id: ${sessionID}`, "</task_metadata>"].join("\n")
return output
}

View File

@@ -2,14 +2,15 @@ import type { ALLOWED_AGENTS } from "./constants"
export type AllowedAgentType = (typeof ALLOWED_AGENTS)[number]
export interface OmoTaskArgs {
export interface CallOmoAgentArgs {
description: string
prompt: string
subagent_type: string
run_in_background: boolean
session_id?: string
}
export interface OmoTaskResult {
export interface CallOmoAgentSyncResult {
title: string
metadata: {
summary?: Array<{

View File

@@ -33,7 +33,7 @@ import type { BackgroundManager } from "../features/background-agent"
type OpencodeClient = PluginInput["client"]
export { createOmoTask } from "./omo-task"
export { createCallOmoAgent } from "./call-omo-agent"
export function createBackgroundTools(manager: BackgroundManager, client: OpencodeClient) {
return {

View File

@@ -1,111 +0,0 @@
import { tool, type PluginInput } from "@opencode-ai/plugin"
import { ALLOWED_AGENTS, TASK_TOOL_DESCRIPTION_TEMPLATE } from "./constants"
import type { OmoTaskArgs } from "./types"
import { log } from "../../shared/logger"
export function createOmoTask(ctx: PluginInput) {
const agentDescriptions = ALLOWED_AGENTS.map((name) => `- ${name}: Specialized agent for ${name} tasks`).join(
"\n"
)
const description = TASK_TOOL_DESCRIPTION_TEMPLATE.replace("{agents}", agentDescriptions)
return tool({
description,
args: {
description: tool.schema.string().describe("A short (3-5 words) description of the task"),
prompt: tool.schema.string().describe("The task for the agent to perform"),
subagent_type: tool.schema
.enum(ALLOWED_AGENTS)
.describe("The type of specialized agent to use for this task (explore or librarian only)"),
session_id: tool.schema.string().describe("Existing Task session to continue").optional(),
},
async execute(args: OmoTaskArgs, toolContext) {
log(`[omo_task] Starting with agent: ${args.subagent_type}`)
if (!ALLOWED_AGENTS.includes(args.subagent_type as any)) {
return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.`
}
let sessionID: string
if (args.session_id) {
log(`[omo_task] Using existing session: ${args.session_id}`)
const sessionResult = await ctx.client.session.get({
path: { id: args.session_id },
})
if (sessionResult.error) {
log(`[omo_task] Session get error:`, sessionResult.error)
return `Error: Failed to get existing session: ${sessionResult.error}`
}
sessionID = args.session_id
} else {
log(`[omo_task] Creating new session with parent: ${toolContext.sessionID}`)
const createResult = await ctx.client.session.create({
body: {
parentID: toolContext.sessionID,
title: `${args.description} (@${args.subagent_type} subagent)`,
},
})
if (createResult.error) {
log(`[omo_task] Session create error:`, createResult.error)
return `Error: Failed to create session: ${createResult.error}`
}
sessionID = createResult.data.id
log(`[omo_task] Created session: ${sessionID}`)
}
log(`[omo_task] Sending prompt to session ${sessionID}`)
log(`[omo_task] Prompt text:`, args.prompt.substring(0, 100))
await ctx.client.session.prompt({
path: { id: sessionID },
body: {
agent: args.subagent_type,
tools: {
task: false,
omo_task: false,
},
parts: [{ type: "text", text: args.prompt }],
},
})
log(`[omo_task] Prompt sent, fetching messages...`)
const messagesResult = await ctx.client.session.messages({
path: { id: sessionID },
})
if (messagesResult.error) {
log(`[omo_task] Messages error:`, messagesResult.error)
return `Error: Failed to get messages: ${messagesResult.error}`
}
const messages = messagesResult.data
log(`[omo_task] Got ${messages.length} messages`)
const lastAssistantMessage = messages
.filter((m: any) => m.info.role === "assistant")
.sort((a: any, b: any) => (b.info.time?.created || 0) - (a.info.time?.created || 0))[0]
if (!lastAssistantMessage) {
log(`[omo_task] No assistant message found`)
log(`[omo_task] All messages:`, JSON.stringify(messages, null, 2))
return `Error: No assistant response found\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
}
log(`[omo_task] Found assistant message with ${lastAssistantMessage.parts.length} parts`)
const textParts = lastAssistantMessage.parts.filter((p: any) => p.type === "text")
const responseText = textParts.map((p: any) => p.text).join("\n")
log(`[omo_task] Got response, length: ${responseText.length}`)
const output =
responseText + "\n\n" + ["<task_metadata>", `session_id: ${sessionID}`, "</task_metadata>"].join("\n")
return output
},
})
}