The AIAgentEnvironment provides a safe, controlled context for executing tools within an agent. It acts as a protective layer that ensures tools are executed properly, errors are handled gracefully, and the agent’s execution pipeline remains intact.
Direct tool execution bypasses critical infrastructure:
// ❌ WRONG: Direct tool callval result = myTool.execute(args)// - No event notifications// - No feature pipeline// - No error handling// - Can't be mocked in tests// ✅ CORRECT: Through environmentval toolCall = Message.Tool.Call( name = myTool.name, arguments = args)val result = context.environment.executeTool(toolCall)// - Events fired// - Features intercept// - Errors handled// - Mockable in tests
public interface AIAgentEnvironment { /** * Executes a tool call and returns its result */ public suspend fun executeTool( toolCall: Message.Tool.Call ): ReceivedToolResult /** * Executes multiple tool calls */ public suspend fun executeTools( toolCalls: List<Message.Tool.Call> ): List<ReceivedToolResult> /** * Reports a problem that occurred during execution */ public suspend fun reportProblem(exception: Throwable)}
val strategy = strategy<String, String>("custom") { val executeTool by node<Message.Tool.Call, ReceivedToolResult> { toolCall -> // Execute through environment environment.executeTool(toolCall) } nodeStart then executeTool then nodeFinish}
Execute multiple tools with automatic parallelization:
val strategy = strategy<String, String>("batch") { val executeTools by node<List<Message.Tool.Call>, List<ReceivedToolResult>> { toolCalls -> // Executes tools in supervisorScope environment.executeTools(toolCalls) } nodeStart then executeTools then nodeFinish}
The executeTools method uses supervisorScope and async to execute tools in parallel while ensuring that one tool’s failure doesn’t cancel the others.
The environment works with Message.Tool.Call objects:
data class Message.Tool.Call( val id: String, // Unique call ID val name: String, // Tool name val arguments: JsonObject, // Tool arguments as JSON val metadata: Map<String, String> = emptyMap())
The environment returns ReceivedToolResult objects:
data class ReceivedToolResult( val toolName: String, val id: String, val content: String, // Serialized result val metadata: Map<String, String> = emptyMap(), val error: String? = null // Error message if failed)
Using results:
val result = environment.executeTool(toolCall)if (result.error != null) { println("Tool failed: ${result.error}")} else { println("Tool returned: ${result.content}")}
For critical errors that should stop agent execution:
val strategy = strategy<String, String>("resilient") { val process by node<String, String> { input -> try { processInput(input) } catch (e: CriticalException) { // Report to environment environment.reportProblem(e) throw e // Re-throw to stop execution } } nodeStart then process then nodeFinish}
val strategy = strategy<String, String>("isolated") { val key = createStorageKey<Int>("counter") val increment by node<Unit, Int> { val current = storage.get(key) ?: 0 val next = current + 1 storage.set(key, next) next } // Storage is per-session // Environment is per-session // They're isolated but coordinated}
val strategy = strategy<String, String>("graph") { val customNode by node<String, String> { input -> // environment is available here val toolCall = createToolCall(input) val result = environment.executeTool(toolCall) result.content } nodeStart then customNode then nodeFinish}
val functionalStrategy = object : AIAgentFunctionalStrategy<String, String> { override val name = "functional" override suspend fun execute( context: AIAgentFunctionalContext, input: String ): String { val toolCall = createToolCall(input) val result = context.environment.executeTool(toolCall) return result.content }}
val strategy = strategy("single-tool") { val executeTool by nodeExecuteTool() nodeStart then executeTool then nodeFinish}// Input: Message.Tool.Call// Output: ReceivedToolResult
val strategy = strategy("multi-tool") { val executeTools by nodeExecuteMultipleTools( parallelTools = true // Execute in parallel ) nodeStart then executeTools then nodeFinish}// Input: List<Message.Tool.Call>// Output: List<ReceivedToolResult>
Critical Rule: Never call tools directly in production code.
// ❌ NEVER do thisval myTool = toolRegistry.getTool<MyTool>()val result = myTool.execute(args)// ✅ ALWAYS do thisval toolCall = Message.Tool.Call( name = "myTool", arguments = encodeArgs(args))val result = context.environment.executeTool(toolCall)
val strategy = strategy<String, String>("conditional") { val analyze by nodeLLMRequest() val executeTool by node<Message.Tool.Call, String> { toolCall -> // Only execute if conditions are met if (shouldExecute(toolCall)) { val result = environment.executeTool(toolCall) result.content } else { "Tool execution skipped" } } nodeStart then analyze then executeTool then nodeFinish}
val transform by node<ReceivedToolResult, String> { result -> when { result.error != null -> "Error: ${result.error}" result.content.isEmpty() -> "No results" else -> formatForLLM(result.content) }}