Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit c9af762

Browse files
committed
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
1 parent 3b99b93 commit c9af762

11 files changed

Lines changed: 612 additions & 1 deletion

File tree

packages/types/src/tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export type ToolGroup = z.infer<typeof toolGroupsSchema>
1717
export const toolNames = [
1818
"execute_command",
1919
"write_stdin",
20+
"terminate_session",
21+
"list_sessions",
2022
"read_file",
2123
"read_command_output",
2224
"write_to_file",

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ export class NativeToolCallParser {
411411
}
412412
break
413413

414+
case "terminate_session":
415+
if (partialArgs.session_id !== undefined) {
416+
nativeArgs = {
417+
session_id: partialArgs.session_id,
418+
}
419+
}
420+
break
421+
422+
case "list_sessions":
423+
// No parameters needed
424+
nativeArgs = {}
425+
break
426+
414427
case "write_to_file":
415428
if (partialArgs.path || partialArgs.content) {
416429
nativeArgs = {
@@ -709,6 +722,19 @@ export class NativeToolCallParser {
709722
}
710723
break
711724

725+
case "terminate_session":
726+
if (args.session_id !== undefined) {
727+
nativeArgs = {
728+
session_id: args.session_id,
729+
} as NativeArgsFor<TName>
730+
}
731+
break
732+
733+
case "list_sessions":
734+
// No parameters needed
735+
nativeArgs = {} as NativeArgsFor<TName>
736+
break
737+
712738
case "apply_diff":
713739
if (args.path !== undefined && args.diff !== undefined) {
714740
nativeArgs = {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { searchFilesTool } from "../tools/SearchFilesTool"
2727
import { browserActionTool } from "../tools/BrowserActionTool"
2828
import { executeCommandTool } from "../tools/ExecuteCommandTool"
2929
import { writeStdinTool } from "../tools/WriteStdinTool"
30+
import { terminateSessionTool } from "../tools/TerminateSessionTool"
31+
import { listSessionsTool } from "../tools/ListSessionsTool"
3032
import { useMcpToolTool } from "../tools/UseMcpToolTool"
3133
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
3234
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
@@ -864,6 +866,20 @@ export async function presentAssistantMessage(cline: Task) {
864866
pushToolResult,
865867
})
866868
break
869+
case "terminate_session":
870+
await terminateSessionTool.handle(cline, block as ToolUse<"terminate_session">, {
871+
askApproval,
872+
handleError,
873+
pushToolResult,
874+
})
875+
break
876+
case "list_sessions":
877+
await listSessionsTool.handle(cline, block as ToolUse<"list_sessions">, {
878+
askApproval,
879+
handleError,
880+
pushToolResult,
881+
})
882+
break
867883
case "use_mcp_tool":
868884
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
869885
askApproval,

src/core/prompts/tools/native-tools/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import executeCommand from "./execute_command"
1010
import fetchInstructions from "./fetch_instructions"
1111
import generateImage from "./generate_image"
1212
import listFiles from "./list_files"
13+
import listSessions from "./list_sessions"
1314
import newTask from "./new_task"
1415
import readCommandOutput from "./read_command_output"
1516
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
@@ -19,6 +20,7 @@ import searchReplace from "./search_replace"
1920
import edit_file from "./edit_file"
2021
import searchFiles from "./search_files"
2122
import switchMode from "./switch_mode"
23+
import terminateSession from "./terminate_session"
2224
import updateTodoList from "./update_todo_list"
2325
import writeStdin from "./write_stdin"
2426
import writeToFile from "./write_to_file"
@@ -66,6 +68,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
6668
fetchInstructions,
6769
generateImage,
6870
listFiles,
71+
listSessions,
6972
newTask,
7073
readCommandOutput,
7174
createReadFileTool(readFileOptions),
@@ -75,6 +78,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
7578
edit_file,
7679
searchFiles,
7780
switchMode,
81+
terminateSession,
7882
updateTodoList,
7983
writeStdin,
8084
writeToFile,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type OpenAI from "openai"
2+
3+
/**
4+
* Native tool definition for list_sessions.
5+
*
6+
* This tool allows the LLM to see all active terminal sessions
7+
* that can be interacted with using write_stdin or terminated.
8+
*/
9+
10+
const LIST_SESSIONS_DESCRIPTION = `Lists all active terminal sessions that were started by execute_command.
11+
12+
Use this tool when:
13+
1. You need to know which sessions are still running
14+
2. You forgot or lost track of a session_id
15+
3. You want to see the status of multiple background processes
16+
4. Before using write_stdin or terminate_session when unsure of the session_id
17+
18+
Returns a list of sessions with:
19+
- session_id: The identifier to use with write_stdin or terminate_session
20+
- command: The original command that was executed
21+
- running: Whether the process is still actively running
22+
- last_used: Relative time since last interaction
23+
24+
Example response:
25+
┌──────────┬─────────────────────────────────┬─────────┬──────────────┐
26+
│ Session │ Command │ Status │ Last Used │
27+
├──────────┼─────────────────────────────────┼─────────┼──────────────┤
28+
│ 1 │ npm run dev │ Running │ 30 seconds │
29+
│ 2 │ python manage.py runserver │ Running │ 2 minutes │
30+
│ 3 │ tail -f /var/log/syslog │ Stopped │ 5 minutes │
31+
└──────────┴─────────────────────────────────┴─────────┴──────────────┘
32+
33+
This tool takes no parameters.`
34+
35+
export default {
36+
type: "function",
37+
function: {
38+
name: "list_sessions",
39+
description: LIST_SESSIONS_DESCRIPTION,
40+
parameters: {
41+
type: "object",
42+
properties: {},
43+
required: [],
44+
additionalProperties: false,
45+
},
46+
},
47+
} satisfies OpenAI.Chat.ChatCompletionTool
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type OpenAI from "openai"
2+
3+
/**
4+
* Native tool definition for terminate_session.
5+
*
6+
* This tool allows the LLM to terminate a running terminal session
7+
* that was started by execute_command and is still active.
8+
*/
9+
10+
const TERMINATE_SESSION_DESCRIPTION = `Terminates a running terminal session by sending an abort signal to the process.
11+
12+
Use this tool when:
13+
1. A long-running command needs to be stopped (e.g., a server, watch process)
14+
2. A command is stuck or unresponsive
15+
3. You no longer need a background process that was started earlier
16+
4. You want to free up resources from idle sessions
17+
18+
The session_id is returned by execute_command when a process is still running.
19+
20+
Parameters:
21+
- session_id: (required) Identifier of the running exec session to terminate
22+
23+
Example: Terminating a development server
24+
{ "session_id": 1234 }
25+
26+
Note: After termination, the session_id is no longer valid. Use list_sessions to see remaining active sessions.`
27+
28+
const SESSION_ID_DESCRIPTION = `Identifier of the running exec session to terminate (returned by execute_command)`
29+
30+
export default {
31+
type: "function",
32+
function: {
33+
name: "terminate_session",
34+
description: TERMINATE_SESSION_DESCRIPTION,
35+
parameters: {
36+
type: "object",
37+
properties: {
38+
session_id: {
39+
type: "number",
40+
description: SESSION_ID_DESCRIPTION,
41+
},
42+
},
43+
required: ["session_id"],
44+
additionalProperties: false,
45+
},
46+
},
47+
} satisfies OpenAI.Chat.ChatCompletionTool

src/core/tools/ListSessionsTool.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { Task } from "../task/Task"
2+
import { ToolUse } from "../../shared/tools"
3+
import { formatResponse } from "../prompts/responses"
4+
import { ProcessManager } from "../../integrations/terminal/ProcessManager"
5+
6+
import { BaseTool, ToolCallbacks } from "./BaseTool"
7+
8+
/**
9+
* ListSessionsTool enables the LLM to see all active terminal sessions.
10+
*
11+
* This tool lists all terminal sessions that were started by execute_command
12+
* and can be interacted with using write_stdin or terminated.
13+
*
14+
* ## Use Cases
15+
*
16+
* - Checking which background processes are still running
17+
* - Finding a session_id that was forgotten
18+
* - Auditing resource usage before task completion
19+
* - Verifying that a server/process is still active
20+
*/
21+
export class ListSessionsTool extends BaseTool<"list_sessions"> {
22+
readonly name = "list_sessions" as const
23+
24+
async execute(_params: Record<string, never>, task: Task, callbacks: ToolCallbacks): Promise<void> {
25+
const { handleError, pushToolResult } = callbacks
26+
27+
try {
28+
// Get all sessions from ProcessManager
29+
const processManager = ProcessManager.getInstance()
30+
const sessions = processManager.listSessions(task.taskId)
31+
32+
task.consecutiveMistakeCount = 0
33+
34+
// Format response
35+
const response = this.formatResponse(sessions)
36+
37+
await task.say("tool", response, undefined, false)
38+
pushToolResult(formatResponse.toolResult(response))
39+
} catch (error) {
40+
const errorMessage = error instanceof Error ? error.message : String(error)
41+
await handleError("listing sessions", error instanceof Error ? error : new Error(errorMessage))
42+
task.recordToolError("list_sessions")
43+
}
44+
}
45+
46+
override async handlePartial(task: Task, _block: ToolUse<"list_sessions">): Promise<void> {
47+
await task.say(
48+
"tool",
49+
JSON.stringify({
50+
tool: "list_sessions",
51+
content: "Listing active sessions...",
52+
}),
53+
undefined,
54+
true,
55+
)
56+
}
57+
58+
/**
59+
* Format the sessions list into a readable table.
60+
*/
61+
private formatResponse(
62+
sessions: Array<{
63+
sessionId: number
64+
taskId: string
65+
command: string
66+
running: boolean
67+
lastUsed: number
68+
}>,
69+
): string {
70+
if (sessions.length === 0) {
71+
return `## Active Terminal Sessions
72+
73+
No active sessions found.
74+
75+
Sessions are created when execute_command starts a process that doesn't complete within the yield time.
76+
Use execute_command to start a new process that can be interacted with.`
77+
}
78+
79+
const lines: string[] = []
80+
lines.push("## Active Terminal Sessions")
81+
lines.push("")
82+
lines.push(`Found ${sessions.length} active session${sessions.length !== 1 ? "s" : ""}:`)
83+
lines.push("")
84+
lines.push("| Session | Command | Status | Last Used |")
85+
lines.push("|---------|---------|--------|-----------|")
86+
87+
for (const session of sessions) {
88+
const status = session.running ? "🟢 Running" : "⚪ Stopped"
89+
const lastUsed = this.formatTimeSince(session.lastUsed)
90+
const command = this.truncateCommand(session.command, 40)
91+
92+
lines.push(`| ${session.sessionId} | \`${command}\` | ${status} | ${lastUsed} |`)
93+
}
94+
95+
lines.push("")
96+
lines.push("**Actions:**")
97+
lines.push("- Use `write_stdin` with a session_id to send input to a running process")
98+
lines.push("- Use `terminate_session` with a session_id to stop a process")
99+
100+
return lines.join("\n")
101+
}
102+
103+
/**
104+
* Format time since a timestamp as a human-readable string.
105+
*/
106+
private formatTimeSince(timestamp: number): string {
107+
const seconds = Math.floor((Date.now() - timestamp) / 1000)
108+
109+
if (seconds < 60) {
110+
return `${seconds}s ago`
111+
}
112+
113+
const minutes = Math.floor(seconds / 60)
114+
if (minutes < 60) {
115+
return `${minutes}m ago`
116+
}
117+
118+
const hours = Math.floor(minutes / 60)
119+
if (hours < 24) {
120+
return `${hours}h ago`
121+
}
122+
123+
const days = Math.floor(hours / 24)
124+
return `${days}d ago`
125+
}
126+
127+
/**
128+
* Truncate a command string for display.
129+
*/
130+
private truncateCommand(command: string, maxLength: number): string {
131+
// Remove newlines and extra whitespace
132+
const cleaned = command.replace(/\s+/g, " ").trim()
133+
134+
if (cleaned.length <= maxLength) {
135+
return cleaned
136+
}
137+
138+
return cleaned.slice(0, maxLength - 3) + "..."
139+
}
140+
}
141+
142+
// Export singleton instance
143+
export const listSessionsTool = new ListSessionsTool()

0 commit comments

Comments
 (0)