refactor(tools/call-omo-agent): split tools.ts into focused modules under 200 LOC
- Extract getMessageDir to message-dir.ts - Extract executeBackground to background-executor.ts - Extract session creation logic to session-creator.ts - Extract polling logic to completion-poller.ts - Extract message processing to message-processor.ts - Create sync-executor.ts to orchestrate sync execution - Add ToolContextWithMetadata type to types.ts - tools.ts now <200 LOC and focused on tool definition
This commit is contained in:
parent
984da95f15
commit
6bb9a3b7bc
83
src/tools/call-omo-agent/background-executor.ts
Normal file
83
src/tools/call-omo-agent/background-executor.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
import { consumeNewMessages } from "../../shared/session-cursor"
|
||||||
|
import { findFirstMessageWithAgent, findNearestMessageWithFields } from "../../features/hook-message-injector"
|
||||||
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
|
import { getMessageDir } from "./message-dir"
|
||||||
|
|
||||||
|
export async function executeBackground(
|
||||||
|
args: CallOmoAgentArgs,
|
||||||
|
toolContext: {
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
agent: string
|
||||||
|
abort: AbortSignal
|
||||||
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||||
|
},
|
||||||
|
manager: BackgroundManager
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const messageDir = getMessageDir(toolContext.sessionID)
|
||||||
|
const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
||||||
|
const firstMessageAgent = messageDir ? findFirstMessageWithAgent(messageDir) : null
|
||||||
|
const sessionAgent = getSessionAgent(toolContext.sessionID)
|
||||||
|
const parentAgent = toolContext.agent ?? sessionAgent ?? firstMessageAgent ?? prevMessage?.agent
|
||||||
|
|
||||||
|
log("[call_omo_agent] parentAgent resolution", {
|
||||||
|
sessionID: toolContext.sessionID,
|
||||||
|
messageDir,
|
||||||
|
ctxAgent: toolContext.agent,
|
||||||
|
sessionAgent,
|
||||||
|
firstMessageAgent,
|
||||||
|
prevMessageAgent: prevMessage?.agent,
|
||||||
|
resolvedParentAgent: parentAgent,
|
||||||
|
})
|
||||||
|
|
||||||
|
const task = await manager.launch({
|
||||||
|
description: args.description,
|
||||||
|
prompt: args.prompt,
|
||||||
|
agent: args.subagent_type,
|
||||||
|
parentSessionID: toolContext.sessionID,
|
||||||
|
parentMessageID: toolContext.messageID,
|
||||||
|
parentAgent,
|
||||||
|
})
|
||||||
|
|
||||||
|
const WAIT_FOR_SESSION_INTERVAL_MS = 50
|
||||||
|
const WAIT_FOR_SESSION_TIMEOUT_MS = 30000
|
||||||
|
const waitStart = Date.now()
|
||||||
|
let sessionId = task.sessionID
|
||||||
|
while (!sessionId && Date.now() - waitStart < WAIT_FOR_SESSION_TIMEOUT_MS) {
|
||||||
|
if (toolContext.abort?.aborted) {
|
||||||
|
return `Task aborted while waiting for session to start.\n\nTask ID: ${task.id}`
|
||||||
|
}
|
||||||
|
const updated = manager.getTask(task.id)
|
||||||
|
if (updated?.status === "error" || updated?.status === "cancelled") {
|
||||||
|
return `Task failed to start (status: ${updated.status}).\n\nTask ID: ${task.id}`
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, WAIT_FOR_SESSION_INTERVAL_MS))
|
||||||
|
sessionId = manager.getTask(task.id)?.sessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
await toolContext.metadata?.({
|
||||||
|
title: args.description,
|
||||||
|
metadata: { sessionId: sessionId ?? "pending" },
|
||||||
|
})
|
||||||
|
|
||||||
|
return `Background agent task launched successfully.
|
||||||
|
|
||||||
|
Task ID: ${task.id}
|
||||||
|
Session ID: ${sessionId ?? "pending"}
|
||||||
|
Description: ${task.description}
|
||||||
|
Agent: ${task.agent} (subagent)
|
||||||
|
Status: ${task.status}
|
||||||
|
|
||||||
|
The system will notify you when the task completes.
|
||||||
|
Use \`background_output\` tool with task_id="${task.id}" to check progress:
|
||||||
|
- block=false (default): Check status immediately - returns full status info
|
||||||
|
- block=true: Wait for completion (rarely needed since system notifies)`
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
return `Failed to launch background agent task: ${message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/tools/call-omo-agent/completion-poller.ts
Normal file
67
src/tools/call-omo-agent/completion-poller.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
export async function waitForCompletion(
|
||||||
|
sessionID: string,
|
||||||
|
toolContext: {
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
agent: string
|
||||||
|
abort: AbortSignal
|
||||||
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||||
|
},
|
||||||
|
ctx: PluginInput
|
||||||
|
): Promise<void> {
|
||||||
|
log(`[call_omo_agent] Polling for completion...`)
|
||||||
|
|
||||||
|
// Poll for session completion
|
||||||
|
const POLL_INTERVAL_MS = 500
|
||||||
|
const MAX_POLL_TIME_MS = 5 * 60 * 1000 // 5 minutes max
|
||||||
|
const pollStart = Date.now()
|
||||||
|
let lastMsgCount = 0
|
||||||
|
let stablePolls = 0
|
||||||
|
const STABILITY_REQUIRED = 3
|
||||||
|
|
||||||
|
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
||||||
|
// Check if aborted
|
||||||
|
if (toolContext.abort?.aborted) {
|
||||||
|
log(`[call_omo_agent] Aborted by user`)
|
||||||
|
throw new Error("Task aborted.")
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
||||||
|
|
||||||
|
// Check session status
|
||||||
|
const statusResult = await ctx.client.session.status()
|
||||||
|
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
||||||
|
const sessionStatus = allStatuses[sessionID]
|
||||||
|
|
||||||
|
// If session is actively running, reset stability counter
|
||||||
|
if (sessionStatus && sessionStatus.type !== "idle") {
|
||||||
|
stablePolls = 0
|
||||||
|
lastMsgCount = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is idle - check message stability
|
||||||
|
const messagesCheck = await ctx.client.session.messages({ path: { id: sessionID } })
|
||||||
|
const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>
|
||||||
|
const currentMsgCount = msgs.length
|
||||||
|
|
||||||
|
if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
|
||||||
|
stablePolls++
|
||||||
|
if (stablePolls >= STABILITY_REQUIRED) {
|
||||||
|
log(`[call_omo_agent] Session complete, ${currentMsgCount} messages`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stablePolls = 0
|
||||||
|
lastMsgCount = currentMsgCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
|
||||||
|
log(`[call_omo_agent] Timeout reached`)
|
||||||
|
throw new Error("Agent task timed out after 5 minutes.")
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/tools/call-omo-agent/message-dir.ts
Normal file
18
src/tools/call-omo-agent/message-dir.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { existsSync, readdirSync } from "node:fs"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||||
|
|
||||||
|
export function getMessageDir(sessionID: string): string | null {
|
||||||
|
if (!sessionID.startsWith("ses_")) return null
|
||||||
|
if (!existsSync(MESSAGE_STORAGE)) return null
|
||||||
|
|
||||||
|
const directPath = join(MESSAGE_STORAGE, sessionID)
|
||||||
|
if (existsSync(directPath)) return directPath
|
||||||
|
|
||||||
|
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
||||||
|
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
||||||
|
if (existsSync(sessionPath)) return sessionPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
84
src/tools/call-omo-agent/message-processor.ts
Normal file
84
src/tools/call-omo-agent/message-processor.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
import { consumeNewMessages } from "../../shared/session-cursor"
|
||||||
|
|
||||||
|
export async function processMessages(
|
||||||
|
sessionID: string,
|
||||||
|
ctx: PluginInput
|
||||||
|
): Promise<string> {
|
||||||
|
const messagesResult = await ctx.client.session.messages({
|
||||||
|
path: { id: sessionID },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (messagesResult.error) {
|
||||||
|
log(`[call_omo_agent] Messages error:`, messagesResult.error)
|
||||||
|
throw new Error(`Failed to get messages: ${messagesResult.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = messagesResult.data
|
||||||
|
log(`[call_omo_agent] Got ${messages.length} messages`)
|
||||||
|
|
||||||
|
// Include both assistant messages AND tool messages
|
||||||
|
// Tool results (grep, glob, bash output) come from role "tool"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const relevantMessages = messages.filter(
|
||||||
|
(m: any) => m.info?.role === "assistant" || m.info?.role === "tool"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relevantMessages.length === 0) {
|
||||||
|
log(`[call_omo_agent] No assistant or tool messages found`)
|
||||||
|
log(`[call_omo_agent] All messages:`, JSON.stringify(messages, null, 2))
|
||||||
|
throw new Error("No assistant or tool response found")
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[call_omo_agent] Found ${relevantMessages.length} relevant messages`)
|
||||||
|
|
||||||
|
// Sort by time ascending (oldest first) to process messages in order
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const sortedMessages = [...relevantMessages].sort((a: any, b: any) => {
|
||||||
|
const timeA = a.info?.time?.created ?? 0
|
||||||
|
const timeB = b.info?.time?.created ?? 0
|
||||||
|
return timeA - timeB
|
||||||
|
})
|
||||||
|
|
||||||
|
const newMessages = consumeNewMessages(sessionID, sortedMessages)
|
||||||
|
|
||||||
|
if (newMessages.length === 0) {
|
||||||
|
return "No new output since last check."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract content from ALL messages, not just the last one
|
||||||
|
// Tool results may be in earlier messages while the final message is empty
|
||||||
|
const extractedContent: string[] = []
|
||||||
|
|
||||||
|
for (const message of newMessages) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
for (const part of (message as any).parts ?? []) {
|
||||||
|
// Handle both "text" and "reasoning" parts (thinking models use "reasoning")
|
||||||
|
if ((part.type === "text" || part.type === "reasoning") && part.text) {
|
||||||
|
extractedContent.push(part.text)
|
||||||
|
} else if (part.type === "tool_result") {
|
||||||
|
// Tool results contain the actual output from tool calls
|
||||||
|
const toolResult = part as { content?: string | Array<{ type: string; text?: string }> }
|
||||||
|
if (typeof toolResult.content === "string" && toolResult.content) {
|
||||||
|
extractedContent.push(toolResult.content)
|
||||||
|
} else if (Array.isArray(toolResult.content)) {
|
||||||
|
// Handle array of content blocks
|
||||||
|
for (const block of toolResult.content) {
|
||||||
|
if ((block.type === "text" || block.type === "reasoning") && block.text) {
|
||||||
|
extractedContent.push(block.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = extractedContent
|
||||||
|
.filter((text) => text.length > 0)
|
||||||
|
.join("\n\n")
|
||||||
|
|
||||||
|
log(`[call_omo_agent] Got response, length: ${responseText.length}`)
|
||||||
|
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
70
src/tools/call-omo-agent/session-creator.ts
Normal file
70
src/tools/call-omo-agent/session-creator.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
export async function createOrGetSession(
|
||||||
|
args: CallOmoAgentArgs,
|
||||||
|
toolContext: {
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
agent: string
|
||||||
|
abort: AbortSignal
|
||||||
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||||
|
},
|
||||||
|
ctx: PluginInput
|
||||||
|
): Promise<{ sessionID: string; isNew: boolean }> {
|
||||||
|
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)
|
||||||
|
throw new Error(`Failed to get existing session: ${sessionResult.error}`)
|
||||||
|
}
|
||||||
|
return { sessionID: args.session_id, isNew: false }
|
||||||
|
} else {
|
||||||
|
log(`[call_omo_agent] Creating new session with parent: ${toolContext.sessionID}`)
|
||||||
|
const parentSession = await ctx.client.session.get({
|
||||||
|
path: { id: toolContext.sessionID },
|
||||||
|
}).catch((err) => {
|
||||||
|
log(`[call_omo_agent] Failed to get parent session:`, err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
log(`[call_omo_agent] Parent session dir: ${parentSession?.data?.directory}, fallback: ${ctx.directory}`)
|
||||||
|
const parentDirectory = parentSession?.data?.directory ?? ctx.directory
|
||||||
|
|
||||||
|
const createResult = await ctx.client.session.create({
|
||||||
|
body: {
|
||||||
|
parentID: toolContext.sessionID,
|
||||||
|
title: `${args.description} (@${args.subagent_type} subagent)`,
|
||||||
|
permission: [
|
||||||
|
{ permission: "question", action: "deny" as const, pattern: "*" },
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
query: {
|
||||||
|
directory: parentDirectory,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createResult.error) {
|
||||||
|
log(`[call_omo_agent] Session create error:`, createResult.error)
|
||||||
|
const errorStr = String(createResult.error)
|
||||||
|
if (errorStr.toLowerCase().includes("unauthorized")) {
|
||||||
|
throw new Error(`Failed to create session (Unauthorized). This may be due to:
|
||||||
|
1. OAuth token restrictions (e.g., Claude Code credentials are restricted to Claude Code only)
|
||||||
|
2. Provider authentication issues
|
||||||
|
3. Session permission inheritance problems
|
||||||
|
|
||||||
|
Try using a different provider or API key authentication.
|
||||||
|
|
||||||
|
Original error: ${createResult.error}`)
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to create session: ${createResult.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionID = createResult.data.id
|
||||||
|
log(`[call_omo_agent] Created session: ${sessionID}`)
|
||||||
|
return { sessionID, isNew: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/tools/call-omo-agent/sync-executor.ts
Normal file
59
src/tools/call-omo-agent/sync-executor.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
import { getAgentToolRestrictions } from "../../shared"
|
||||||
|
import { createOrGetSession } from "./session-creator"
|
||||||
|
import { waitForCompletion } from "./completion-poller"
|
||||||
|
import { processMessages } from "./message-processor"
|
||||||
|
|
||||||
|
export async function executeSync(
|
||||||
|
args: CallOmoAgentArgs,
|
||||||
|
toolContext: {
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
agent: string
|
||||||
|
abort: AbortSignal
|
||||||
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||||
|
},
|
||||||
|
ctx: PluginInput
|
||||||
|
): Promise<string> {
|
||||||
|
const { sessionID } = await createOrGetSession(args, toolContext, ctx)
|
||||||
|
|
||||||
|
await toolContext.metadata?.({
|
||||||
|
title: args.description,
|
||||||
|
metadata: { sessionId: sessionID },
|
||||||
|
})
|
||||||
|
|
||||||
|
log(`[call_omo_agent] Sending prompt to session ${sessionID}`)
|
||||||
|
log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await (ctx.client.session as any).promptAsync({
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: {
|
||||||
|
agent: args.subagent_type,
|
||||||
|
tools: {
|
||||||
|
...getAgentToolRestrictions(args.subagent_type),
|
||||||
|
task: false,
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
log(`[call_omo_agent] Prompt error:`, errorMessage)
|
||||||
|
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
||||||
|
return `Error: Agent "${args.subagent_type}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||||
|
}
|
||||||
|
return `Error: Failed to send prompt: ${errorMessage}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForCompletion(sessionID, toolContext, ctx)
|
||||||
|
|
||||||
|
const responseText = await processMessages(sessionID, ctx)
|
||||||
|
|
||||||
|
const output =
|
||||||
|
responseText + "\n\n" + ["<task_metadata>", `session_id: ${sessionID}`, "</task_metadata>"].join("\n")
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
@ -1,36 +1,10 @@
|
|||||||
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
||||||
import { existsSync, readdirSync } from "node:fs"
|
|
||||||
import { join } from "node:path"
|
|
||||||
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
||||||
import type { CallOmoAgentArgs } from "./types"
|
import type { CallOmoAgentArgs, ToolContextWithMetadata } from "./types"
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
import { log, getAgentToolRestrictions } from "../../shared"
|
import { log } from "../../shared"
|
||||||
import { consumeNewMessages } from "../../shared/session-cursor"
|
import { executeBackground } from "./background-executor"
|
||||||
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
import { executeSync } from "./sync-executor"
|
||||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
|
||||||
|
|
||||||
function getMessageDir(sessionID: string): string | null {
|
|
||||||
if (!sessionID.startsWith("ses_")) return null
|
|
||||||
if (!existsSync(MESSAGE_STORAGE)) return null
|
|
||||||
|
|
||||||
const directPath = join(MESSAGE_STORAGE, sessionID)
|
|
||||||
if (existsSync(directPath)) return directPath
|
|
||||||
|
|
||||||
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
|
||||||
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
|
||||||
if (existsSync(sessionPath)) return sessionPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolContextWithMetadata = {
|
|
||||||
sessionID: string
|
|
||||||
messageID: string
|
|
||||||
agent: string
|
|
||||||
abort: AbortSignal
|
|
||||||
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCallOmoAgent(
|
export function createCallOmoAgent(
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
@ -79,296 +53,3 @@ export function createCallOmoAgent(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeBackground(
|
|
||||||
args: CallOmoAgentArgs,
|
|
||||||
toolContext: ToolContextWithMetadata,
|
|
||||||
manager: BackgroundManager
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const messageDir = getMessageDir(toolContext.sessionID)
|
|
||||||
const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
||||||
const firstMessageAgent = messageDir ? findFirstMessageWithAgent(messageDir) : null
|
|
||||||
const sessionAgent = getSessionAgent(toolContext.sessionID)
|
|
||||||
const parentAgent = toolContext.agent ?? sessionAgent ?? firstMessageAgent ?? prevMessage?.agent
|
|
||||||
|
|
||||||
log("[call_omo_agent] parentAgent resolution", {
|
|
||||||
sessionID: toolContext.sessionID,
|
|
||||||
messageDir,
|
|
||||||
ctxAgent: toolContext.agent,
|
|
||||||
sessionAgent,
|
|
||||||
firstMessageAgent,
|
|
||||||
prevMessageAgent: prevMessage?.agent,
|
|
||||||
resolvedParentAgent: parentAgent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const task = await manager.launch({
|
|
||||||
description: args.description,
|
|
||||||
prompt: args.prompt,
|
|
||||||
agent: args.subagent_type,
|
|
||||||
parentSessionID: toolContext.sessionID,
|
|
||||||
parentMessageID: toolContext.messageID,
|
|
||||||
parentAgent,
|
|
||||||
})
|
|
||||||
|
|
||||||
const WAIT_FOR_SESSION_INTERVAL_MS = 50
|
|
||||||
const WAIT_FOR_SESSION_TIMEOUT_MS = 30000
|
|
||||||
const waitStart = Date.now()
|
|
||||||
let sessionId = task.sessionID
|
|
||||||
while (!sessionId && Date.now() - waitStart < WAIT_FOR_SESSION_TIMEOUT_MS) {
|
|
||||||
if (toolContext.abort?.aborted) {
|
|
||||||
return `Task aborted while waiting for session to start.\n\nTask ID: ${task.id}`
|
|
||||||
}
|
|
||||||
const updated = manager.getTask(task.id)
|
|
||||||
if (updated?.status === "error" || updated?.status === "cancelled") {
|
|
||||||
return `Task failed to start (status: ${updated.status}).\n\nTask ID: ${task.id}`
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, WAIT_FOR_SESSION_INTERVAL_MS))
|
|
||||||
sessionId = manager.getTask(task.id)?.sessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
await toolContext.metadata?.({
|
|
||||||
title: args.description,
|
|
||||||
metadata: { sessionId: sessionId ?? "pending" },
|
|
||||||
})
|
|
||||||
|
|
||||||
return `Background agent task launched successfully.
|
|
||||||
|
|
||||||
Task ID: ${task.id}
|
|
||||||
Session ID: ${sessionId ?? "pending"}
|
|
||||||
Description: ${task.description}
|
|
||||||
Agent: ${task.agent} (subagent)
|
|
||||||
Status: ${task.status}
|
|
||||||
|
|
||||||
The system will notify you when the task completes.
|
|
||||||
Use \`background_output\` tool with task_id="${task.id}" to check progress:
|
|
||||||
- block=false (default): Check status immediately - returns full status info
|
|
||||||
- block=true: Wait for completion (rarely needed since system notifies)`
|
|
||||||
} 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: ToolContextWithMetadata,
|
|
||||||
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 parentSession = await ctx.client.session.get({
|
|
||||||
path: { id: toolContext.sessionID },
|
|
||||||
}).catch((err) => {
|
|
||||||
log(`[call_omo_agent] Failed to get parent session:`, err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
log(`[call_omo_agent] Parent session dir: ${parentSession?.data?.directory}, fallback: ${ctx.directory}`)
|
|
||||||
const parentDirectory = parentSession?.data?.directory ?? ctx.directory
|
|
||||||
|
|
||||||
const createResult = await ctx.client.session.create({
|
|
||||||
body: {
|
|
||||||
parentID: toolContext.sessionID,
|
|
||||||
title: `${args.description} (@${args.subagent_type} subagent)`,
|
|
||||||
permission: [
|
|
||||||
{ permission: "question", action: "deny" as const, pattern: "*" },
|
|
||||||
],
|
|
||||||
} as any,
|
|
||||||
query: {
|
|
||||||
directory: parentDirectory,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (createResult.error) {
|
|
||||||
log(`[call_omo_agent] Session create error:`, createResult.error)
|
|
||||||
const errorStr = String(createResult.error)
|
|
||||||
if (errorStr.toLowerCase().includes("unauthorized")) {
|
|
||||||
return `Error: Failed to create session (Unauthorized). This may be due to:
|
|
||||||
1. OAuth token restrictions (e.g., Claude Code credentials are restricted to Claude Code only)
|
|
||||||
2. Provider authentication issues
|
|
||||||
3. Session permission inheritance problems
|
|
||||||
|
|
||||||
Try using a different provider or API key authentication.
|
|
||||||
|
|
||||||
Original error: ${createResult.error}`
|
|
||||||
}
|
|
||||||
return `Error: Failed to create session: ${createResult.error}`
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID = createResult.data.id
|
|
||||||
log(`[call_omo_agent] Created session: ${sessionID}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await toolContext.metadata?.({
|
|
||||||
title: args.description,
|
|
||||||
metadata: { sessionId: sessionID },
|
|
||||||
})
|
|
||||||
|
|
||||||
log(`[call_omo_agent] Sending prompt to session ${sessionID}`)
|
|
||||||
log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100))
|
|
||||||
|
|
||||||
try {
|
|
||||||
await (ctx.client.session as any).promptAsync({
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: {
|
|
||||||
agent: args.subagent_type,
|
|
||||||
tools: {
|
|
||||||
...getAgentToolRestrictions(args.subagent_type),
|
|
||||||
task: false,
|
|
||||||
},
|
|
||||||
parts: [{ type: "text", text: args.prompt }],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
log(`[call_omo_agent] Prompt error:`, errorMessage)
|
|
||||||
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
|
||||||
return `Error: Agent "${args.subagent_type}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
return `Error: Failed to send prompt: ${errorMessage}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`[call_omo_agent] Prompt sent, polling for completion...`)
|
|
||||||
|
|
||||||
// Poll for session completion
|
|
||||||
const POLL_INTERVAL_MS = 500
|
|
||||||
const MAX_POLL_TIME_MS = 5 * 60 * 1000 // 5 minutes max
|
|
||||||
const pollStart = Date.now()
|
|
||||||
let lastMsgCount = 0
|
|
||||||
let stablePolls = 0
|
|
||||||
const STABILITY_REQUIRED = 3
|
|
||||||
|
|
||||||
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
|
||||||
// Check if aborted
|
|
||||||
if (toolContext.abort?.aborted) {
|
|
||||||
log(`[call_omo_agent] Aborted by user`)
|
|
||||||
return `Task aborted.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
||||||
|
|
||||||
// Check session status
|
|
||||||
const statusResult = await ctx.client.session.status()
|
|
||||||
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
|
||||||
const sessionStatus = allStatuses[sessionID]
|
|
||||||
|
|
||||||
// If session is actively running, reset stability counter
|
|
||||||
if (sessionStatus && sessionStatus.type !== "idle") {
|
|
||||||
stablePolls = 0
|
|
||||||
lastMsgCount = 0
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session is idle - check message stability
|
|
||||||
const messagesCheck = await ctx.client.session.messages({ path: { id: sessionID } })
|
|
||||||
const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>
|
|
||||||
const currentMsgCount = msgs.length
|
|
||||||
|
|
||||||
if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
|
|
||||||
stablePolls++
|
|
||||||
if (stablePolls >= STABILITY_REQUIRED) {
|
|
||||||
log(`[call_omo_agent] Session complete, ${currentMsgCount} messages`)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stablePolls = 0
|
|
||||||
lastMsgCount = currentMsgCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
|
|
||||||
log(`[call_omo_agent] Timeout reached`)
|
|
||||||
return `Error: Agent task timed out after 5 minutes.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
|
|
||||||
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`)
|
|
||||||
|
|
||||||
// Include both assistant messages AND tool messages
|
|
||||||
// Tool results (grep, glob, bash output) come from role "tool"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const relevantMessages = messages.filter(
|
|
||||||
(m: any) => m.info?.role === "assistant" || m.info?.role === "tool"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (relevantMessages.length === 0) {
|
|
||||||
log(`[call_omo_agent] No assistant or tool messages found`)
|
|
||||||
log(`[call_omo_agent] All messages:`, JSON.stringify(messages, null, 2))
|
|
||||||
return `Error: No assistant or tool response found\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`[call_omo_agent] Found ${relevantMessages.length} relevant messages`)
|
|
||||||
|
|
||||||
// Sort by time ascending (oldest first) to process messages in order
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const sortedMessages = [...relevantMessages].sort((a: any, b: any) => {
|
|
||||||
const timeA = a.info?.time?.created ?? 0
|
|
||||||
const timeB = b.info?.time?.created ?? 0
|
|
||||||
return timeA - timeB
|
|
||||||
})
|
|
||||||
|
|
||||||
const newMessages = consumeNewMessages(sessionID, sortedMessages)
|
|
||||||
|
|
||||||
if (newMessages.length === 0) {
|
|
||||||
return `No new output since last check.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract content from ALL messages, not just the last one
|
|
||||||
// Tool results may be in earlier messages while the final message is empty
|
|
||||||
const extractedContent: string[] = []
|
|
||||||
|
|
||||||
for (const message of newMessages) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
for (const part of (message as any).parts ?? []) {
|
|
||||||
// Handle both "text" and "reasoning" parts (thinking models use "reasoning")
|
|
||||||
if ((part.type === "text" || part.type === "reasoning") && part.text) {
|
|
||||||
extractedContent.push(part.text)
|
|
||||||
} else if (part.type === "tool_result") {
|
|
||||||
// Tool results contain the actual output from tool calls
|
|
||||||
const toolResult = part as { content?: string | Array<{ type: string; text?: string }> }
|
|
||||||
if (typeof toolResult.content === "string" && toolResult.content) {
|
|
||||||
extractedContent.push(toolResult.content)
|
|
||||||
} else if (Array.isArray(toolResult.content)) {
|
|
||||||
// Handle array of content blocks
|
|
||||||
for (const block of toolResult.content) {
|
|
||||||
if ((block.type === "text" || block.type === "reasoning") && block.text) {
|
|
||||||
extractedContent.push(block.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseText = extractedContent
|
|
||||||
.filter((text) => text.length > 0)
|
|
||||||
.join("\n\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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,3 +25,10 @@ export interface CallOmoAgentSyncResult {
|
|||||||
}
|
}
|
||||||
output: string
|
output: string
|
||||||
}
|
}
|
||||||
|
export type ToolContextWithMetadata = {
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
agent: string
|
||||||
|
abort: AbortSignal
|
||||||
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user