mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
feat: add terminate_session and list_sessions tools
- Add terminate_session tool to kill running terminal sessions - Add list_sessions tool to view active terminal sessions - Add listSessions and terminateSession methods to ProcessManager - Create native tool schemas for both tools - Create tool handlers (TerminateSessionTool, ListSessionsTool) - Add routing in presentAssistantMessage.ts - Add parsing support in NativeToolCallParser.ts - Add 8 new tests for ProcessManager methods (23 total tests) These tools complement write_stdin to provide complete terminal session management: - execute_command starts a process (returns session_id if still running) - write_stdin sends input to running processes - list_sessions shows all active sessions - terminate_session kills a running session
This commit is contained in:
parent
3b99b936f8
commit
c9af762114
11 changed files with 612 additions and 1 deletions
|
|
@ -17,6 +17,8 @@ export type ToolGroup = z.infer<typeof toolGroupsSchema>
|
|||
export const toolNames = [
|
||||
"execute_command",
|
||||
"write_stdin",
|
||||
"terminate_session",
|
||||
"list_sessions",
|
||||
"read_file",
|
||||
"read_command_output",
|
||||
"write_to_file",
|
||||
|
|
|
|||
|
|
@ -411,6 +411,19 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "terminate_session":
|
||||
if (partialArgs.session_id !== undefined) {
|
||||
nativeArgs = {
|
||||
session_id: partialArgs.session_id,
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "list_sessions":
|
||||
// No parameters needed
|
||||
nativeArgs = {}
|
||||
break
|
||||
|
||||
case "write_to_file":
|
||||
if (partialArgs.path || partialArgs.content) {
|
||||
nativeArgs = {
|
||||
|
|
@ -709,6 +722,19 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
case "terminate_session":
|
||||
if (args.session_id !== undefined) {
|
||||
nativeArgs = {
|
||||
session_id: args.session_id,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "list_sessions":
|
||||
// No parameters needed
|
||||
nativeArgs = {} as NativeArgsFor<TName>
|
||||
break
|
||||
|
||||
case "apply_diff":
|
||||
if (args.path !== undefined && args.diff !== undefined) {
|
||||
nativeArgs = {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { searchFilesTool } from "../tools/SearchFilesTool"
|
|||
import { browserActionTool } from "../tools/BrowserActionTool"
|
||||
import { executeCommandTool } from "../tools/ExecuteCommandTool"
|
||||
import { writeStdinTool } from "../tools/WriteStdinTool"
|
||||
import { terminateSessionTool } from "../tools/TerminateSessionTool"
|
||||
import { listSessionsTool } from "../tools/ListSessionsTool"
|
||||
import { useMcpToolTool } from "../tools/UseMcpToolTool"
|
||||
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
|
||||
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
|
||||
|
|
@ -864,6 +866,20 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "terminate_session":
|
||||
await terminateSessionTool.handle(cline, block as ToolUse<"terminate_session">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "list_sessions":
|
||||
await listSessionsTool.handle(cline, block as ToolUse<"list_sessions">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
})
|
||||
break
|
||||
case "use_mcp_tool":
|
||||
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
|
||||
askApproval,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import executeCommand from "./execute_command"
|
|||
import fetchInstructions from "./fetch_instructions"
|
||||
import generateImage from "./generate_image"
|
||||
import listFiles from "./list_files"
|
||||
import listSessions from "./list_sessions"
|
||||
import newTask from "./new_task"
|
||||
import readCommandOutput from "./read_command_output"
|
||||
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
|
||||
|
|
@ -19,6 +20,7 @@ import searchReplace from "./search_replace"
|
|||
import edit_file from "./edit_file"
|
||||
import searchFiles from "./search_files"
|
||||
import switchMode from "./switch_mode"
|
||||
import terminateSession from "./terminate_session"
|
||||
import updateTodoList from "./update_todo_list"
|
||||
import writeStdin from "./write_stdin"
|
||||
import writeToFile from "./write_to_file"
|
||||
|
|
@ -66,6 +68,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
|
|||
fetchInstructions,
|
||||
generateImage,
|
||||
listFiles,
|
||||
listSessions,
|
||||
newTask,
|
||||
readCommandOutput,
|
||||
createReadFileTool(readFileOptions),
|
||||
|
|
@ -75,6 +78,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
|
|||
edit_file,
|
||||
searchFiles,
|
||||
switchMode,
|
||||
terminateSession,
|
||||
updateTodoList,
|
||||
writeStdin,
|
||||
writeToFile,
|
||||
|
|
|
|||
47
src/core/prompts/tools/native-tools/list_sessions.ts
Normal file
47
src/core/prompts/tools/native-tools/list_sessions.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
/**
|
||||
* Native tool definition for list_sessions.
|
||||
*
|
||||
* This tool allows the LLM to see all active terminal sessions
|
||||
* that can be interacted with using write_stdin or terminated.
|
||||
*/
|
||||
|
||||
const LIST_SESSIONS_DESCRIPTION = `Lists all active terminal sessions that were started by execute_command.
|
||||
|
||||
Use this tool when:
|
||||
1. You need to know which sessions are still running
|
||||
2. You forgot or lost track of a session_id
|
||||
3. You want to see the status of multiple background processes
|
||||
4. Before using write_stdin or terminate_session when unsure of the session_id
|
||||
|
||||
Returns a list of sessions with:
|
||||
- session_id: The identifier to use with write_stdin or terminate_session
|
||||
- command: The original command that was executed
|
||||
- running: Whether the process is still actively running
|
||||
- last_used: Relative time since last interaction
|
||||
|
||||
Example response:
|
||||
┌──────────┬─────────────────────────────────┬─────────┬──────────────┐
|
||||
│ Session │ Command │ Status │ Last Used │
|
||||
├──────────┼─────────────────────────────────┼─────────┼──────────────┤
|
||||
│ 1 │ npm run dev │ Running │ 30 seconds │
|
||||
│ 2 │ python manage.py runserver │ Running │ 2 minutes │
|
||||
│ 3 │ tail -f /var/log/syslog │ Stopped │ 5 minutes │
|
||||
└──────────┴─────────────────────────────────┴─────────┴──────────────┘
|
||||
|
||||
This tool takes no parameters.`
|
||||
|
||||
export default {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_sessions",
|
||||
description: LIST_SESSIONS_DESCRIPTION,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
} satisfies OpenAI.Chat.ChatCompletionTool
|
||||
47
src/core/prompts/tools/native-tools/terminate_session.ts
Normal file
47
src/core/prompts/tools/native-tools/terminate_session.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
/**
|
||||
* Native tool definition for terminate_session.
|
||||
*
|
||||
* This tool allows the LLM to terminate a running terminal session
|
||||
* that was started by execute_command and is still active.
|
||||
*/
|
||||
|
||||
const TERMINATE_SESSION_DESCRIPTION = `Terminates a running terminal session by sending an abort signal to the process.
|
||||
|
||||
Use this tool when:
|
||||
1. A long-running command needs to be stopped (e.g., a server, watch process)
|
||||
2. A command is stuck or unresponsive
|
||||
3. You no longer need a background process that was started earlier
|
||||
4. You want to free up resources from idle sessions
|
||||
|
||||
The session_id is returned by execute_command when a process is still running.
|
||||
|
||||
Parameters:
|
||||
- session_id: (required) Identifier of the running exec session to terminate
|
||||
|
||||
Example: Terminating a development server
|
||||
{ "session_id": 1234 }
|
||||
|
||||
Note: After termination, the session_id is no longer valid. Use list_sessions to see remaining active sessions.`
|
||||
|
||||
const SESSION_ID_DESCRIPTION = `Identifier of the running exec session to terminate (returned by execute_command)`
|
||||
|
||||
export default {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "terminate_session",
|
||||
description: TERMINATE_SESSION_DESCRIPTION,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
session_id: {
|
||||
type: "number",
|
||||
description: SESSION_ID_DESCRIPTION,
|
||||
},
|
||||
},
|
||||
required: ["session_id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
} satisfies OpenAI.Chat.ChatCompletionTool
|
||||
143
src/core/tools/ListSessionsTool.ts
Normal file
143
src/core/tools/ListSessionsTool.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { Task } from "../task/Task"
|
||||
import { ToolUse } from "../../shared/tools"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ProcessManager } from "../../integrations/terminal/ProcessManager"
|
||||
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
||||
/**
|
||||
* ListSessionsTool enables the LLM to see all active terminal sessions.
|
||||
*
|
||||
* This tool lists all terminal sessions that were started by execute_command
|
||||
* and can be interacted with using write_stdin or terminated.
|
||||
*
|
||||
* ## Use Cases
|
||||
*
|
||||
* - Checking which background processes are still running
|
||||
* - Finding a session_id that was forgotten
|
||||
* - Auditing resource usage before task completion
|
||||
* - Verifying that a server/process is still active
|
||||
*/
|
||||
export class ListSessionsTool extends BaseTool<"list_sessions"> {
|
||||
readonly name = "list_sessions" as const
|
||||
|
||||
async execute(_params: Record<string, never>, task: Task, callbacks: ToolCallbacks): Promise<void> {
|
||||
const { handleError, pushToolResult } = callbacks
|
||||
|
||||
try {
|
||||
// Get all sessions from ProcessManager
|
||||
const processManager = ProcessManager.getInstance()
|
||||
const sessions = processManager.listSessions(task.taskId)
|
||||
|
||||
task.consecutiveMistakeCount = 0
|
||||
|
||||
// Format response
|
||||
const response = this.formatResponse(sessions)
|
||||
|
||||
await task.say("tool", response, undefined, false)
|
||||
pushToolResult(formatResponse.toolResult(response))
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await handleError("listing sessions", error instanceof Error ? error : new Error(errorMessage))
|
||||
task.recordToolError("list_sessions")
|
||||
}
|
||||
}
|
||||
|
||||
override async handlePartial(task: Task, _block: ToolUse<"list_sessions">): Promise<void> {
|
||||
await task.say(
|
||||
"tool",
|
||||
JSON.stringify({
|
||||
tool: "list_sessions",
|
||||
content: "Listing active sessions...",
|
||||
}),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the sessions list into a readable table.
|
||||
*/
|
||||
private formatResponse(
|
||||
sessions: Array<{
|
||||
sessionId: number
|
||||
taskId: string
|
||||
command: string
|
||||
running: boolean
|
||||
lastUsed: number
|
||||
}>,
|
||||
): string {
|
||||
if (sessions.length === 0) {
|
||||
return `## Active Terminal Sessions
|
||||
|
||||
No active sessions found.
|
||||
|
||||
Sessions are created when execute_command starts a process that doesn't complete within the yield time.
|
||||
Use execute_command to start a new process that can be interacted with.`
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push("## Active Terminal Sessions")
|
||||
lines.push("")
|
||||
lines.push(`Found ${sessions.length} active session${sessions.length !== 1 ? "s" : ""}:`)
|
||||
lines.push("")
|
||||
lines.push("| Session | Command | Status | Last Used |")
|
||||
lines.push("|---------|---------|--------|-----------|")
|
||||
|
||||
for (const session of sessions) {
|
||||
const status = session.running ? "🟢 Running" : "⚪ Stopped"
|
||||
const lastUsed = this.formatTimeSince(session.lastUsed)
|
||||
const command = this.truncateCommand(session.command, 40)
|
||||
|
||||
lines.push(`| ${session.sessionId} | \`${command}\` | ${status} | ${lastUsed} |`)
|
||||
}
|
||||
|
||||
lines.push("")
|
||||
lines.push("**Actions:**")
|
||||
lines.push("- Use `write_stdin` with a session_id to send input to a running process")
|
||||
lines.push("- Use `terminate_session` with a session_id to stop a process")
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time since a timestamp as a human-readable string.
|
||||
*/
|
||||
private formatTimeSince(timestamp: number): string {
|
||||
const seconds = Math.floor((Date.now() - timestamp) / 1000)
|
||||
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s ago`
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) {
|
||||
return `${hours}h ago`
|
||||
}
|
||||
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a command string for display.
|
||||
*/
|
||||
private truncateCommand(command: string, maxLength: number): string {
|
||||
// Remove newlines and extra whitespace
|
||||
const cleaned = command.replace(/\s+/g, " ").trim()
|
||||
|
||||
if (cleaned.length <= maxLength) {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
return cleaned.slice(0, maxLength - 3) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const listSessionsTool = new ListSessionsTool()
|
||||
113
src/core/tools/TerminateSessionTool.ts
Normal file
113
src/core/tools/TerminateSessionTool.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { Task } from "../task/Task"
|
||||
import { ToolUse } from "../../shared/tools"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ProcessManager } from "../../integrations/terminal/ProcessManager"
|
||||
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
||||
/**
|
||||
* Parameters for the terminate_session tool.
|
||||
*/
|
||||
interface TerminateSessionParams {
|
||||
/** Session ID of the running process to terminate */
|
||||
session_id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TerminateSessionTool enables the LLM to terminate running terminal sessions.
|
||||
*
|
||||
* This tool works in conjunction with execute_command:
|
||||
* 1. execute_command starts a process and returns a session_id if still running
|
||||
* 2. terminate_session uses that session_id to abort the process
|
||||
*
|
||||
* ## Use Cases
|
||||
*
|
||||
* - Stopping a development server that's no longer needed
|
||||
* - Terminating stuck or unresponsive processes
|
||||
* - Cleaning up background processes before completing a task
|
||||
* - Freeing resources from long-running processes
|
||||
*/
|
||||
export class TerminateSessionTool extends BaseTool<"terminate_session"> {
|
||||
readonly name = "terminate_session" as const
|
||||
|
||||
async execute(params: TerminateSessionParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
|
||||
const { handleError, pushToolResult } = callbacks
|
||||
|
||||
try {
|
||||
const { session_id } = params
|
||||
|
||||
// Validate session_id
|
||||
if (session_id === undefined || session_id === null) {
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("terminate_session")
|
||||
pushToolResult(await task.sayAndCreateMissingParamError("terminate_session", "session_id"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get ProcessManager and terminate the session
|
||||
const processManager = ProcessManager.getInstance()
|
||||
const result = processManager.terminateSession(session_id)
|
||||
|
||||
if (result.success) {
|
||||
task.consecutiveMistakeCount = 0
|
||||
|
||||
// Format success response
|
||||
const response = this.formatResponse({
|
||||
sessionId: session_id,
|
||||
success: true,
|
||||
message: result.message,
|
||||
})
|
||||
|
||||
await task.say("tool", response, undefined, false)
|
||||
pushToolResult(formatResponse.toolResult(response))
|
||||
} else {
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("terminate_session")
|
||||
task.didToolFailInCurrentTurn = true
|
||||
|
||||
const errorMsg = result.message
|
||||
await task.say("error", errorMsg)
|
||||
pushToolResult(`Error: ${errorMsg}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await handleError("terminating session", error instanceof Error ? error : new Error(errorMessage))
|
||||
task.recordToolError("terminate_session")
|
||||
}
|
||||
}
|
||||
|
||||
override async handlePartial(task: Task, block: ToolUse<"terminate_session">): Promise<void> {
|
||||
const sessionId = block.params.session_id || block.nativeArgs?.session_id
|
||||
|
||||
if (sessionId) {
|
||||
await task.say(
|
||||
"tool",
|
||||
JSON.stringify({
|
||||
tool: "terminate_session",
|
||||
session_id: sessionId,
|
||||
content: `Terminating session ${sessionId}...`,
|
||||
}),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the response message for the tool result.
|
||||
*/
|
||||
private formatResponse(params: { sessionId: number; success: boolean; message: string }): string {
|
||||
const { sessionId, success, message } = params
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push(`## Session ${sessionId} Termination`)
|
||||
lines.push("")
|
||||
lines.push(`**Status:** ${success ? "✅ Success" : "❌ Failed"}`)
|
||||
lines.push(`**Message:** ${message}`)
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const terminateSessionTool = new TerminateSessionTool()
|
||||
|
|
@ -210,6 +210,94 @@ export class ProcessManager {
|
|||
return this.processes.size
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active sessions with their info.
|
||||
*
|
||||
* @param taskId - Optional task ID to filter by
|
||||
* @returns Array of session info objects
|
||||
*/
|
||||
listSessions(taskId?: string): Array<{
|
||||
sessionId: number
|
||||
taskId: string
|
||||
command: string
|
||||
running: boolean
|
||||
lastUsed: number
|
||||
}> {
|
||||
// Clean up first to get accurate state
|
||||
this.cleanup()
|
||||
|
||||
const sessions: Array<{
|
||||
sessionId: number
|
||||
taskId: string
|
||||
command: string
|
||||
running: boolean
|
||||
lastUsed: number
|
||||
}> = []
|
||||
|
||||
for (const [sessionId, entry] of this.processes.entries()) {
|
||||
if (!taskId || entry.taskId === taskId) {
|
||||
sessions.push({
|
||||
sessionId,
|
||||
taskId: entry.taskId,
|
||||
command: entry.command,
|
||||
running: entry.running,
|
||||
lastUsed: entry.lastUsed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by session ID for consistent ordering
|
||||
return sessions.sort((a, b) => a.sessionId - b.sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate a session by sending abort signal to the process.
|
||||
*
|
||||
* @param sessionId - The session ID to terminate
|
||||
* @returns Object with success status and optional message
|
||||
*/
|
||||
terminateSession(sessionId: number): { success: boolean; message: string } {
|
||||
const entry = this.processes.get(sessionId)
|
||||
|
||||
if (!entry) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Session ${sessionId} not found. Use list_sessions to see active sessions.`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.running) {
|
||||
// Session exists but already completed
|
||||
this.processes.delete(sessionId)
|
||||
return {
|
||||
success: true,
|
||||
message: `Session ${sessionId} was already completed. Entry removed.`,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Abort the process
|
||||
entry.process.abort()
|
||||
entry.running = false
|
||||
|
||||
// Remove from tracking
|
||||
this.processes.delete(sessionId)
|
||||
|
||||
console.log(`[ProcessManager] Terminated session ${sessionId}`)
|
||||
return {
|
||||
success: true,
|
||||
message: `Session ${sessionId} terminated successfully.`,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[ProcessManager] Error terminating session ${sessionId}:`, errorMessage)
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to terminate session ${sessionId}: ${errorMessage}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up completed processes.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -222,4 +222,125 @@ describe("ProcessManager", () => {
|
|||
expect(sessions).toContain(id2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("listSessions", () => {
|
||||
it("should return all sessions when no taskId filter", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
|
||||
manager.registerProcess(terminal, createMockProcess(), "task-1", "echo 1")
|
||||
manager.registerProcess(terminal, createMockProcess(), "task-2", "echo 2")
|
||||
|
||||
const sessions = manager.listSessions()
|
||||
|
||||
expect(sessions).toHaveLength(2)
|
||||
expect(sessions[0]).toMatchObject({
|
||||
taskId: "task-1",
|
||||
command: "echo 1",
|
||||
running: true,
|
||||
})
|
||||
expect(sessions[1]).toMatchObject({
|
||||
taskId: "task-2",
|
||||
command: "echo 2",
|
||||
running: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should filter sessions by taskId", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
|
||||
manager.registerProcess(terminal, createMockProcess(), "task-1", "echo 1")
|
||||
manager.registerProcess(terminal, createMockProcess(), "task-2", "echo 2")
|
||||
|
||||
const sessions = manager.listSessions("task-1")
|
||||
|
||||
expect(sessions).toHaveLength(1)
|
||||
expect(sessions[0].taskId).toBe("task-1")
|
||||
})
|
||||
|
||||
it("should return empty array when no sessions exist", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
|
||||
const sessions = manager.listSessions()
|
||||
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should sort sessions by session ID", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
|
||||
const id1 = manager.registerProcess(terminal, createMockProcess(), "task-1", "echo 1")
|
||||
const id2 = manager.registerProcess(terminal, createMockProcess(), "task-1", "echo 2")
|
||||
|
||||
const sessions = manager.listSessions()
|
||||
|
||||
expect(sessions[0].sessionId).toBe(id1)
|
||||
expect(sessions[1].sessionId).toBe(id2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("terminateSession", () => {
|
||||
it("should return error for non-existent session", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
|
||||
const result = manager.terminateSession(999)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.message).toContain("not found")
|
||||
})
|
||||
|
||||
it("should remove completed session and return success", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
const process = createMockProcess()
|
||||
|
||||
const sessionId = manager.registerProcess(terminal, process, "task-1", "echo test")
|
||||
manager.markCompleted(sessionId)
|
||||
|
||||
const result = manager.terminateSession(sessionId)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.message).toContain("already completed")
|
||||
expect(manager.getProcess(sessionId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should abort running process and return success", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
const mockAbort = vi.fn()
|
||||
const process = {
|
||||
...createMockProcess(),
|
||||
abort: mockAbort,
|
||||
} as unknown as RooTerminalProcess
|
||||
|
||||
const sessionId = manager.registerProcess(terminal, process, "task-1", "sleep 100")
|
||||
|
||||
const result = manager.terminateSession(sessionId)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.message).toContain("terminated successfully")
|
||||
expect(mockAbort).toHaveBeenCalled()
|
||||
expect(manager.getProcess(sessionId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle abort errors gracefully", () => {
|
||||
const manager = ProcessManager.getInstance()
|
||||
const terminal = createMockTerminal(1)
|
||||
const process = {
|
||||
...createMockProcess(),
|
||||
abort: () => {
|
||||
throw new Error("Failed to abort")
|
||||
},
|
||||
} as unknown as RooTerminalProcess
|
||||
|
||||
const sessionId = manager.registerProcess(terminal, process, "task-1", "sleep 100")
|
||||
|
||||
const result = manager.terminateSession(sessionId)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.message).toContain("Failed to abort")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@ export type NativeToolArgs = {
|
|||
update_todo_list: { todos: string }
|
||||
use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> }
|
||||
write_stdin: { session_id: number; chars?: string; yield_time_ms?: number; max_output_tokens?: number }
|
||||
terminate_session: { session_id: number }
|
||||
list_sessions: Record<string, never> // No parameters
|
||||
write_to_file: { path: string; content: string }
|
||||
// Add more tools as they are migrated to native protocol
|
||||
}
|
||||
|
|
@ -252,6 +254,8 @@ export type ToolGroupConfig = {
|
|||
export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
|
||||
execute_command: "run commands",
|
||||
write_stdin: "write to terminal input",
|
||||
terminate_session: "terminate terminal sessions",
|
||||
list_sessions: "list active terminal sessions",
|
||||
read_file: "read files",
|
||||
read_command_output: "read command output",
|
||||
fetch_instructions: "fetch instructions",
|
||||
|
|
@ -290,7 +294,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
|
|||
tools: ["browser_action"],
|
||||
},
|
||||
command: {
|
||||
tools: ["execute_command", "write_stdin", "read_command_output"],
|
||||
tools: ["execute_command", "write_stdin", "terminate_session", "list_sessions", "read_command_output"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue