|
@@ -1,6 +1,6 @@
|
|
|
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
|
|
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
|
|
|
import type { BackgroundTaskManager } from "../features";
|
|
import type { BackgroundTaskManager } from "../features";
|
|
|
-import { getSubagentNames } from "../agents";
|
|
|
|
|
|
|
+import { SUBAGENT_NAMES } from "../config";
|
|
|
import {
|
|
import {
|
|
|
POLL_INTERVAL_MS,
|
|
POLL_INTERVAL_MS,
|
|
|
MAX_POLL_TIME_MS,
|
|
MAX_POLL_TIME_MS,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
import type { TmuxConfig } from "../config/schema";
|
|
import type { TmuxConfig } from "../config/schema";
|
|
|
import type { PluginConfig } from "../config";
|
|
import type { PluginConfig } from "../config";
|
|
|
import { applyAgentVariant, resolveAgentVariant } from "../utils";
|
|
import { applyAgentVariant, resolveAgentVariant } from "../utils";
|
|
|
-import { log } from "../shared/logger";
|
|
|
|
|
|
|
+import { log } from "../utils/logger";
|
|
|
|
|
|
|
|
const z = tool.schema;
|
|
const z = tool.schema;
|
|
|
|
|
|
|
@@ -21,14 +21,32 @@ type ToolContext = {
|
|
|
abort: AbortSignal;
|
|
abort: AbortSignal;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+interface SessionMessage {
|
|
|
|
|
+ info?: { role: string };
|
|
|
|
|
+ parts?: Array<{ type: string; text?: string }>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SessionStatus {
|
|
|
|
|
+ type: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Creates background task management tools for the plugin.
|
|
|
|
|
+ * @param ctx - Plugin input context
|
|
|
|
|
+ * @param manager - Background task manager for launching and tracking tasks
|
|
|
|
|
+ * @param tmuxConfig - Optional tmux configuration for session management
|
|
|
|
|
+ * @param pluginConfig - Optional plugin configuration for agent variants
|
|
|
|
|
+ * @returns Object containing background_task, background_output, and background_cancel tools
|
|
|
|
|
+ */
|
|
|
export function createBackgroundTools(
|
|
export function createBackgroundTools(
|
|
|
ctx: PluginInput,
|
|
ctx: PluginInput,
|
|
|
manager: BackgroundTaskManager,
|
|
manager: BackgroundTaskManager,
|
|
|
tmuxConfig?: TmuxConfig,
|
|
tmuxConfig?: TmuxConfig,
|
|
|
pluginConfig?: PluginConfig
|
|
pluginConfig?: PluginConfig
|
|
|
): Record<string, ToolDefinition> {
|
|
): Record<string, ToolDefinition> {
|
|
|
- const agentNames = getSubagentNames().join(", ");
|
|
|
|
|
|
|
+ const agentNames = SUBAGENT_NAMES.join(", ");
|
|
|
|
|
|
|
|
|
|
+ // Tool for launching agent tasks (async or sync mode)
|
|
|
const background_task = tool({
|
|
const background_task = tool({
|
|
|
description: `Run agent task. Use sync=true to wait for result, sync=false (default) to run in background.
|
|
description: `Run agent task. Use sync=true to wait for result, sync=false (default) to run in background.
|
|
|
|
|
|
|
@@ -44,21 +62,35 @@ Sync mode blocks until completion and returns the result directly.`,
|
|
|
session_id: z.string().optional().describe("Continue existing session (sync mode only)"),
|
|
session_id: z.string().optional().describe("Continue existing session (sync mode only)"),
|
|
|
},
|
|
},
|
|
|
async execute(args, toolContext) {
|
|
async execute(args, toolContext) {
|
|
|
- const tctx = toolContext as ToolContext;
|
|
|
|
|
|
|
+ // Validate tool context has required sessionID
|
|
|
|
|
+ if (!toolContext || typeof toolContext !== "object" || !("sessionID" in toolContext)) {
|
|
|
|
|
+ throw new Error("Invalid toolContext: missing sessionID");
|
|
|
|
|
+ }
|
|
|
const agent = String(args.agent);
|
|
const agent = String(args.agent);
|
|
|
const prompt = String(args.prompt);
|
|
const prompt = String(args.prompt);
|
|
|
const description = String(args.description);
|
|
const description = String(args.description);
|
|
|
const isSync = args.sync === true;
|
|
const isSync = args.sync === true;
|
|
|
|
|
|
|
|
|
|
+ // Sync mode: execute task and wait for completion
|
|
|
if (isSync) {
|
|
if (isSync) {
|
|
|
- return await executeSync(description, prompt, agent, tctx, ctx, tmuxConfig, pluginConfig, args.session_id as string | undefined);
|
|
|
|
|
|
|
+ return await executeSync(
|
|
|
|
|
+ description,
|
|
|
|
|
+ prompt,
|
|
|
|
|
+ agent,
|
|
|
|
|
+ toolContext as ToolContext,
|
|
|
|
|
+ ctx,
|
|
|
|
|
+ tmuxConfig,
|
|
|
|
|
+ pluginConfig,
|
|
|
|
|
+ args.session_id as string | undefined
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Async mode: launch task and return immediately with task ID
|
|
|
const task = await manager.launch({
|
|
const task = await manager.launch({
|
|
|
agent,
|
|
agent,
|
|
|
prompt,
|
|
prompt,
|
|
|
description,
|
|
description,
|
|
|
- parentSessionId: tctx.sessionID,
|
|
|
|
|
|
|
+ parentSessionId: (toolContext as ToolContext).sessionID,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return `Background task launched.
|
|
return `Background task launched.
|
|
@@ -71,6 +103,7 @@ Use \`background_output\` with task_id="${task.id}" to get results.`;
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Tool for retrieving output from background tasks
|
|
|
const background_output = tool({
|
|
const background_output = tool({
|
|
|
description: "Get output from background task.",
|
|
description: "Get output from background task.",
|
|
|
args: {
|
|
args: {
|
|
@@ -78,30 +111,33 @@ Use \`background_output\` with task_id="${task.id}" to get results.`;
|
|
|
block: z.boolean().optional().describe("Wait for completion (default: false)"),
|
|
block: z.boolean().optional().describe("Wait for completion (default: false)"),
|
|
|
timeout: z.number().optional().describe("Timeout in ms (default: 120000)"),
|
|
timeout: z.number().optional().describe("Timeout in ms (default: 120000)"),
|
|
|
},
|
|
},
|
|
|
- async execute(args) {
|
|
|
|
|
|
|
+async execute(args) {
|
|
|
const taskId = String(args.task_id);
|
|
const taskId = String(args.task_id);
|
|
|
const block = args.block === true;
|
|
const block = args.block === true;
|
|
|
const timeout = typeof args.timeout === "number" ? args.timeout : DEFAULT_TIMEOUT_MS;
|
|
const timeout = typeof args.timeout === "number" ? args.timeout : DEFAULT_TIMEOUT_MS;
|
|
|
|
|
|
|
|
|
|
+ // Retrieve task result (optionally blocking until completion)
|
|
|
const task = await manager.getResult(taskId, block, timeout);
|
|
const task = await manager.getResult(taskId, block, timeout);
|
|
|
if (!task) {
|
|
if (!task) {
|
|
|
return `Task not found: ${taskId}`;
|
|
return `Task not found: ${taskId}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Calculate task duration
|
|
|
const duration = task.completedAt
|
|
const duration = task.completedAt
|
|
|
? `${Math.floor((task.completedAt.getTime() - task.startedAt.getTime()) / 1000)}s`
|
|
? `${Math.floor((task.completedAt.getTime() - task.startedAt.getTime()) / 1000)}s`
|
|
|
: "running";
|
|
: "running";
|
|
|
|
|
|
|
|
let output = `Task: ${task.id}
|
|
let output = `Task: ${task.id}
|
|
|
-Description: ${task.description}
|
|
|
|
|
-Status: ${task.status}
|
|
|
|
|
-Duration: ${duration}
|
|
|
|
|
|
|
+ Description: ${task.description}
|
|
|
|
|
+ Status: ${task.status}
|
|
|
|
|
+ Duration: ${duration}
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+ ---
|
|
|
|
|
|
|
|
-`;
|
|
|
|
|
|
|
+ `;
|
|
|
|
|
|
|
|
- if (task.status === "completed" && task.result) {
|
|
|
|
|
|
|
+ // Include task result or error based on status
|
|
|
|
|
+ if (task.status === "completed" && task.result != null) {
|
|
|
output += task.result;
|
|
output += task.result;
|
|
|
} else if (task.status === "failed") {
|
|
} else if (task.status === "failed") {
|
|
|
output += `Error: ${task.error}`;
|
|
output += `Error: ${task.error}`;
|
|
@@ -113,6 +149,7 @@ Duration: ${duration}
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Tool for canceling running background tasks
|
|
|
const background_cancel = tool({
|
|
const background_cancel = tool({
|
|
|
description: "Cancel running background task(s). Use all=true to cancel all.",
|
|
description: "Cancel running background task(s). Use all=true to cancel all.",
|
|
|
args: {
|
|
args: {
|
|
@@ -120,11 +157,13 @@ Duration: ${duration}
|
|
|
all: z.boolean().optional().describe("Cancel all running tasks"),
|
|
all: z.boolean().optional().describe("Cancel all running tasks"),
|
|
|
},
|
|
},
|
|
|
async execute(args) {
|
|
async execute(args) {
|
|
|
|
|
+ // Cancel all running tasks if requested
|
|
|
if (args.all === true) {
|
|
if (args.all === true) {
|
|
|
const count = manager.cancel();
|
|
const count = manager.cancel();
|
|
|
return `Cancelled ${count} running task(s).`;
|
|
return `Cancelled ${count} running task(s).`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Cancel specific task if task_id provided
|
|
|
if (typeof args.task_id === "string") {
|
|
if (typeof args.task_id === "string") {
|
|
|
const count = manager.cancel(args.task_id);
|
|
const count = manager.cancel(args.task_id);
|
|
|
return count > 0 ? `Cancelled task ${args.task_id}.` : `Task ${args.task_id} not found or not running.`;
|
|
return count > 0 ? `Cancelled task ${args.task_id}.` : `Task ${args.task_id} not found or not running.`;
|
|
@@ -137,6 +176,19 @@ Duration: ${duration}
|
|
|
return { background_task, background_output, background_cancel };
|
|
return { background_task, background_output, background_cancel };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Executes a task synchronously by creating/resuming a session, sending a prompt,
|
|
|
|
|
+ * polling for completion, and extracting the response.
|
|
|
|
|
+ * @param description - Short description of the task
|
|
|
|
|
+ * @param prompt - The task prompt for the agent
|
|
|
|
|
+ * @param agent - The agent to use for the task
|
|
|
|
|
+ * @param toolContext - Tool context containing session ID and abort signal
|
|
|
|
|
+ * @param ctx - Plugin input context
|
|
|
|
|
+ * @param tmuxConfig - Optional tmux configuration for session management
|
|
|
|
|
+ * @param pluginConfig - Optional plugin configuration for agent variants
|
|
|
|
|
+ * @param existingSessionId - Optional existing session ID to resume
|
|
|
|
|
+ * @returns The agent's response text with task metadata
|
|
|
|
|
+ */
|
|
|
async function executeSync(
|
|
async function executeSync(
|
|
|
description: string,
|
|
description: string,
|
|
|
prompt: string,
|
|
prompt: string,
|
|
@@ -147,40 +199,129 @@ async function executeSync(
|
|
|
pluginConfig?: PluginConfig,
|
|
pluginConfig?: PluginConfig,
|
|
|
existingSessionId?: string
|
|
existingSessionId?: string
|
|
|
): Promise<string> {
|
|
): Promise<string> {
|
|
|
- let sessionID: string;
|
|
|
|
|
|
|
+ // Resolve or create session for the task
|
|
|
|
|
+ const { sessionID, error: sessionError } = await resolveSessionId(
|
|
|
|
|
+ ctx,
|
|
|
|
|
+ toolContext,
|
|
|
|
|
+ description,
|
|
|
|
|
+ agent,
|
|
|
|
|
+ tmuxConfig,
|
|
|
|
|
+ existingSessionId
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (sessionError) {
|
|
|
|
|
+ return `Error: ${sessionError}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Disable recursive delegation tools to prevent infinite loops
|
|
|
|
|
+ log(`[background-sync] launching sync task for agent="${agent}"`, { description });
|
|
|
|
|
+
|
|
|
|
|
+ // Send prompt to the session
|
|
|
|
|
+ const { error: promptError } = await sendPrompt(ctx, sessionID, prompt, agent, pluginConfig);
|
|
|
|
|
+ if (promptError) {
|
|
|
|
|
+ return withTaskMetadata(`Error: ${promptError}`, sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Poll session until completion, abort, or timeout
|
|
|
|
|
+ const pollResult = await pollSession(ctx, sessionID, toolContext.abort);
|
|
|
|
|
+ if (pollResult.aborted) {
|
|
|
|
|
+ return withTaskMetadata("Task aborted.", sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pollResult.timeout) {
|
|
|
|
|
+ const minutes = Math.floor(MAX_POLL_TIME_MS / 60000);
|
|
|
|
|
+ return withTaskMetadata(`Error: Agent timed out after ${minutes} minutes.`, sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pollResult.error) {
|
|
|
|
|
+ return withTaskMetadata(`Error: ${pollResult.error}`, sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Retrieve and extract the agent's response
|
|
|
|
|
+ const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
|
|
|
|
|
+ if (messagesResult.error) {
|
|
|
|
|
+ return `Error: Failed to get messages: ${messagesResult.error}`;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ const messages = messagesResult.data as SessionMessage[];
|
|
|
|
|
+ const responseText = extractResponseText(messages);
|
|
|
|
|
+
|
|
|
|
|
+ if (!responseText) {
|
|
|
|
|
+ return withTaskMetadata("Error: No response from agent.", sessionID);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Pane closing is handled by TmuxSessionManager via polling
|
|
|
|
|
+ return formatResponse(responseText, sessionID);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Resolves an existing session or creates a new one.
|
|
|
|
|
+ */
|
|
|
|
|
+export async function resolveSessionId(
|
|
|
|
|
+ ctx: PluginInput,
|
|
|
|
|
+ toolContext: ToolContext,
|
|
|
|
|
+ description: string,
|
|
|
|
|
+ agent: string,
|
|
|
|
|
+ tmuxConfig?: TmuxConfig,
|
|
|
|
|
+ existingSessionId?: string
|
|
|
|
|
+): Promise<{ sessionID: string; error?: string }> {
|
|
|
|
|
+ // If existing session ID provided, validate and return it
|
|
|
if (existingSessionId) {
|
|
if (existingSessionId) {
|
|
|
const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
|
|
const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
|
|
|
if (sessionResult.error) {
|
|
if (sessionResult.error) {
|
|
|
- return `Error: Failed to get session: ${sessionResult.error}`;
|
|
|
|
|
|
|
+ return { sessionID: "", error: `Failed to get session: ${sessionResult.error}` };
|
|
|
}
|
|
}
|
|
|
- sessionID = existingSessionId;
|
|
|
|
|
- } else {
|
|
|
|
|
- const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
|
|
|
|
|
- const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
|
|
|
|
|
-
|
|
|
|
|
- const createResult = await ctx.client.session.create({
|
|
|
|
|
- body: {
|
|
|
|
|
- parentID: toolContext.sessionID,
|
|
|
|
|
- title: `${description} (@${agent})`,
|
|
|
|
|
- },
|
|
|
|
|
- query: { directory: parentDirectory },
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ return { sessionID: existingSessionId };
|
|
|
|
|
+ }
|
|
|
|
|
+ // Otherwise, create a new session
|
|
|
|
|
+ return createSession(ctx, toolContext, description, agent, tmuxConfig);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (createResult.error) {
|
|
|
|
|
- return `Error: Failed to create session: ${createResult.error}`;
|
|
|
|
|
- }
|
|
|
|
|
- sessionID = createResult.data.id;
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Creates a new session with proper configuration.
|
|
|
|
|
+ */
|
|
|
|
|
+export async function createSession(
|
|
|
|
|
+ ctx: PluginInput,
|
|
|
|
|
+ toolContext: ToolContext,
|
|
|
|
|
+ description: string,
|
|
|
|
|
+ agent: string,
|
|
|
|
|
+ tmuxConfig?: TmuxConfig
|
|
|
|
|
+): Promise<{ sessionID: string; error?: string }> {
|
|
|
|
|
+ // Get parent session to inherit directory context
|
|
|
|
|
+ const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
|
|
|
|
|
+ const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
|
|
|
|
|
+
|
|
|
|
|
+ // Create new session with parent relationship
|
|
|
|
|
+ const createResult = await ctx.client.session.create({
|
|
|
|
|
+ body: {
|
|
|
|
|
+ parentID: toolContext.sessionID,
|
|
|
|
|
+ title: `${description} (@${agent})`,
|
|
|
|
|
+ },
|
|
|
|
|
+ query: { directory: parentDirectory },
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // Give TmuxSessionManager time to spawn the pane via event hook
|
|
|
|
|
- // before we send the prompt (so the TUI can receive streaming updates)
|
|
|
|
|
- if (tmuxConfig?.enabled) {
|
|
|
|
|
- await new Promise((r) => setTimeout(r, 500));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (createResult.error) {
|
|
|
|
|
+ return { sessionID: "", error: `Failed to create session: ${createResult.error}` };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Disable recursive delegation tools to prevent infinite loops
|
|
|
|
|
- log(`[background-sync] launching sync task for agent="${agent}"`, { description });
|
|
|
|
|
|
|
+ // Give TmuxSessionManager time to spawn the pane via event hook
|
|
|
|
|
+ // before we send the prompt (so the TUI can receive streaming updates)
|
|
|
|
|
+ if (tmuxConfig?.enabled) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 500));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { sessionID: createResult.data.id };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Sends a prompt to the specified session.
|
|
|
|
|
+ */
|
|
|
|
|
+export async function sendPrompt(
|
|
|
|
|
+ ctx: PluginInput,
|
|
|
|
|
+ sessionID: string,
|
|
|
|
|
+ prompt: string,
|
|
|
|
|
+ agent: string,
|
|
|
|
|
+ pluginConfig?: PluginConfig
|
|
|
|
|
+): Promise<{ error?: string }> {
|
|
|
|
|
+ // Resolve agent variant configuration
|
|
|
const resolvedVariant = resolveAgentVariant(pluginConfig, agent);
|
|
const resolvedVariant = resolveAgentVariant(pluginConfig, agent);
|
|
|
|
|
|
|
|
type PromptBody = {
|
|
type PromptBody = {
|
|
@@ -190,6 +331,7 @@ async function executeSync(
|
|
|
variant?: string;
|
|
variant?: string;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // Build prompt body with recursive tools disabled to prevent infinite loops
|
|
|
const baseBody: PromptBody = {
|
|
const baseBody: PromptBody = {
|
|
|
agent,
|
|
agent,
|
|
|
tools: { background_task: false, task: false },
|
|
tools: { background_task: false, task: false },
|
|
@@ -197,36 +339,45 @@ async function executeSync(
|
|
|
};
|
|
};
|
|
|
const promptBody = applyAgentVariant(resolvedVariant, baseBody);
|
|
const promptBody = applyAgentVariant(resolvedVariant, baseBody);
|
|
|
|
|
|
|
|
|
|
+ // Send prompt to the session
|
|
|
try {
|
|
try {
|
|
|
await ctx.client.session.prompt({
|
|
await ctx.client.session.prompt({
|
|
|
path: { id: sessionID },
|
|
path: { id: sessionID },
|
|
|
body: promptBody,
|
|
body: promptBody,
|
|
|
});
|
|
});
|
|
|
|
|
+ return {};
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- return `Error: Failed to send prompt: ${error instanceof Error ? error.message : String(error)}
|
|
|
|
|
-
|
|
|
|
|
-<task_metadata>
|
|
|
|
|
-session_id: ${sessionID}
|
|
|
|
|
-</task_metadata>`;
|
|
|
|
|
|
|
+ return { error: `Failed to send prompt: ${error instanceof Error ? error.message : String(error)}` };
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Polls the session until it becomes idle and has messages.
|
|
|
|
|
+ */
|
|
|
|
|
+export async function pollSession(
|
|
|
|
|
+ ctx: PluginInput,
|
|
|
|
|
+ sessionID: string,
|
|
|
|
|
+ abortSignal: AbortSignal
|
|
|
|
|
+): Promise<{ error?: string; timeout?: boolean; aborted?: boolean }> {
|
|
|
const pollStart = Date.now();
|
|
const pollStart = Date.now();
|
|
|
let lastMsgCount = 0;
|
|
let lastMsgCount = 0;
|
|
|
let stablePolls = 0;
|
|
let stablePolls = 0;
|
|
|
|
|
|
|
|
|
|
+ // Poll until timeout, abort, or stable message count detected
|
|
|
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
|
while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
|
|
|
- if (toolContext.abort?.aborted) {
|
|
|
|
|
- return `Task aborted.
|
|
|
|
|
-
|
|
|
|
|
-<task_metadata>
|
|
|
|
|
-session_id: ${sessionID}
|
|
|
|
|
-</task_metadata>`;
|
|
|
|
|
|
|
+ // Check for abort signal
|
|
|
|
|
+ if (abortSignal.aborted) {
|
|
|
|
|
+ return { aborted: true };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
|
|
|
|
|
|
|
|
+ // Check session status - if not idle, reset stability counters
|
|
|
const statusResult = await ctx.client.session.status();
|
|
const statusResult = await ctx.client.session.status();
|
|
|
- const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
|
|
|
|
|
|
|
+ if (statusResult.error) {
|
|
|
|
|
+ return { error: `Failed to get session status: ${statusResult.error}` };
|
|
|
|
|
+ }
|
|
|
|
|
+ const allStatuses = (statusResult.data ?? {}) as Record<string, SessionStatus>;
|
|
|
const sessionStatus = allStatuses[sessionID];
|
|
const sessionStatus = allStatuses[sessionID];
|
|
|
|
|
|
|
|
if (sessionStatus && sessionStatus.type !== "idle") {
|
|
if (sessionStatus && sessionStatus.type !== "idle") {
|
|
@@ -235,44 +386,35 @@ session_id: ${sessionID}
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Check message count - if stable for threshold, task is complete
|
|
|
const messagesCheck = await ctx.client.session.messages({ path: { id: sessionID } });
|
|
const messagesCheck = await ctx.client.session.messages({ path: { id: sessionID } });
|
|
|
- const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>;
|
|
|
|
|
|
|
+ if (messagesCheck.error) {
|
|
|
|
|
+ return { error: `Failed to check messages: ${messagesCheck.error}` };
|
|
|
|
|
+ }
|
|
|
|
|
+ const msgs = messagesCheck.data as SessionMessage[];
|
|
|
const currentMsgCount = msgs.length;
|
|
const currentMsgCount = msgs.length;
|
|
|
|
|
|
|
|
if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
|
|
if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
|
|
|
stablePolls++;
|
|
stablePolls++;
|
|
|
- if (stablePolls >= STABLE_POLLS_THRESHOLD) break;
|
|
|
|
|
|
|
+ if (stablePolls >= STABLE_POLLS_THRESHOLD) return {};
|
|
|
} else {
|
|
} else {
|
|
|
stablePolls = 0;
|
|
stablePolls = 0;
|
|
|
lastMsgCount = currentMsgCount;
|
|
lastMsgCount = currentMsgCount;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
|
|
|
|
|
- return `Error: Agent timed out after 5 minutes.
|
|
|
|
|
-
|
|
|
|
|
-<task_metadata>
|
|
|
|
|
-session_id: ${sessionID}
|
|
|
|
|
-</task_metadata>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
|
|
|
|
|
- if (messagesResult.error) {
|
|
|
|
|
- return `Error: Failed to get messages: ${messagesResult.error}`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { timeout: true };
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const messages = messagesResult.data as Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>;
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Extracts the assistant's response text from session messages.
|
|
|
|
|
+ */
|
|
|
|
|
+export function extractResponseText(messages: SessionMessage[]): string {
|
|
|
|
|
+ // Filter for assistant messages only
|
|
|
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
|
-
|
|
|
|
|
- if (assistantMessages.length === 0) {
|
|
|
|
|
- return `Error: No response from agent.
|
|
|
|
|
-
|
|
|
|
|
-<task_metadata>
|
|
|
|
|
-session_id: ${sessionID}
|
|
|
|
|
-</task_metadata>`;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
const extractedContent: string[] = [];
|
|
const extractedContent: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // Extract text and reasoning content from message parts
|
|
|
for (const message of assistantMessages) {
|
|
for (const message of assistantMessages) {
|
|
|
for (const part of message.parts ?? []) {
|
|
for (const part of message.parts ?? []) {
|
|
|
if ((part.type === "text" || part.type === "reasoning") && part.text) {
|
|
if ((part.type === "text" || part.type === "reasoning") && part.text) {
|
|
@@ -281,10 +423,22 @@ session_id: ${sessionID}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const responseText = extractedContent.filter((t) => t.length > 0).join("\n\n");
|
|
|
|
|
|
|
+ // Join non-empty content with double newlines
|
|
|
|
|
+ return extractedContent.filter((t) => t.length > 0).join("\n\n");
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Pane closing is handled by TmuxSessionManager via polling
|
|
|
|
|
- return `${responseText}
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Formats the final response with metadata.
|
|
|
|
|
+ */
|
|
|
|
|
+function formatResponse(responseText: string, sessionID: string): string {
|
|
|
|
|
+ return withTaskMetadata(responseText, sessionID);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Wraps content with task metadata footer.
|
|
|
|
|
+ */
|
|
|
|
|
+function withTaskMetadata(content: string, sessionID: string): string {
|
|
|
|
|
+ return `${content}
|
|
|
|
|
|
|
|
<task_metadata>
|
|
<task_metadata>
|
|
|
session_id: ${sessionID}
|
|
session_id: ${sessionID}
|