mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-05 02:41:26 +00:00
Merge ad968d16bf into 9cfaf38d8a
This commit is contained in:
commit
aa397a36d1
11 changed files with 974 additions and 0 deletions
|
|
@ -232,6 +232,15 @@ export const globalSettingsSchema = z.object({
|
||||||
* Tools in this list will be excluded from prompt generation and rejected at execution time.
|
* Tools in this list will be excluded from prompt generation and rejected at execution time.
|
||||||
*/
|
*/
|
||||||
disabledTools: z.array(toolNamesSchema).optional(),
|
disabledTools: z.array(toolNamesSchema).optional(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable programmatic tool calling.
|
||||||
|
* When enabled, supported models can generate Python code that calls multiple tools
|
||||||
|
* within a single sandboxed code execution, reducing round-trips to the model.
|
||||||
|
* Tools still require individual approval before execution.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
enableProgrammaticToolCalling: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
|
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,13 @@ export const modelInfoSchema = z.object({
|
||||||
// These tools will be added if they belong to an allowed group in the current mode
|
// These tools will be added if they belong to an allowed group in the current mode
|
||||||
// Cannot force-add tools from groups the mode doesn't allow
|
// Cannot force-add tools from groups the mode doesn't allow
|
||||||
includedTools: z.array(z.string()).optional(),
|
includedTools: z.array(z.string()).optional(),
|
||||||
|
/**
|
||||||
|
* Whether the model supports programmatic tool calling.
|
||||||
|
* When true, the model can generate Python code that calls tool functions,
|
||||||
|
* enabling multiple tool calls within a single code execution in a sandbox.
|
||||||
|
* Currently supported by Anthropic Claude models.
|
||||||
|
*/
|
||||||
|
supportsProgrammaticToolCalling: z.boolean().optional(),
|
||||||
/**
|
/**
|
||||||
* Service tiers with pricing information.
|
* Service tiers with pricing information.
|
||||||
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.
|
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||||
cacheReadsPrice: 0.3, // $0.30 per million tokens
|
cacheReadsPrice: 0.3, // $0.30 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
|
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
|
||||||
tiers: [
|
tiers: [
|
||||||
{
|
{
|
||||||
|
|
@ -59,6 +60,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
cacheWritesPrice: 3.75, // $3.75 per million tokens
|
||||||
cacheReadsPrice: 0.3, // $0.30 per million tokens
|
cacheReadsPrice: 0.3, // $0.30 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
|
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
|
||||||
tiers: [
|
tiers: [
|
||||||
{
|
{
|
||||||
|
|
@ -80,6 +82,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 6.25, // $6.25 per million tokens
|
cacheWritesPrice: 6.25, // $6.25 per million tokens
|
||||||
cacheReadsPrice: 0.5, // $0.50 per million tokens
|
cacheReadsPrice: 0.5, // $0.50 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
// Tiered pricing for extended context (requires beta flag)
|
// Tiered pricing for extended context (requires beta flag)
|
||||||
tiers: [
|
tiers: [
|
||||||
{
|
{
|
||||||
|
|
@ -101,6 +104,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 6.25, // $6.25 per million tokens
|
cacheWritesPrice: 6.25, // $6.25 per million tokens
|
||||||
cacheReadsPrice: 0.5, // $0.50 per million tokens
|
cacheReadsPrice: 0.5, // $0.50 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
},
|
},
|
||||||
"claude-opus-4-1-20250805": {
|
"claude-opus-4-1-20250805": {
|
||||||
maxTokens: 32_000, // Overridden to 8k if `enableReasoningEffort` is false.
|
maxTokens: 32_000, // Overridden to 8k if `enableReasoningEffort` is false.
|
||||||
|
|
@ -112,6 +116,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
||||||
cacheReadsPrice: 1.5, // $1.50 per million tokens
|
cacheReadsPrice: 1.5, // $1.50 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
},
|
},
|
||||||
"claude-opus-4-20250514": {
|
"claude-opus-4-20250514": {
|
||||||
maxTokens: 32_000, // Overridden to 8k if `enableReasoningEffort` is false.
|
maxTokens: 32_000, // Overridden to 8k if `enableReasoningEffort` is false.
|
||||||
|
|
@ -123,6 +128,7 @@ export const anthropicModels = {
|
||||||
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
cacheWritesPrice: 18.75, // $18.75 per million tokens
|
||||||
cacheReadsPrice: 1.5, // $1.50 per million tokens
|
cacheReadsPrice: 1.5, // $1.50 per million tokens
|
||||||
supportsReasoningBudget: true,
|
supportsReasoningBudget: true,
|
||||||
|
supportsProgrammaticToolCalling: true,
|
||||||
},
|
},
|
||||||
"claude-3-7-sonnet-20250219:thinking": {
|
"claude-3-7-sonnet-20250219:thinking": {
|
||||||
maxTokens: 128_000, // Unlocked by passing `beta` flag to the model. Otherwise, it's 64k.
|
maxTokens: 128_000, // Unlocked by passing `beta` flag to the model. Otherwise, it's 64k.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export type ApiStreamChunk =
|
||||||
| ApiStreamToolCallDeltaChunk
|
| ApiStreamToolCallDeltaChunk
|
||||||
| ApiStreamToolCallEndChunk
|
| ApiStreamToolCallEndChunk
|
||||||
| ApiStreamToolCallPartialChunk
|
| ApiStreamToolCallPartialChunk
|
||||||
|
| ApiStreamCodeExecutionChunk
|
||||||
| ApiStreamError
|
| ApiStreamError
|
||||||
|
|
||||||
export interface ApiStreamError {
|
export interface ApiStreamError {
|
||||||
|
|
@ -107,6 +108,19 @@ export interface ApiStreamToolCallPartialChunk {
|
||||||
arguments?: string
|
arguments?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code execution chunk from programmatic tool calling.
|
||||||
|
* The model generates Python code that calls tool functions in a sandboxed environment.
|
||||||
|
* This chunk contains the code to execute and, once executed, the results.
|
||||||
|
*/
|
||||||
|
export interface ApiStreamCodeExecutionChunk {
|
||||||
|
type: "code_execution"
|
||||||
|
/** Unique identifier for this code execution block */
|
||||||
|
id: string
|
||||||
|
/** The Python code generated by the model */
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GroundingSource {
|
export interface GroundingSource {
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
|
|
|
||||||
309
src/services/programmatic-tool-calling/DockerSandboxExecutor.ts
Normal file
309
src/services/programmatic-tool-calling/DockerSandboxExecutor.ts
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
/**
|
||||||
|
* DockerSandboxExecutor - Executes Python code in an isolated Docker container.
|
||||||
|
*
|
||||||
|
* This executor manages the lifecycle of Docker containers used for programmatic
|
||||||
|
* tool calling. It handles:
|
||||||
|
* - Container creation with resource limits
|
||||||
|
* - Mounting the Python tool SDK
|
||||||
|
* - IPC communication between the container and Roo Code
|
||||||
|
* - Tool call routing and approval
|
||||||
|
* - Execution timeout and cleanup
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "child_process"
|
||||||
|
import { EventEmitter } from "events"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as os from "os"
|
||||||
|
|
||||||
|
import type { CodeExecutionResult, SandboxConfig, SandboxToolCall, SandboxToolResult } from "./types"
|
||||||
|
import { DEFAULT_SANDBOX_CONFIG, isSupportedProgrammaticTool } from "./types"
|
||||||
|
import { generatePythonSDK, generateExecutionScript } from "./ToolBridge"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback type for requesting tool approval from the user.
|
||||||
|
* Returns true if the tool call is approved, false otherwise.
|
||||||
|
*/
|
||||||
|
export type ToolApprovalCallback = (toolCall: SandboxToolCall) => Promise<boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback type for executing an approved tool call.
|
||||||
|
* Returns the tool result.
|
||||||
|
*/
|
||||||
|
export type ToolExecutorCallback = (toolCall: SandboxToolCall) => Promise<SandboxToolResult>
|
||||||
|
|
||||||
|
interface ExecutorOptions {
|
||||||
|
config?: Partial<SandboxConfig>
|
||||||
|
onToolApproval: ToolApprovalCallback
|
||||||
|
onToolExecute: ToolExecutorCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DockerSandboxExecutor manages the execution of Python code in a Docker container
|
||||||
|
* with IPC-based tool calling support.
|
||||||
|
*/
|
||||||
|
export class DockerSandboxExecutor extends EventEmitter {
|
||||||
|
private config: SandboxConfig
|
||||||
|
private onToolApproval: ToolApprovalCallback
|
||||||
|
private onToolExecute: ToolExecutorCallback
|
||||||
|
|
||||||
|
constructor(options: ExecutorOptions) {
|
||||||
|
super()
|
||||||
|
this.config = { ...DEFAULT_SANDBOX_CONFIG, ...options.config }
|
||||||
|
this.onToolApproval = options.onToolApproval
|
||||||
|
this.onToolExecute = options.onToolExecute
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Docker is available on the system.
|
||||||
|
*/
|
||||||
|
async isDockerAvailable(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = spawn("docker", ["info"], {
|
||||||
|
stdio: ["ignore", "ignore", "ignore"],
|
||||||
|
})
|
||||||
|
proc.on("close", (code) => resolve(code === 0))
|
||||||
|
proc.on("error", () => resolve(false))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute Python code in a Docker sandbox with tool calling support.
|
||||||
|
*
|
||||||
|
* @param code - The Python code to execute (generated by the model)
|
||||||
|
* @returns The execution result including stdout, stderr, and tool call history
|
||||||
|
*/
|
||||||
|
async execute(code: string): Promise<CodeExecutionResult> {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const toolCalls: Array<{ call: SandboxToolCall; result: SandboxToolResult }> = []
|
||||||
|
|
||||||
|
// Create a temporary directory for mounting the SDK and script
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "roo-sandbox-"))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write the Python SDK module
|
||||||
|
const sdkCode = generatePythonSDK()
|
||||||
|
await fs.writeFile(path.join(tmpDir, "roo_tools.py"), sdkCode, "utf-8")
|
||||||
|
|
||||||
|
// Write the execution script
|
||||||
|
const scriptCode = generateExecutionScript(code)
|
||||||
|
await fs.writeFile(path.join(tmpDir, "run.py"), scriptCode, "utf-8")
|
||||||
|
|
||||||
|
// Build Docker run arguments
|
||||||
|
const dockerArgs = this.buildDockerArgs(tmpDir)
|
||||||
|
|
||||||
|
return await new Promise<CodeExecutionResult>((resolve) => {
|
||||||
|
let stdout = ""
|
||||||
|
let stderr = ""
|
||||||
|
let resolved = false
|
||||||
|
|
||||||
|
const proc = spawn("docker", dockerArgs, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set up timeout
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
proc.kill("SIGKILL")
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
toolCalls,
|
||||||
|
error: `Execution timed out after ${this.config.timeoutMs}ms`,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, this.config.timeoutMs)
|
||||||
|
|
||||||
|
// Handle stdout (IPC messages from the sandbox)
|
||||||
|
let stdoutBuffer = ""
|
||||||
|
proc.stdout!.on("data", (data: Buffer) => {
|
||||||
|
stdoutBuffer += data.toString()
|
||||||
|
|
||||||
|
// Process complete lines (JSON messages)
|
||||||
|
const lines = stdoutBuffer.split("\n")
|
||||||
|
stdoutBuffer = lines.pop() ?? "" // Keep incomplete line in buffer
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.handleSandboxMessage(line, proc, toolCalls).catch((err) => {
|
||||||
|
stderr += `IPC error: ${err.message}\n`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Collect stderr
|
||||||
|
proc.stderr!.on("data", (data: Buffer) => {
|
||||||
|
stderr += data.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle process exit
|
||||||
|
proc.on("close", (exitCode) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
|
||||||
|
// Process any remaining stdout
|
||||||
|
if (stdoutBuffer.trim()) {
|
||||||
|
// Try to parse last message as completion
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(stdoutBuffer)
|
||||||
|
if (msg.type === "complete") {
|
||||||
|
resolve({
|
||||||
|
success: msg.success ?? false,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
toolCalls,
|
||||||
|
error: msg.error,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, treat as regular output
|
||||||
|
stdout += stdoutBuffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: exitCode === 0,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
toolCalls,
|
||||||
|
error: exitCode !== 0 ? `Process exited with code ${exitCode}` : undefined,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
toolCalls,
|
||||||
|
error: `Failed to start Docker container: ${err.message}`,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Clean up temporary directory
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Docker run command arguments.
|
||||||
|
*/
|
||||||
|
private buildDockerArgs(tmpDir: string): string[] {
|
||||||
|
const args = [
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
// Resource limits
|
||||||
|
`--memory=${this.config.memoryLimit}`,
|
||||||
|
`--cpus=${this.config.cpuLimit}`,
|
||||||
|
// Security: read-only root filesystem, no new privileges
|
||||||
|
"--read-only",
|
||||||
|
"--security-opt=no-new-privileges",
|
||||||
|
// Disable network unless explicitly enabled
|
||||||
|
...(this.config.networkEnabled ? [] : ["--network=none"]),
|
||||||
|
// Mount the SDK and script as read-only
|
||||||
|
`-v=${tmpDir}:/workspace:ro`,
|
||||||
|
// Working directory
|
||||||
|
"-w=/workspace",
|
||||||
|
// Interactive mode for stdin/stdout IPC
|
||||||
|
"-i",
|
||||||
|
// Image
|
||||||
|
this.config.image,
|
||||||
|
// Command
|
||||||
|
"python",
|
||||||
|
"/workspace/run.py",
|
||||||
|
]
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a JSON message from the sandbox process.
|
||||||
|
*/
|
||||||
|
private async handleSandboxMessage(
|
||||||
|
line: string,
|
||||||
|
proc: ReturnType<typeof spawn>,
|
||||||
|
toolCalls: Array<{ call: SandboxToolCall; result: SandboxToolResult }>,
|
||||||
|
): Promise<void> {
|
||||||
|
let message: Record<string, unknown>
|
||||||
|
try {
|
||||||
|
message = JSON.parse(line)
|
||||||
|
} catch {
|
||||||
|
// Not a JSON message, treat as regular stdout output
|
||||||
|
this.emit("stdout", line)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case "ready":
|
||||||
|
// Container is ready, send start signal
|
||||||
|
proc.stdin!.write(JSON.stringify({ type: "start" }) + "\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "tool_request": {
|
||||||
|
const toolCall: SandboxToolCall = {
|
||||||
|
tool: message.tool as string,
|
||||||
|
args: message.args as Record<string, unknown>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: SandboxToolResult
|
||||||
|
|
||||||
|
// Validate tool is supported
|
||||||
|
if (!isSupportedProgrammaticTool(toolCall.tool)) {
|
||||||
|
result = {
|
||||||
|
success: false,
|
||||||
|
error: `Tool '${toolCall.tool}' is not supported in programmatic tool calling`,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Request approval from the user
|
||||||
|
const approved = await this.onToolApproval(toolCall)
|
||||||
|
|
||||||
|
if (approved) {
|
||||||
|
// Execute the tool
|
||||||
|
result = await this.onToolExecute(toolCall)
|
||||||
|
} else {
|
||||||
|
result = {
|
||||||
|
success: false,
|
||||||
|
error: `Tool '${toolCall.tool}' was not approved by the user`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCalls.push({ call: toolCall, result })
|
||||||
|
|
||||||
|
// Send response back to the sandbox
|
||||||
|
const response = {
|
||||||
|
requestId: message.requestId,
|
||||||
|
result,
|
||||||
|
}
|
||||||
|
proc.stdin!.write(JSON.stringify(response) + "\n")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "complete":
|
||||||
|
// Code execution completed - handled in the close event
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown message type, emit as stdout
|
||||||
|
this.emit("stdout", line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/services/programmatic-tool-calling/ToolBridge.ts
Normal file
215
src/services/programmatic-tool-calling/ToolBridge.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* ToolBridge - Maps Roo Code tools to Python function definitions for the sandbox.
|
||||||
|
*
|
||||||
|
* This module generates the Python SDK code that gets mounted into the Docker container.
|
||||||
|
* It provides Python function stubs that communicate back to Roo Code via stdin/stdout
|
||||||
|
* IPC to execute actual tool operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SUPPORTED_PROGRAMMATIC_TOOLS, type SupportedProgrammaticTool } from "./types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python function signatures for each supported tool.
|
||||||
|
* These are used to generate the Python SDK that runs inside the Docker sandbox.
|
||||||
|
*/
|
||||||
|
const TOOL_PYTHON_DEFINITIONS: Record<SupportedProgrammaticTool, string> = {
|
||||||
|
read_file: `def read_file(path: str, mode: str = "slice", offset: int = 1, limit: int = 2000) -> str:
|
||||||
|
"""Read a file and return its contents.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The file path relative to the workspace directory.
|
||||||
|
mode: Reading mode - 'slice' for sequential lines (default).
|
||||||
|
offset: 1-based line offset to start reading from (default: 1).
|
||||||
|
limit: Maximum number of lines to return (default: 2000).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The file contents as a string.
|
||||||
|
"""
|
||||||
|
return _call_tool("read_file", {"path": path, "mode": mode, "offset": offset, "limit": limit})`,
|
||||||
|
|
||||||
|
write_to_file: `def write_to_file(path: str, content: str) -> str:
|
||||||
|
"""Write content to a file. Creates the file if it doesn't exist, overwrites if it does.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The file path relative to the workspace directory.
|
||||||
|
content: The complete file content to write.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A confirmation message.
|
||||||
|
"""
|
||||||
|
return _call_tool("write_to_file", {"path": path, "content": content})`,
|
||||||
|
|
||||||
|
execute_command: `def execute_command(command: str, cwd: str = None) -> str:
|
||||||
|
"""Execute a CLI command on the host system.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: The command to execute.
|
||||||
|
cwd: Optional working directory for the command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The command output (stdout and stderr).
|
||||||
|
"""
|
||||||
|
args = {"command": command}
|
||||||
|
if cwd is not None:
|
||||||
|
args["cwd"] = cwd
|
||||||
|
return _call_tool("execute_command", args)`,
|
||||||
|
|
||||||
|
search_files: `def search_files(path: str, regex: str, file_pattern: str = None) -> str:
|
||||||
|
"""Search for a regex pattern across files in a directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The directory path to search in (relative to workspace).
|
||||||
|
regex: The regular expression pattern to search for.
|
||||||
|
file_pattern: Optional glob pattern to filter files (e.g., '*.ts').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Search results with context.
|
||||||
|
"""
|
||||||
|
args = {"path": path, "regex": regex}
|
||||||
|
if file_pattern is not None:
|
||||||
|
args["file_pattern"] = file_pattern
|
||||||
|
return _call_tool("search_files", args)`,
|
||||||
|
|
||||||
|
list_files: `def list_files(path: str, recursive: bool = False) -> str:
|
||||||
|
"""List files and directories in a specified directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The directory path to list contents for (relative to workspace).
|
||||||
|
recursive: Whether to list files recursively (default: False).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A listing of files and directories.
|
||||||
|
"""
|
||||||
|
return _call_tool("list_files", {"path": path, "recursive": recursive})`,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the complete Python SDK code that will be mounted into the Docker sandbox.
|
||||||
|
*
|
||||||
|
* The SDK provides:
|
||||||
|
* - Tool function definitions (read_file, write_to_file, etc.)
|
||||||
|
* - IPC mechanism using stdin/stdout JSON messages
|
||||||
|
* - Error handling and result parsing
|
||||||
|
*
|
||||||
|
* @param enabledTools - Subset of tools to include in the SDK (defaults to all supported tools)
|
||||||
|
* @returns The Python SDK source code as a string
|
||||||
|
*/
|
||||||
|
export function generatePythonSDK(enabledTools?: SupportedProgrammaticTool[]): string {
|
||||||
|
const tools = enabledTools ?? [...SUPPORTED_PROGRAMMATIC_TOOLS]
|
||||||
|
|
||||||
|
const toolDefinitions = tools.map((tool) => TOOL_PYTHON_DEFINITIONS[tool]).join("\n\n")
|
||||||
|
|
||||||
|
return `"""
|
||||||
|
Roo Code Programmatic Tool Calling SDK
|
||||||
|
|
||||||
|
This module provides Python functions for calling Roo Code tools from within
|
||||||
|
a sandboxed environment. Each function communicates with the host Roo Code
|
||||||
|
instance via JSON IPC over stdin/stdout.
|
||||||
|
|
||||||
|
Auto-generated - do not edit manually.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def _call_tool(tool_name: str, args: dict) -> str:
|
||||||
|
"""Internal: Send a tool call request to Roo Code and wait for the result.
|
||||||
|
|
||||||
|
Communication protocol:
|
||||||
|
1. Write a JSON request to stdout (flushed)
|
||||||
|
2. Read a JSON response from stdin
|
||||||
|
3. Parse and return the result or raise an error
|
||||||
|
"""
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
request = {
|
||||||
|
"type": "tool_request",
|
||||||
|
"requestId": request_id,
|
||||||
|
"tool": tool_name,
|
||||||
|
"args": args,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send request via stdout
|
||||||
|
sys.stdout.write(json.dumps(request) + "\\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Read response from stdin
|
||||||
|
response_line = sys.stdin.readline()
|
||||||
|
if not response_line:
|
||||||
|
raise RuntimeError(f"No response received for tool call: {tool_name}")
|
||||||
|
|
||||||
|
response = json.loads(response_line)
|
||||||
|
|
||||||
|
if response.get("requestId") != request_id:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Response ID mismatch: expected {request_id}, got {response.get('requestId')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.get("result", {})
|
||||||
|
|
||||||
|
if not result.get("success", False):
|
||||||
|
error_msg = result.get("error", "Unknown error")
|
||||||
|
raise RuntimeError(f"Tool '{tool_name}' failed: {error_msg}")
|
||||||
|
|
||||||
|
return result.get("result", "")
|
||||||
|
|
||||||
|
|
||||||
|
# Tool function definitions
|
||||||
|
${toolDefinitions}
|
||||||
|
|
||||||
|
|
||||||
|
# Export list of available tools
|
||||||
|
AVAILABLE_TOOLS = [${tools.map((t) => `"${t}"`).join(", ")}]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a Python wrapper script that imports the SDK and executes user code.
|
||||||
|
*
|
||||||
|
* @param userCode - The Python code generated by the model
|
||||||
|
* @returns Complete Python script to execute in the sandbox
|
||||||
|
*/
|
||||||
|
export function generateExecutionScript(userCode: string): string {
|
||||||
|
return `"""
|
||||||
|
Roo Code Programmatic Tool Calling - Execution Wrapper
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# Import the tool SDK
|
||||||
|
from roo_tools import *
|
||||||
|
|
||||||
|
# Signal ready
|
||||||
|
sys.stdout.write(json.dumps({"type": "ready"}) + "\\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Wait for start signal
|
||||||
|
start_line = sys.stdin.readline()
|
||||||
|
start = json.loads(start_line)
|
||||||
|
if start.get("type") != "start":
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute the model-generated code
|
||||||
|
${userCode
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => ` ${line}`)
|
||||||
|
.join("\n")}
|
||||||
|
|
||||||
|
# Signal completion
|
||||||
|
sys.stdout.write(json.dumps({"type": "complete", "success": True}) + "\\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
# Signal error
|
||||||
|
sys.stdout.write(json.dumps({
|
||||||
|
"type": "complete",
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
}) + "\\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { DockerSandboxExecutor } from "../DockerSandboxExecutor"
|
||||||
|
import type { SandboxToolCall, SandboxToolResult } from "../types"
|
||||||
|
import { DEFAULT_SANDBOX_CONFIG } from "../types"
|
||||||
|
|
||||||
|
// Mock child_process
|
||||||
|
vi.mock("child_process", () => ({
|
||||||
|
spawn: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock fs/promises
|
||||||
|
vi.mock("fs/promises", () => ({
|
||||||
|
mkdtemp: vi.fn().mockResolvedValue("/tmp/roo-sandbox-test"),
|
||||||
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||||
|
rm: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("DockerSandboxExecutor", () => {
|
||||||
|
let executor: DockerSandboxExecutor
|
||||||
|
let mockApproval: ReturnType<typeof vi.fn>
|
||||||
|
let mockExecute: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockApproval = vi.fn().mockResolvedValue(true)
|
||||||
|
mockExecute = vi.fn().mockResolvedValue({ success: true, result: "ok" } satisfies SandboxToolResult)
|
||||||
|
executor = new DockerSandboxExecutor({
|
||||||
|
onToolApproval: mockApproval,
|
||||||
|
onToolExecute: mockExecute,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("should use default config when no config is provided", () => {
|
||||||
|
const exec = new DockerSandboxExecutor({
|
||||||
|
onToolApproval: mockApproval,
|
||||||
|
onToolExecute: mockExecute,
|
||||||
|
})
|
||||||
|
// Executor is created without errors
|
||||||
|
expect(exec).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should merge custom config with defaults", () => {
|
||||||
|
const exec = new DockerSandboxExecutor({
|
||||||
|
config: { timeoutMs: 60_000, networkEnabled: true },
|
||||||
|
onToolApproval: mockApproval,
|
||||||
|
onToolExecute: mockExecute,
|
||||||
|
})
|
||||||
|
expect(exec).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isDockerAvailable", () => {
|
||||||
|
it("should resolve to false when docker is not available", async () => {
|
||||||
|
const { spawn } = await import("child_process")
|
||||||
|
const mockSpawn = vi.mocked(spawn)
|
||||||
|
|
||||||
|
// Mock spawn to simulate docker not found
|
||||||
|
mockSpawn.mockImplementation((() => {
|
||||||
|
const proc = {
|
||||||
|
on: vi.fn((event: string, cb: (...args: unknown[]) => void) => {
|
||||||
|
if (event === "error") {
|
||||||
|
setTimeout(() => cb(new Error("ENOENT")), 0)
|
||||||
|
}
|
||||||
|
return proc
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return proc
|
||||||
|
}) as unknown as typeof spawn)
|
||||||
|
|
||||||
|
const result = await executor.isDockerAvailable()
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should resolve to true when docker is available", async () => {
|
||||||
|
const { spawn } = await import("child_process")
|
||||||
|
const mockSpawn = vi.mocked(spawn)
|
||||||
|
|
||||||
|
mockSpawn.mockImplementation((() => {
|
||||||
|
const proc = {
|
||||||
|
on: vi.fn((event: string, cb: (...args: unknown[]) => void) => {
|
||||||
|
if (event === "close") {
|
||||||
|
setTimeout(() => cb(0), 0)
|
||||||
|
}
|
||||||
|
return proc
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return proc
|
||||||
|
}) as unknown as typeof spawn)
|
||||||
|
|
||||||
|
const result = await executor.isDockerAvailable()
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("buildDockerArgs (private, tested indirectly)", () => {
|
||||||
|
it("should construct proper Docker arguments", () => {
|
||||||
|
// We test this indirectly through the execute method
|
||||||
|
// but also verify the config structure
|
||||||
|
const config = { ...DEFAULT_SANDBOX_CONFIG }
|
||||||
|
expect(config.image).toBe("python:3.12-slim")
|
||||||
|
expect(config.networkEnabled).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { generatePythonSDK, generateExecutionScript } from "../ToolBridge"
|
||||||
|
import { SUPPORTED_PROGRAMMATIC_TOOLS } from "../types"
|
||||||
|
|
||||||
|
describe("ToolBridge", () => {
|
||||||
|
describe("generatePythonSDK", () => {
|
||||||
|
it("should generate valid Python code", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
expect(sdk).toContain("import json")
|
||||||
|
expect(sdk).toContain("import sys")
|
||||||
|
expect(sdk).toContain("import uuid")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include the _call_tool helper function", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
expect(sdk).toContain("def _call_tool(tool_name: str, args: dict) -> str:")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include all supported tool functions by default", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
expect(sdk).toContain("def read_file(")
|
||||||
|
expect(sdk).toContain("def write_to_file(")
|
||||||
|
expect(sdk).toContain("def execute_command(")
|
||||||
|
expect(sdk).toContain("def search_files(")
|
||||||
|
expect(sdk).toContain("def list_files(")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include AVAILABLE_TOOLS list with all tools", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
expect(sdk).toContain("AVAILABLE_TOOLS")
|
||||||
|
for (const tool of SUPPORTED_PROGRAMMATIC_TOOLS) {
|
||||||
|
expect(sdk).toContain(`"${tool}"`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate SDK with only specified tools when filtered", () => {
|
||||||
|
const sdk = generatePythonSDK(["read_file", "list_files"])
|
||||||
|
expect(sdk).toContain("def read_file(")
|
||||||
|
expect(sdk).toContain("def list_files(")
|
||||||
|
expect(sdk).not.toContain("def write_to_file(")
|
||||||
|
expect(sdk).not.toContain("def execute_command(")
|
||||||
|
expect(sdk).not.toContain("def search_files(")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include the IPC protocol implementation", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
// Should write JSON to stdout
|
||||||
|
expect(sdk).toContain("sys.stdout.write(json.dumps(request)")
|
||||||
|
// Should read JSON from stdin
|
||||||
|
expect(sdk).toContain("sys.stdin.readline()")
|
||||||
|
// Should use request IDs
|
||||||
|
expect(sdk).toContain("uuid.uuid4()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include proper docstrings for tool functions", () => {
|
||||||
|
const sdk = generatePythonSDK()
|
||||||
|
expect(sdk).toContain('"""Read a file and return its contents.')
|
||||||
|
expect(sdk).toContain('"""Write content to a file.')
|
||||||
|
expect(sdk).toContain('"""Execute a CLI command')
|
||||||
|
expect(sdk).toContain('"""Search for a regex pattern')
|
||||||
|
expect(sdk).toContain('"""List files and directories')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("generateExecutionScript", () => {
|
||||||
|
it("should wrap user code in the execution template", () => {
|
||||||
|
const userCode = 'result = read_file("test.txt")\nprint(result)'
|
||||||
|
const script = generateExecutionScript(userCode)
|
||||||
|
|
||||||
|
// Should import the tool SDK
|
||||||
|
expect(script).toContain("from roo_tools import *")
|
||||||
|
|
||||||
|
// Should include the user code (indented)
|
||||||
|
expect(script).toContain(' result = read_file("test.txt")')
|
||||||
|
expect(script).toContain(" print(result)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should send ready signal on startup", () => {
|
||||||
|
const script = generateExecutionScript("pass")
|
||||||
|
expect(script).toContain('"type": "ready"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should wait for start signal before executing", () => {
|
||||||
|
const script = generateExecutionScript("pass")
|
||||||
|
expect(script).toContain('!= "start"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should send completion signal with success status", () => {
|
||||||
|
const script = generateExecutionScript("pass")
|
||||||
|
expect(script).toContain('"type": "complete"')
|
||||||
|
expect(script).toContain('"success": True')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle exceptions and report errors", () => {
|
||||||
|
const script = generateExecutionScript("pass")
|
||||||
|
expect(script).toContain("except Exception as e")
|
||||||
|
expect(script).toContain('"success": False')
|
||||||
|
expect(script).toContain("traceback.format_exc()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should properly indent multi-line user code", () => {
|
||||||
|
const userCode = "for i in range(3):\n print(i)\n read_file(f'file_{i}.txt')"
|
||||||
|
const script = generateExecutionScript(userCode)
|
||||||
|
|
||||||
|
expect(script).toContain(" for i in range(3):")
|
||||||
|
expect(script).toContain(" print(i)")
|
||||||
|
expect(script).toContain(" read_file(f'file_{i}.txt')")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { SUPPORTED_PROGRAMMATIC_TOOLS, DEFAULT_SANDBOX_CONFIG, isSupportedProgrammaticTool } from "../types"
|
||||||
|
|
||||||
|
describe("programmatic-tool-calling types", () => {
|
||||||
|
describe("SUPPORTED_PROGRAMMATIC_TOOLS", () => {
|
||||||
|
it("should include the initial subset of tools", () => {
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toContain("read_file")
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toContain("write_to_file")
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toContain("execute_command")
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toContain("search_files")
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toContain("list_files")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have exactly 5 tools in the initial implementation", () => {
|
||||||
|
expect(SUPPORTED_PROGRAMMATIC_TOOLS).toHaveLength(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DEFAULT_SANDBOX_CONFIG", () => {
|
||||||
|
it("should use python:3.12-slim as the default image", () => {
|
||||||
|
expect(DEFAULT_SANDBOX_CONFIG.image).toBe("python:3.12-slim")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have a 256MB memory limit", () => {
|
||||||
|
expect(DEFAULT_SANDBOX_CONFIG.memoryLimit).toBe(256 * 1024 * 1024)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have a 0.5 CPU limit", () => {
|
||||||
|
expect(DEFAULT_SANDBOX_CONFIG.cpuLimit).toBe(0.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have a 30 second timeout", () => {
|
||||||
|
expect(DEFAULT_SANDBOX_CONFIG.timeoutMs).toBe(30_000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have network disabled by default", () => {
|
||||||
|
expect(DEFAULT_SANDBOX_CONFIG.networkEnabled).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isSupportedProgrammaticTool", () => {
|
||||||
|
it("should return true for supported tools", () => {
|
||||||
|
expect(isSupportedProgrammaticTool("read_file")).toBe(true)
|
||||||
|
expect(isSupportedProgrammaticTool("write_to_file")).toBe(true)
|
||||||
|
expect(isSupportedProgrammaticTool("execute_command")).toBe(true)
|
||||||
|
expect(isSupportedProgrammaticTool("search_files")).toBe(true)
|
||||||
|
expect(isSupportedProgrammaticTool("list_files")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return false for unsupported tools", () => {
|
||||||
|
expect(isSupportedProgrammaticTool("apply_diff")).toBe(false)
|
||||||
|
expect(isSupportedProgrammaticTool("attempt_completion")).toBe(false)
|
||||||
|
expect(isSupportedProgrammaticTool("switch_mode")).toBe(false)
|
||||||
|
expect(isSupportedProgrammaticTool("nonexistent_tool")).toBe(false)
|
||||||
|
expect(isSupportedProgrammaticTool("")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
25
src/services/programmatic-tool-calling/index.ts
Normal file
25
src/services/programmatic-tool-calling/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Programmatic Tool Calling Service
|
||||||
|
*
|
||||||
|
* Enables models to generate Python code that calls multiple tools within a single
|
||||||
|
* sandboxed Docker execution, reducing round-trips to the model.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - DockerSandboxExecutor: Manages Docker container lifecycle and IPC
|
||||||
|
* - ToolBridge: Generates Python SDK code for tool functions
|
||||||
|
* - Types: Shared type definitions for the service
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { DockerSandboxExecutor } from "./DockerSandboxExecutor"
|
||||||
|
export type { ToolApprovalCallback, ToolExecutorCallback } from "./DockerSandboxExecutor"
|
||||||
|
export { generatePythonSDK, generateExecutionScript } from "./ToolBridge"
|
||||||
|
export { SUPPORTED_PROGRAMMATIC_TOOLS, DEFAULT_SANDBOX_CONFIG, isSupportedProgrammaticTool } from "./types"
|
||||||
|
export type {
|
||||||
|
CodeExecutionResult,
|
||||||
|
SandboxConfig,
|
||||||
|
SandboxToolCall,
|
||||||
|
SandboxToolResult,
|
||||||
|
ToolExecutionRequest,
|
||||||
|
ToolExecutionResponse,
|
||||||
|
SupportedProgrammaticTool,
|
||||||
|
} from "./types"
|
||||||
120
src/services/programmatic-tool-calling/types.ts
Normal file
120
src/services/programmatic-tool-calling/types.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Types for the programmatic tool calling service.
|
||||||
|
*
|
||||||
|
* Programmatic tool calling allows models to generate Python code that calls
|
||||||
|
* tool functions within a sandboxed Docker container, enabling multiple tool
|
||||||
|
* invocations in a single round-trip to the model.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single tool call made from within the Python sandbox.
|
||||||
|
*/
|
||||||
|
export interface SandboxToolCall {
|
||||||
|
/** The tool name (e.g., "read_file", "write_to_file") */
|
||||||
|
tool: string
|
||||||
|
/** The tool arguments as a JSON-serializable object */
|
||||||
|
args: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a single tool call executed from the sandbox.
|
||||||
|
*/
|
||||||
|
export interface SandboxToolResult {
|
||||||
|
/** Whether the tool call succeeded */
|
||||||
|
success: boolean
|
||||||
|
/** The tool result (string content) on success */
|
||||||
|
result?: string
|
||||||
|
/** Error message on failure */
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request from the sandbox to execute a tool.
|
||||||
|
* Sent from the Docker container to Roo Code via the IPC bridge.
|
||||||
|
*/
|
||||||
|
export interface ToolExecutionRequest {
|
||||||
|
/** Unique request ID for correlating responses */
|
||||||
|
requestId: string
|
||||||
|
/** The tool to invoke */
|
||||||
|
tool: string
|
||||||
|
/** Arguments for the tool */
|
||||||
|
args: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response sent back to the sandbox after a tool is executed.
|
||||||
|
*/
|
||||||
|
export interface ToolExecutionResponse {
|
||||||
|
/** The request ID this response corresponds to */
|
||||||
|
requestId: string
|
||||||
|
/** The result of the tool execution */
|
||||||
|
result: SandboxToolResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of executing a Python code block in the sandbox.
|
||||||
|
*/
|
||||||
|
export interface CodeExecutionResult {
|
||||||
|
/** Whether the code execution completed successfully */
|
||||||
|
success: boolean
|
||||||
|
/** Standard output from the code execution */
|
||||||
|
stdout: string
|
||||||
|
/** Standard error from the code execution */
|
||||||
|
stderr: string
|
||||||
|
/** The tool calls that were made during execution, with their results */
|
||||||
|
toolCalls: Array<{
|
||||||
|
call: SandboxToolCall
|
||||||
|
result: SandboxToolResult
|
||||||
|
}>
|
||||||
|
/** Error message if execution failed */
|
||||||
|
error?: string
|
||||||
|
/** Execution duration in milliseconds */
|
||||||
|
durationMs: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the Docker sandbox.
|
||||||
|
*/
|
||||||
|
export interface SandboxConfig {
|
||||||
|
/** Docker image to use for the sandbox (default: "python:3.12-slim") */
|
||||||
|
image: string
|
||||||
|
/** Memory limit in bytes (default: 256MB) */
|
||||||
|
memoryLimit: number
|
||||||
|
/** CPU limit as a fraction of a CPU (default: 0.5) */
|
||||||
|
cpuLimit: number
|
||||||
|
/** Execution timeout in milliseconds (default: 30000) */
|
||||||
|
timeoutMs: number
|
||||||
|
/** Whether network access is allowed (default: false) */
|
||||||
|
networkEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default sandbox configuration values.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_SANDBOX_CONFIG: SandboxConfig = {
|
||||||
|
image: "python:3.12-slim",
|
||||||
|
memoryLimit: 256 * 1024 * 1024, // 256MB
|
||||||
|
cpuLimit: 0.5,
|
||||||
|
timeoutMs: 30_000,
|
||||||
|
networkEnabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subset of tools supported in the initial programmatic tool calling implementation.
|
||||||
|
*/
|
||||||
|
export const SUPPORTED_PROGRAMMATIC_TOOLS = [
|
||||||
|
"read_file",
|
||||||
|
"write_to_file",
|
||||||
|
"execute_command",
|
||||||
|
"search_files",
|
||||||
|
"list_files",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type SupportedProgrammaticTool = (typeof SUPPORTED_PROGRAMMATIC_TOOLS)[number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tool name is supported for programmatic tool calling.
|
||||||
|
*/
|
||||||
|
export function isSupportedProgrammaticTool(toolName: string): toolName is SupportedProgrammaticTool {
|
||||||
|
return (SUPPORTED_PROGRAMMATIC_TOOLS as readonly string[]).includes(toolName)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue