Local Environment Documentation¶
Overview¶
The Local Environment is a TypeScript implementation of the Environment interface that manages tool registration, execution, and lifecycle within the Azad system. It provides a bridge between the WebSocket client and tool implementations, handling tool approval, execution, and observer pattern integration.
Architecture¶
The Local Environment architecture consists of several key components:
- Environment Interface: Defines the contract for environment implementations
- LocalEnvironment: Concrete implementation of the Environment interface
- ToolRegistry: Manages tool registration and lifecycle
- Authority: Handles tool approval decisions
- Observers: Monitor tool execution and provide additional insights
┌─────────────┐ ┌────────────────┐
│ AzadClient │◄────►│ WebSocketClient│
└──────┬──────┘ └────────────────┘
│ ▲
│ │
▼ │
┌──────────────┐ ┌─────────────┐
│ Environment │◄────►│ Authority │
└──────┬───────┘ └─────────────┘
│
▼
┌───────────────┐ ┌───────────┐
│ Tool Registry │◄───►│ Tools │
└───────────────┘ └───────────┘
│
▼
┌───────────────┐
│ Observers │
└───────────────┘
Key Interfaces¶
Environment Interface¶
The Environment interface defines the contract that environment implementations must fulfill:
interface Environment extends BaseEnvironment {
approveTool(
toolCall: ToolCallPart
): Promise<EnvironmentResponse<ApprovalResponse>>;
executeTool(
toolCall: ToolCallPart
): Promise<EnvironmentResponse<ToolResultPart>>;
}
interface BaseEnvironment {
setClient(client: WebSocketClient): void;
}
LocalEnvironment¶
The LocalEnvironment class implements the Environment interface:
class LocalEnvironment implements Environment {
// Tool Registry
private _toolRegistry: ReturnType<typeof toolRegistry.createInstanceRegistry>;
private requestToolCalls = new Map<string, AnyTool>();
// Observers
private observers = new Map<string, Observer>();
// Client and Control
private client: WebSocketClient | null = null;
private _currentAbortController: AbortController | null = null;
// Callbacks
private toolApprovalCallback?: ToolApprovalCallback;
private toolExecutionCallback?: (
toolCall: ToolCallPart,
toolResult?: ToolResultPart
) => void;
// Authority
public authority: CallbackAuthority;
// Methods
constructor();
async approveTool(
toolCall: ToolCallPart
): Promise<EnvironmentResponse<ApprovalResponse>>;
async executeTool(
toolCall: ToolCallPart
): Promise<EnvironmentResponse<ToolResultPart>>;
registerRequestToolCall(toolCall: ToolCallPart): void;
updateRequestToolParams(toolCall: ToolCallPart, isFinal?: boolean): void;
registerTool(tool: AnyTool): void;
abortStep(): void;
setClient(client: WebSocketClient): void;
dispose(): void;
// Callback Registration
registerToolApprovalCallback(callback: ToolApprovalCallback): void;
clearToolApprovalCallback(): void;
registerExecuteCallback(
startCallback: (toolCall: ToolCallPart) => void,
endCallback: (toolCall: ToolCallPart) => void
): void;
clearExecuteCallback(): void;
}
Tool Registry¶
The Tool Registry manages tool registration and lifecycle:
class ToolRegistry {
private _tools = new Map<string, AnyTool>();
// Methods
registerTool(tool: AnyTool): void;
cloneTool(toolName: string): AnyTool | null;
createInstanceRegistry(environment: Environment): {
tools: Map<string, AnyTool>;
registerTool: (tool: AnyTool) => void;
getToolMetadata: () => ToolMetadata[];
disposeAll: () => void;
cloneTool: (toolName: string) => AnyTool | null;
};
}
Authority¶
The Authority handles tool approval decisions:
class CallbackAuthority {
constructor(
private approveToolCallback: (
toolCall: ToolCallPart
) => Promise<EnvironmentResponse<ApprovalResponse>>
);
// Methods
async approveTool(
toolCall: ToolCallPart
): Promise<EnvironmentResponse<ApprovalResponse>>;
}
Usage Examples¶
Creating and Using a Local Environment¶
import { LocalEnvironment } from './environment/environment';
import { executeCommandTool } from './environment/tools/definitions/execute-command';
import { createToolInstance } from './environment/tools/utils/tool-registry';
// Create a local environment
const environment = new LocalEnvironment();
// Register tools
const execTool = createToolInstance(executeCommandTool);
environment.registerTool(execTool);
// Register tool approval callback
environment.registerToolApprovalCallback(async (toolCall) => {
console.log(`Tool approval requested: ${toolCall.tool_name}`);
console.log('Arguments:', toolCall.args);
// Automatically approve all tools
return { approved: true, images: [], feedback: null };
});
// Execute a tool
const result = await environment.executeTool({
type: 'toolCall',
tool_call_id: 'tool-call-123',
tool_name: 'execute_command',
args: { command: 'ls -la' },
is_approval_pending: false,
is_loading: false,
});
console.log('Tool execution result:', result);
// Clean up
environment.dispose();
Integrated with AzadClient¶
import { AzadClient } from './server/azad-client';
import { executeCommandTool } from './environment/tools/definitions/execute-command';
// Create the client
const client = new AzadClient(process.cwd());
// Execute a task with tools
await client.executeTask({
model: {
id: 'gpt-4',
apiKey: 'your-api-key',
},
tools: [executeCommandTool],
task: 'List all files in the current directory',
// Tool approval callback
onToolApproval: async (toolName, toolCallId, args) => {
return { approved: true, images: [], feedback: null };
},
});
Tool Execution Flow¶
The tool execution flow in the LocalEnvironment is as follows:
- Tool Registration: Tools are registered with the environment using
registerTool() - Tool Call Received: A tool call is received and registered using
registerRequestToolCall() - Parameter Updates: Tool parameters are updated through
updateRequestToolParams() - Parameter Finalization: Once parameters are complete,
updateRequestToolParams(isFinal = true)is called - Tool Approval: The tool is sent for approval through the authority
- Tool Execution: If approved, the tool is executed
- Observer Execution: All registered observers are executed
- Result Return: The tool result is returned
- Tool Disposal: The tool instance is disposed
sequenceDiagram
participant Client as AzadClient
participant Env as LocalEnvironment
participant Auth as Authority
participant Tool as Tool
participant Observers as Observers
Client->>Env: registerRequestToolCall()
Client->>Env: updateRequestToolParams()
Client->>Env: updateRequestToolParams(isFinal=true)
Env->>Auth: approveTool()
Auth-->>Env: ApprovalResponse
alt Tool Approved
Env->>Tool: executeTool()
Tool-->>Env: ToolResultPart
Env->>Observers: execute()
Observers-->>Env: ObserverResults
Env-->>Client: Success Response
else Tool Rejected
Env-->>Client: Error Response
end
Env->>Tool: dispose()
Tool Approval Mechanism¶
The tool approval mechanism in the LocalEnvironment is handled by the Authority:
- Callback Registration: A tool approval callback is registered using
registerToolApprovalCallback() - Approval Request: When a tool needs approval, the callback is invoked with the tool call
- Approval Decision: The callback returns an ApprovalResponse with approved flag and optional feedback
- Decision Processing: If approved, the tool is executed; if rejected, an error response is returned
// Register approval callback
environment.registerToolApprovalCallback(async (toolCall) => {
// Custom approval logic
if (toolCall.tool_name === 'execute_command') {
const command = toolCall.args.command;
// Reject potentially harmful commands
if (command.includes('rm -rf') || command.includes('sudo')) {
return {
approved: false,
images: [],
feedback: 'Potentially harmful command rejected',
};
}
}
// Approve other tools
return { approved: true, images: [], feedback: null };
});
Observer Pattern¶
The LocalEnvironment implements the observer pattern to monitor tool execution:
- Observer Registration: Observers are registered with the environment
- Tool Execution: When a tool is executed, all observers are notified
- Observer Processing: Each observer processes the tool call and result
- Result Collection: Observer results are collected and attached to the tool result
// Example observer
class LoggingObserver implements Observer {
name = 'logging_observer';
description = 'Logs all tool calls and results';
async execute(
toolCall: ToolCallPart,
toolResult: ToolResultPart
): Promise<ObserverResult> {
console.log(`Tool executed: ${toolCall.tool_name}`);
console.log('Arguments:', toolCall.args);
console.log('Result:', toolResult);
return {
observer_name: this.name,
observer_result: {
timestamp: new Date().toISOString(),
logged: true,
},
};
}
}
// Register observer
const observer = new LoggingObserver();
environment.observers.set(observer.name, observer);
Tool Lifecycle¶
Tools in the LocalEnvironment go through the following lifecycle:
- Registration: The tool is registered with the environment
- Initialization: The tool is initialized with the environment
- Execution: The tool is executed with parameters
- Disposal: The tool is disposed after execution
// Tool lifecycle example
const tool = createToolInstance(myToolSchema);
// Registration
environment.registerTool(tool);
// Initialization (automatic during registration)
// tool.init(environment);
// Execution
await environment.executeTool({
type: 'toolCall',
tool_call_id: 'tool-call-123',
tool_name: tool.name,
args: {
/* arguments */
},
is_approval_pending: false,
is_loading: false,
});
// Disposal (automatic after execution)
// tool.dispose();
Error Handling and Abort Mechanism¶
The LocalEnvironment includes error handling and an abort mechanism:
- Error Handling: Tool execution errors are caught and returned as error responses
- Abort Mechanism: The environment maintains an AbortController that can be used to abort tool execution
- Abort Signal: The abort signal is passed to tools during execution
// Abort example
try {
// Start executing a long-running tool
const executePromise = environment.executeTool({
type: 'toolCall',
tool_call_id: 'tool-call-123',
tool_name: 'long_running_tool',
args: {
/* arguments */
},
is_approval_pending: false,
is_loading: false,
});
// Abort the execution after 5 seconds
setTimeout(() => {
environment.abortStep();
}, 5000);
// Wait for the result or abort
const result = await executePromise;
console.log('Tool execution result:', result);
} catch (error) {
console.error('Tool execution aborted:', error);
}
Implementation Details¶
Tool Registration and Management¶
The LocalEnvironment uses the ToolRegistry to manage tools:
- Tool Registration: Tools are registered with the registry using
registerTool() - Tool Instance Creation: Tool instances are created using
createInstanceRegistry().cloneTool() - Tool Metadata: Tool metadata is retrieved using
getToolMetadata() - Tool Disposal: Tools are disposed using
disposeAll()
Tool Execution¶
The tool execution process in detail:
- Tool Call Validation: The tool call is validated to ensure it contains required fields
- Tool Lookup: The tool is looked up in the registry using the tool name
- Tool Context Creation: A context is created for the tool execution with abort signal
- Tool Execution: The tool is executed with the parameters and context
- Observer Execution: All registered observers are executed with the tool call and result
- Result Processing: The tool result is processed and returned
Authority Implementation¶
The CallbackAuthority delegates approval decisions to a callback function:
- Callback Invocation: The callback is invoked with the tool call
- Response Handling: The callback response is processed and returned
- Auto-Approval: If no callback is registered, tools are auto-approved (for headless/testing)
Advanced Topics¶
Custom Observers¶
Custom observers can be created by implementing the Observer interface:
class CustomObserver implements Observer {
name = 'custom_observer';
description = 'Custom observer for tool execution';
async execute(
toolCall: ToolCallPart,
toolResult: ToolResultPart
): Promise<ObserverResult> {
// Custom observation logic
const data = {
toolName: toolCall.tool_name,
argsCount: Object.keys(toolCall.args).length,
resultSuccess: toolResult.success,
timestamp: Date.now(),
};
// Return observation result
return {
observer_name: this.name,
observer_result: data,
};
}
}
Tool UI Integration¶
The LocalEnvironment supports tool UI integration through the loadUI method:
- Partial Parameters: When partial parameters are received,
updateRequestToolParams()is called - UI Loading: The tool's loadUI method is called with the partial parameters
- UI Update: The UI can be updated based on the partial parameters
Parallel Tool Execution¶
The LocalEnvironment supports parallel tool execution:
- Parallel Flag: Tools can indicate support for parallel execution through the supportsParllelExecution flag
- Execution Context: The execution context includes the parallel flag
- Abort Handling: Each parallel execution gets its own abort controller
Best Practices¶
- Tool Registration: Register tools before using them
- Tool Approval: Implement proper tool approval callbacks to ensure security
- Error Handling: Handle tool execution errors gracefully
- Resource Management: Always call
dispose()when done with the environment - Observer Usage: Use observers for monitoring and logging, not for critical functionality
- Abort Handling: Implement proper abort handling in long-running tools
- Context Management: Use tool context for persistent state across invocations