From 6bb9a3b7bca4cc5e0b14fe1fd23ba7651351777d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 17:00:26 +0900 Subject: [PATCH] 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 --- .../call-omo-agent/background-executor.ts | 83 +++++ src/tools/call-omo-agent/completion-poller.ts | 67 ++++ src/tools/call-omo-agent/message-dir.ts | 18 + src/tools/call-omo-agent/message-processor.ts | 84 +++++ src/tools/call-omo-agent/session-creator.ts | 70 ++++ src/tools/call-omo-agent/sync-executor.ts | 59 ++++ src/tools/call-omo-agent/tools.ts | 327 +----------------- src/tools/call-omo-agent/types.ts | 7 + 8 files changed, 392 insertions(+), 323 deletions(-) create mode 100644 src/tools/call-omo-agent/background-executor.ts create mode 100644 src/tools/call-omo-agent/completion-poller.ts create mode 100644 src/tools/call-omo-agent/message-dir.ts create mode 100644 src/tools/call-omo-agent/message-processor.ts create mode 100644 src/tools/call-omo-agent/session-creator.ts create mode 100644 src/tools/call-omo-agent/sync-executor.ts diff --git a/src/tools/call-omo-agent/background-executor.ts b/src/tools/call-omo-agent/background-executor.ts new file mode 100644 index 000000000..3a838edb8 --- /dev/null +++ b/src/tools/call-omo-agent/background-executor.ts @@ -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 }) => void + }, + manager: BackgroundManager +): Promise { + 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}` + } +} diff --git a/src/tools/call-omo-agent/completion-poller.ts b/src/tools/call-omo-agent/completion-poller.ts new file mode 100644 index 000000000..0ca73e735 --- /dev/null +++ b/src/tools/call-omo-agent/completion-poller.ts @@ -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 }) => void + }, + ctx: PluginInput +): Promise { + 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 + 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 + 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.") + } +} diff --git a/src/tools/call-omo-agent/message-dir.ts b/src/tools/call-omo-agent/message-dir.ts new file mode 100644 index 000000000..01fa68fca --- /dev/null +++ b/src/tools/call-omo-agent/message-dir.ts @@ -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 +} diff --git a/src/tools/call-omo-agent/message-processor.ts b/src/tools/call-omo-agent/message-processor.ts new file mode 100644 index 000000000..6b4eca6c6 --- /dev/null +++ b/src/tools/call-omo-agent/message-processor.ts @@ -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 { + 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 +} diff --git a/src/tools/call-omo-agent/session-creator.ts b/src/tools/call-omo-agent/session-creator.ts new file mode 100644 index 000000000..64aa664d5 --- /dev/null +++ b/src/tools/call-omo-agent/session-creator.ts @@ -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 }) => 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 } + } +} diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts new file mode 100644 index 000000000..f64310fbd --- /dev/null +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -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 }) => void + }, + ctx: PluginInput +): Promise { + 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\nsession_id: ${sessionID}\n` + } + return `Error: Failed to send prompt: ${errorMessage}\n\n\nsession_id: ${sessionID}\n` + } + + await waitForCompletion(sessionID, toolContext, ctx) + + const responseText = await processMessages(sessionID, ctx) + + const output = + responseText + "\n\n" + ["", `session_id: ${sessionID}`, ""].join("\n") + + return output +} diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts index 64fa74a40..25c2c8401 100644 --- a/src/tools/call-omo-agent/tools.ts +++ b/src/tools/call-omo-agent/tools.ts @@ -1,36 +1,10 @@ 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 type { CallOmoAgentArgs } from "./types" +import type { CallOmoAgentArgs, ToolContextWithMetadata } from "./types" import type { BackgroundManager } from "../../features/background-agent" -import { log, getAgentToolRestrictions } from "../../shared" -import { consumeNewMessages } from "../../shared/session-cursor" -import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector" -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 }) => void -} +import { log } from "../../shared" +import { executeBackground } from "./background-executor" +import { executeSync } from "./sync-executor" export function createCallOmoAgent( ctx: PluginInput, @@ -79,296 +53,3 @@ export function createCallOmoAgent( }, }) } - -async function executeBackground( - args: CallOmoAgentArgs, - toolContext: ToolContextWithMetadata, - manager: BackgroundManager -): Promise { - 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 { - 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\nsession_id: ${sessionID}\n` - } - return `Error: Failed to send prompt: ${errorMessage}\n\n\nsession_id: ${sessionID}\n` - } - - 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\nsession_id: ${sessionID}\n` - } - - 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 - 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 - 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\nsession_id: ${sessionID}\n` - } - - 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\nsession_id: ${sessionID}\n` - } - - 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\nsession_id: ${sessionID}\n` - } - - // 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" + ["", `session_id: ${sessionID}`, ""].join("\n") - - return output -} diff --git a/src/tools/call-omo-agent/types.ts b/src/tools/call-omo-agent/types.ts index 814e355cd..17bdd4e07 100644 --- a/src/tools/call-omo-agent/types.ts +++ b/src/tools/call-omo-agent/types.ts @@ -25,3 +25,10 @@ export interface CallOmoAgentSyncResult { } output: string } +export type ToolContextWithMetadata = { + sessionID: string + messageID: string + agent: string + abort: AbortSignal + metadata?: (input: { title?: string; metadata?: Record }) => void +}