59 KiB
Tools — Complete End-to-End Lifecycle
This document describes the complete tool lifecycle in the YiemAgent framework, from definition through execution, for framework authors and maintainers who need a thorough understanding of the architecture.
Table of Contents
- Quick Start: Tool Lifecycle
- Overview
- Tool Definition — The
agentToolStruct - Tool Registration — Per-Agent Tool Stores
- The Agent Loop — High-Level Flow
- Message Processing Pipeline
- Tool Call Extraction from LLM Response
- The Per-Call Pipeline — Prepare, Execute, Finalize
- Execution Modes — Sequential vs Parallel
- Tool Call Batches & Termination Logic
- Tool Result Message Creation
- Error Handling & Recovery Pattern
- Event System — Tool Lifecycle Events
- Agent Lifecycle Hooks
- Self-Modifying Tools
- Complete End-to-End Example
- Tool File Contract
- Appendix: Type Reference
1. Quick Start: Tool Lifecycle
This section shows the complete lifecycle from tool registration through execution and result extraction. Each step maps to the detailed sections below.
Step 1: Discover — listTools
The agent calls the listTools tool to see available tools and detect name collisions before creating new ones.
# The listTools tool is auto-injected via listTool(store) — no manual registration needed
tool = listTool(store) # Returns an agentTool that, when executed, lists all tools in the store
Result extraction:
result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
# result.content[1].text => "Available tools:\n- getTime: Time Lookup — Get current local time...\n- getWeather: Weather Lookup — Fetch current weather..."
Source: toolRegistry.jl:54-82
Step 2: Load — loadTools()
Load all tool modules from a directory into a toolStore. Each .jl file must define getTool()::agentTool. listTool is auto-registered so the LLM can discover available tools.
using YiemAgent, YiemAgent.toolRegistry
store = toolStore(name="myAgent")
tools = loadTools(store, "src/tools")
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
# Also auto-registers listTools for runtime discovery
Result extraction:
all_tools = getTools(store) # OrderedDict{String, agentTool}
# Keys: "getTime", "getWeather", "writeTool", "listTools"
getTime_tool = all_tools["getTime"]
# Manual registration (alternative to loadTools)
registerTool(store, my_tool)
clearTools(store) # Clear all tools from store
Source: toolRegistry.jl:126-178
Step 2.5: Create Agent with Tools
Wire the loaded tools into a new yiemAgent instance. The tools parameter is deep-copied into agent._state.tools; _tool_store is kept for runtime registration.
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
# 1. Set up toolStore and load tools (auto-registers listTools)
store = toolStore(name="myAgent")
loadTools(store, "src/tools")
# 2. Create agent — pass tools + _tool_store
agent = yiemAgent(
systemPrompt = "You are a helpful assistant.",
model = my_model,
tools = getTools(store), # OrderedDict{String, agentTool}
llmCall = my_llm_call, # Function that calls the LLM API
agentEventSink = my_event_sink, # Function for TUI/logging
_tool_store = store, # For runtime registerTool() calls
)
Manual registration (without loadTools):
store = toolStore(name="myAgent")
registerTool(store, getTime_tool)
registerTool(store, getWeather_tool)
registerTool(store, listTool(store)) # needed for manual registration
agent = yiemAgent(
tools = getTools(store),
llmCall = my_llm_call,
agentEventSink = my_event_sink,
_tool_store = store,
)
Key constructor parameters:
| Parameter | Type | Required | Purpose |
|---|---|---|---|
systemPrompt |
String |
No (default: "You are helpful assistant.") | System prompt text |
model |
llmModel |
No | LLM model config |
tools |
OrderedDict{String, agentTool} |
No | Available tools (deep-copied) |
messages |
Vector{agentMessage} |
No (default: empty) | Initial conversation history |
llmCall |
Function |
Yes | (messages::Dict) -> assistantMessage — invokes the LLM |
agentEventSink |
Function |
Yes | (event) -> nothing — receives tool lifecycle events |
_tool_store |
toolStore |
No | Runtime tool registry for registerTool() |
Optional hooks: prepareContext, formatMsgForLLM, beforeToolCall, afterToolCall, sessionId, maxRetryDelayMs, parallelToolExecute.
Source: type.jl:609-657, toolRegistry.jl:38-40, 191-195
Step 3: Use — Tool Execution
Tools can be used in two ways:
Direct execution (testing / standalone):
using YiemAgent.type
sig = nothing
op = x -> x # no-op partial result callback
# Execute a loaded tool directly
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
Via agent loop (production):
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
→ _agentLoop detects message → @spawn _process_message(agent)
→ prepareContext → formatMsgForLLM → llmCall
→ LLM returns tool_calls
→ executeToolCalls(context, response, tool_call_list, config, signal, emit)
→ prepareToolCall → executePreparedToolCall → finalizeExecutedToolCall
→ createToolResultMessage → batch.messages (toolResultMessage[])
Source: Direct: test/toolTest.jl:81-99 | Agent: agentCore.jl:35-311
Step 4: Extract Result
agentToolResult (raw tool output, type.jl:429-434):
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x)
result.content[1] # textContent("Current time in Tokyo: ...")
result.content[1].text # "Current time in Tokyo: 2026-08-10T..."
result.details # Dict{Any,Any}() — tool-specific metadata
result.usage # nothing — llmUsage tracking (optional)
result.terminate # false — signals loop termination
toolResultMessage (wrapped for conversation history, type.jl:152-191):
msg = batch.messages[1] # toolResultMessage
msg.toolCallId # "call-1"
msg.toolName # "getTime"
msg.content # Vector{messageContent}
msg.isError # false
msg.details # tool-specific metadata
msg.timestamp # DateTime
2. Overview
The tool system follows a three-phase pipeline per tool call:
PREPARE → EXECUTE → FINALIZE
Each phase has a single responsibility and produces an intermediate result:
| Phase | Function | Input | Output | Purpose |
|---|---|---|---|---|
| Prepare | prepareToolCall() |
agentContext, assistantMessage, agentToolCall, agentLoopConfig, abortSignal |
preparedToolCall or immediateOutcome |
Resolve tool, validate args, run pre-hook |
| Execute | executePreparedToolCall() |
preparedToolCall, abortSignal, emit |
executedOutcome |
Call tool.execute(), stream partial results |
| Finalize | finalizeExecutedToolCall() |
agentContext, assistantMessage, preparedToolCall, executedOutcome, agentLoopConfig, abortSignal |
finalizedOutcome |
Run post-hook, emit end event |
The pipeline ensures that every tool call produces a result, even on failure. Errors are captured as immediateOutcome, executedOutcome, or finalizedOutcome with isError=true, then converted to toolResultMessage objects that are fed back to the LLM conversation history.
3. Tool Definition — The agentTool Struct
Source: type.jl:261-281
Every tool is an agentTool struct with the following fields:
struct agentTool
name::String # Unique identifier (e.g. "getWeather")
label::String # Human-readable name (e.g. "Weather Lookup")
description::String # What the tool does (shown to the LLM for selection)
inputSchema::Any # JSON Schema (MCP format) describing parameters
execute::Function # Core execution: (toolCallId, args, signal, onPartialResult) -> agentToolResult
prepareArguments::Union{Function, Nothing} # Optional: (args) -> modified_args (before validation)
validateRequiredArgs::Union{Function, Nothing} # Optional: (args) -> Union{Nothing, String} error
parallelToolExecute::Bool # Override: run in parallel with other tools
end
Field Details
| Field | Required | Signature | Purpose |
|---|---|---|---|
name |
Yes | String |
Unique key for tool lookup in context.tools[name] |
label |
Yes | String |
Human-readable name for display |
description |
Yes | String |
Shown to LLM for tool selection decisions |
inputSchema |
Yes | Dict{String,Any} |
JSON Schema (MCP format) with type, properties, required |
execute |
Yes | Function |
The actual tool logic (see signature below) |
prepareArguments |
No | Function |
Transforms args before validation; (args::Dict) -> Dict |
validateRequiredArgs |
No | Function |
Custom validation; (args::Dict) -> Union{Nothing, String} |
parallelToolExecute |
No | Bool |
Default false. When true, allows parallel execution |
Execute Function Signature
execute(toolCallId::String,
args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
| Parameter | Description |
|---|---|
toolCallId |
Unique ID from the LLM's tool call (e.g., "call_abc123") |
args |
Validated arguments from the LLM, already passed through prepareArguments and validateRequiredArgs |
signal |
Optional abortSignal for cancellable operations. Check signal.aborted to abort early. |
onPartialResult |
Callback for streaming progress: onPartialResult(partial_data) emits toolExecUpdateEvent |
Returns — agentToolResult
Source: type.jl:429-434
struct agentToolResult
content::Vector{messageContent} # Output content (textContent, etc.)
details::Dict{Any,Any} # Tool-specific metadata (e.g., counts, IDs)
usage::Union{llmUsage, Nothing} # Token usage tracking (optional)
terminate::Bool # If true, signals the tool requested loop termination
end
The terminate flag is checked at the batch level. See Section 10 for details.
4. Tool Registration — Per-Agent Tool Stores
Source: toolRegistry.jl
How toolStore Works
The registry uses per-agent isolated storage via the toolStore struct. Each agent gets its own store, so tool registration is independent — registerTool(store, tool) only affects that agent's tool set.
struct toolStore
tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration
name::String # identifier for debugging/logs
end
store.tools is an OrderedDict — it provides O(1) lookup by tool name and preserves insertion order for iteration. getTools(store) returns this OrderedDict directly (not a copy), so mutations on the returned value affect the store.
How loadTools(store, dir) Works
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
Source: toolRegistry.jl:126-178
- Scans
dirfor.jlfiles (excluding files matchingregistryin name) - Sorts filenames alphabetically for deterministic registration order
- Wraps each file in a dynamically created submodule:
# For "getWeather.jl" → module _tool_getWeather module _tool_getWeather using ..type using Dates, UUIDs, DataStructures, JSON # (file contents here) end - Evaluates
getTool()within the submodule scope usingCore.eval(mod, :(getTool()))— this avoids world-age issues - Validates the return value is an
agentToolinstance - Registers the tool in
store.tools - Auto-registers
listTool(store)so the LLM can discover available tools at runtime
Why Submodules?
Each tool file is loaded into its own namespaced submodule. This means:
validateRequiredArgs,prepareArguments,executeTool, and helper functions defined ingetTime.jlare scoped under_tool_getTime- No name collisions between tools —
getTime.validateRequiredArgsis distinct fromgetWeather.validateRequiredArgs - The module reference is kept alive by the functions stored in
agentTool(closures inexecute,validateRequiredArgs,prepareArguments) so they don't get garbage collected
Registration API
# Create per-agent stores
store1 = toolStore(name="agent1")
store2 = toolStore(name="agent2")
# Load tools into specific stores (auto-registers listTools)
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
# Manual registration (per-store)
registerTool(store1, my_tool)
# Query (returns OrderedDict keyed by tool name, in registration order)
all_tools = getTools(store1) # OrderedDict{String, agentTool} — O(1) lookup + deterministic order
# Clear (per-store)
clearTools(store1) # only clears store1
getTools(store) returns the internal OrderedDict directly, giving callers:
- O(1) lookup by tool name
- Deterministic iteration order (registration order)
- Consistency with
agentState.tools(alsoOrderedDict{String, agentTool}) - No copy overhead — mutations on the returned value affect the store
Per-Agent Isolation
Each toolStore is completely independent — tools registered in one store do not appear in another:
storeA = toolStore(name="A")
storeB = toolStore(name="B")
registerTool(storeA, getTime_tool)
registerTool(storeB, getWeather_tool)
getTools(storeA) # only contains getTime
getTools(storeB) # only contains getWeather
clearTools(storeA) # storeB is unaffected
This ensures that yiemAgent instances with different tool_store references operate with completely isolated tool sets.
5. The Agent Loop — High-Level Flow
Source: agentCore.jl:35-145
The _agentLoop() function runs as a background @spawn task, created when yiemAgent is constructed.
Channel Architecture
yiemAgent struct contains:
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
- _tool_store (toolStore) ← per-agent isolated tool registry
Loop States
The loop tracks 6 states (documented at agentCore.jl:39-75):
| State | processingTask |
activeRun |
inputChannel |
followUpChannel |
Behavior |
|---|---|---|---|---|---|
| 1 | nothing |
false |
empty | empty | Idle, waiting |
| 2 | nothing |
false |
has msg | empty | New message → spawn _process_message |
| 3 | running | true |
empty | empty | Processing, no new input |
| 4 | running | true |
has msg | empty | New message while busy → queued |
| 5 | running | true |
empty | has msg | Follow-up while busy → queued |
| 6 | done | false |
empty | empty | Task completed → send result, reset |
Loop Logic (simplified)
function _agentLoop(agent::yiemAgent)
while true
# 1. Wait for message from inputChannel (blocking poll)
msg = fetch!(agent.inputChannel) # agentCore.jl:84
# 2. Handle shutdown signal
if msg === :shutdown
drain both channels, break loop
end
# 3. If agent is idle, spawn _process_message
if agent._state.activeRun == false
processingTask = Threads.@spawn _process_message(agent)
agent._state.activeRun = true
end
# 4. While processing: check for followUp messages
if istaskdone(processingTask) == false && isready(agent.followUpChannel)
drain followUpChannel → push to inputChannel
continue # wait for current processing to finish
end
# 5. When processing completes
if istaskdone(processingTask) == true
result = fetch(processingTask)
put!(agent.outputChannel, result)
agent._state.activeRun = false
processingTask = nothing
end
end
end
Key design: The loop always checks inputChannel before followUpChannel. Follow-up messages are merged into inputChannel only when the current processing task is active, ensuring they are processed after the primary message completes but before new input arrives.
6. Message Processing Pipeline
Source: agentCore.jl:175-311
_process_message(agent) is the core function that processes a batch of user messages through the LLM pipeline.
Pipeline Steps
function _process_message(agent::yiemAgent)::assistantMessage
final_response = nothing
while true # Loop until LLM returns response without tool calls
# ── Step 1: Drain inputChannel ──────────────────────────────
while isready(agent.inputChannel)
raw_msg = take!(agent.inputChannel)
if raw_msg === :shutdown
put!(agent.inputChannel, :shutdown)
break
end
user_msg = OpenAiToUserMessage(raw_msg) # Convert Dict → userMessage
push!(agent._state.messages, user_msg)
end
# ── Step 2: Prepare context ─────────────────────────────────
preparedContext = agent.prepareContext(agent._state)
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
# Override point: filter tools, inject context, modify system prompt
# ── Step 3: Format for LLM ──────────────────────────────────
formatted_messages = agent.formatMsgForLLM(preparedContext)
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
# Wraps systemPrompt as system role, converts each messageContent block
# ── Step 4: Call LLM ────────────────────────────────────────
response = agent.llmCall(formatted_messages)
# Returns assistantMessage with content::Vector{messageContent}
# Each content block has a type: "text", "thinking", or "tool_call"
# ── Step 5: Extract tool calls ──────────────────────────────
has_tool_calls, tool_call_list = extract_tool_calls(response.content)
# Inspects content blocks for "tool_calls" or "tool_call" Dict entries
# ── Step 6: Execute tool calls or return ────────────────────
if has_tool_calls && !isempty(tool_call_list)
# Build context and config
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, ...)
signal = nothing
emit = agent.agentEventSink
# Execute tool calls (sequential or parallel)
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
# Save results to conversation history
for tool_result in batch.messages
push!(agent._state.messages, tool_result)
end
# Check termination
if batch.terminate
final_response = build_final_response(batch)
break
end
# Otherwise, loop back to Step 1 (drain any new input) and call LLM again
else
# No tool calls — this is the final response
final_response = response
break
end
end
return final_response
end
Debug Note
There is a deliberate error(5555555) at agentCore.jl:214 that halts execution after the LLM call. This appears to be a debugging/staging marker. Remove or replace it before production use.
7. Tool Call Extraction from LLM Response
Source: agentCore.jl:217-245
After the LLM call, the agent inspects response.content (a Vector{messageContent}) for tool call blocks. Two formats are supported:
Format 1: OpenAI tool_calls array
# Response content block:
Dict(
:type => "tool_calls",
:tool_calls => [
Dict(:id => "call_1", :name => "getWeather", :arguments => Dict("city" => "Tokyo")),
Dict(:id => "call_2", :name => "getTime", :arguments => Dict("timezone" => "Asia/Tokyo")),
]
)
Format 2: Single tool_call block
Dict(
:type => "tool_call",
:id => "call_1",
:name => "getWeather",
:arguments => Dict("city" => "Tokyo")
)
Extraction Logic
tool_call_list = agentToolCall[]
for content_block in response.content
if content_block isa Dict
# OpenAI format: array of tool calls
if get(content_block, :type, "") == "tool_calls"
for tc_data in get(content_block, :tool_calls, [])
tc = agentToolCall(
type = "function",
id = get(tc_data, :id, string(uuid4())), # fallback UUID
name = get(tc_data, :function, Dict())[:name],
arguments = get(tc_data, :function, Dict())[:arguments],
)
push!(tool_call_list, tc)
end
# Alternative format: single tool call
elseif get(content_block, :type, "") == "tool_call"
tc_data = content_block
tc = agentToolCall(
type = "function",
id = get(tc_data, :id, string(uuid4())),
name = get(tc_data, :name, ""),
arguments = get(tc_data, :arguments, Dict{String,Any}()),
)
push!(tool_call_list, tc)
end
end
end
The agentToolCall struct is defined at type.jl:362-367:
struct agentToolCall
type::String # Always "function"
id::String # Unique tool call identifier
name::String # Tool name (matches context.tools keys)
arguments::Dict{String, Any} # Parsed tool arguments
end
8. The Per-Call Pipeline — Prepare, Execute, Finalize
This is the core of the tool execution system. Each tool call (whether part of a batch or standalone) goes through exactly three phases.
8.1 Phase 1: Prepare — prepareToolCall()
Source: agentCore.jl:511-547
function prepareToolCall(
context::agentContext,
assistantMsg::assistantMessage,
toolCall::agentToolCall,
config::agentLoopConfig,
signal::Union{Nothing, abortSignal},
)::Union{preparedToolCall, immediateOutcome}
Steps:
-
Resolve tool by name —
get(context.tools, toolCall.name, nothing)- If
nothing→immediateOutcome(createErrorToolResult("Tool X not found"), true)
- If
-
Prepare arguments —
prepareToolCallArguments(tool, toolCall)- Calls
tool.prepareArguments(toolCall.arguments)if defined - Returns the toolCall with transformed arguments
- If no hook or no change, returns original
toolCall
- Calls
-
Validate arguments —
validateToolArguments(tool, prepared)- Calls
tool.validateRequiredArgs(prepared.arguments)if defined, otherwise uses defaultvalidateRequiredArgs(prepared.arguments, tool.inputSchema) - Default validator checks
inputSchema["required"]array - On failure: throws
ArgumentError(error_string), caught by the try-catch below
- Calls
-
Run
beforeToolCallhook — ifconfig.beforeToolCall !== nothing- Passes
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context)andsignal - Hook can return
nothing(proceed), orDict(:block => true, :reason => "...")(reject) - If
signal.aborted == true→immediateOutcome(createErrorToolResult("Operation aborted"), true) - If
before.block == true→immediateOutcome(createErrorToolResult(get(before, :reason, "blocked")), true)
- Passes
-
Return success →
preparedToolCall(tool, toolCall, validatedArgs)
Source: type.jl:681-685 — preparedToolCall holds the resolved tool, original call metadata, and validated arguments together.
Key design principle: Preparation never throws. Every failure path returns an immediateOutcome with isError=true, ensuring the agent loop always has a valid result to feed back to the LLM.
8.2 Phase 2: Execute — executePreparedToolCall()
Source: agentCore.jl:589-617
function executePreparedToolCall(
prep::preparedToolCall,
signal::Union{Nothing, abortSignal},
emit::Function,
)::executedOutcome
Steps:
-
Initialize streaming state:
updateEvents = promise[] # vector to collect update event handles accepting = true # guard to prevent duplicate emissions -
Call
tool.execute():result = prep.tool.execute( prep.toolCall.id, prep.args, signal, partialResult -> begin if accepting push!(updateEvents, emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name, prep.toolCall.arguments, partialResult))) end end ) -
Wait for streaming to settle:
accepting = false wait.(updateEvents) # wait for all pending update event handlers return executedOutcome(result, false) -
On error:
catch err accepting = false wait.(updateEvents) return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) end
Streaming design: The accepting guard prevents emitting updates after the call completes. If the tool's execute function yields after emitting updates but before returning, no duplicate or stale updates are emitted.
8.3 Phase 3: Finalize — finalizeExecutedToolCall()
Source: agentCore.jl:675-706
function finalizeExecutedToolCall(
context::agentContext,
assistantMsg::assistantMessage,
prep::preparedToolCall,
executed::executedOutcome,
config::agentLoopConfig,
signal::Union{Nothing,abortSignal},
)::finalizedOutcome
Steps:
-
Extract execution result:
result = executed.result isError = executed.isError -
Run
afterToolCallhook — ifconfig.afterToolCall !== nothing:- Passes
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)andsignal - Hook can mutate the result:
after = config.afterToolCall(afterToolCallContext(...)) if after !== nothing result = merge(result, dict( :content => get(after, :content, result.content), :details => get(after, :details, result.details), :usage => get(after, :usage, result.usage), :terminate => get(after, :terminate, result.terminate) )) isError = get(after, :isError, isError) end - Common use cases: mask sensitive data, normalize usage, flip
terminatebased on business logic - On error:
result = createErrorToolResult(sprint(showerror, err)); isError = true
- Passes
-
Return:
return finalizedOutcome(prep.toolCall, result, isError)
Source: type.jl:787-791 — finalizedOutcome holds the original tool call reference, final result (post-hook), and error status.
8.4 Emission — emitToolExecutionEnd()
Source: agentCore.jl:736-739
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
finalized.result, finalized.isError))
end
This is called immediately after finalization, before building the toolResultMessage.
9. Execution Modes — Sequential vs Parallel
Source: agentCore.jl:795-936, 988-1011
Dispatcher — executeToolCalls()
function executeToolCalls(
context::agentContext,
assistantMsg::assistantMessage,
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::Union{Nothing, abortSignal},
emit::Function,
)::agentToolCallBatch
Decision logic (agentCore.jl:997-1010):
hasSequential = false
for tc in toolCalls
t = get(context.tools, tc.name, nothing)
if t !== nothing && !t.parallelToolExecute
hasSequential = true
break
end
end
if config.toolExecution == "sequential" || hasSequential
return executeToolCallsSequential(...)
else
return executeToolCallsParallel(...)
end
Rule: If the global config is "sequential" OR any tool in the batch has parallelToolExecute = false, the entire batch runs sequentially. Sequential is the safe default.
Sequential Execution — executeToolCallsSequential()
Source: agentCore.jl:795-829
function executeToolCallsSequential(...)::agentToolCallBatch
finalizedCalls = finalizedOutcome[]
messages = toolResultMessage[]
for tc in toolCalls
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
else
executed = executePreparedToolCall(prep, signal, emit)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
end
emitToolExecutionEnd(finalized, emit)
push!(messages, createToolResultMessage(finalized))
push!(finalizedCalls, finalized)
if signal !== nothing && signal.aborted
break # abort: skip remaining calls
end
end
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
end
Parallel Execution — executeToolCallsParallel()
Source: agentCore.jl:888-936
function executeToolCallsParallel(...)::agentToolCallBatch
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
for tc in toolCalls
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
emitToolExecutionEnd(finalized, emit)
push!(entries, finalized) # immediate outcome — no task
else
task = task() do
executed = executePreparedToolCall(prep, signal, emit)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
emitToolExecutionEnd(finalized, emit)
return finalized
end
schedule(task)
push!(entries, task) # pending task
end
if signal !== nothing && signal.aborted
break # abort during preparation — skip remaining
end
end
# Collect results in original order
finalizedCalls = finalizedOutcome[]
for entry in entries
outcome = entry isa task ? fetch(entry) : entry
push!(finalizedCalls, outcome)
end
messages = toolResultMessage[]
for f in finalizedCalls
push!(messages, createToolResultMessage(f))
end
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
end
Key design: All tool calls are prepared sequentially (validation, hooks), then prepared calls are executed concurrently as separate tasks. Results are collected in the original call order via fetch().
Trade-off: Parallel execution reduces wall-clock time for independent tools but can overwhelm external resources (rate limits, connection pools, disk I/O).
10. Tool Call Batches & Termination Logic
Source: type.jl:793-838
agentToolCallBatch
struct agentToolCallBatch
messages::Vector{toolResultMessage} # Tool result messages for this batch
terminate::Bool # Whether the batch should terminate the loop
end
Termination Logic — shouldTerminate()
Source: agentCore.jl:409-411
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
return !isempty(batches) && all(b -> b.result.terminate, batches)
end
Rule: terminate is true only when every tool in the batch has result.terminate == true. This prevents a single tool that sets terminate: true (e.g., for metadata purposes) from accidentally stopping the agent when other tools did not intend to terminate.
When to Set terminate = true
From the type documentation (type.jl:803-815):
| Use Case | Description |
|---|---|
| Task completion | A tool like deploy or submit finishes its work and signals the agent to stop |
| Unrecoverable error | A tool hits a fatal condition (auth token expired, database connection lost) |
| Async handoff | A tool triggers a long-running external operation; the external system will later resume via continue() |
Batch Processing in _process_message()
Source: agentCore.jl:266-307
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
# Save results to conversation history
for tool_result in batch.messages
push!(agent._state.messages, tool_result)
end
if batch.terminate
# Build final response from tool results
final_content = [textContent("Tool execution completed.")]
for tool_result in batch.messages
for content_block in tool_result.content
if content_block isa textContent
append!(final_content, [content_block])
elseif content_block isa Dict
if haskey(content_block, :text)
push!(final_content, textContent(content_block[:text]))
end
end
end
end
final_response = assistantMessage(
role = "assistant",
content = final_content,
api = response.api,
model = response.model,
usage = response.usage,
stopReason = "tool_use_terminated",
errorMessage = if any(x -> x.isError, batch.messages)
"One or more tool calls failed"
else
nothing
end,
timestamp = now(),
)
break # exit the while loop
end
# If batch.terminate == false, loop back to call LLM again with tool results
11. Tool Result Message Creation
Source: agentCore.jl:373-379
createToolResultMessage()
function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
return toolResultMessage(
"toolResult", # role
f.toolCall.id, # toolCallId
f.toolCall.name, # toolName
f.result.content, # content (Vector{messageContent})
f.result.details, # details
f.result.usage, # usage
get(f.result, :addedToolNames, string[]), # addedToolNames (for dynamic tools)
f.isError, # isError
nowMillis(), # timestamp
)
end
toolResultMessage Struct
Source: type.jl:152-191
struct toolResultMessage <: agentMessage
role::String # Always "tool"
toolCallId::String # ID matching the original tool call
toolName::String # Name of the executed tool
content::Vector{messageContent} # Tool output content
details::Any # Additional tool-specific details
usage::Union{llmUsage, Nothing} # Token usage if applicable
addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution
isError::Bool # Whether the tool call resulted in an error
timestamp::Timestamp # When the result was recorded
end
Conversation History After Tool Execution
[system] "You are a helpful assistant."
[user] "What's the weather in Tokyo?"
[assistant] {tool_calls: [getWeather(city="Tokyo")]}
[tool] tool_call_id="call_1", tool_name="getWeather", content="Weather in Tokyo: Sunny, 22°C"
On the next loop iteration, formatMsgForLLM() converts toolResultMessage to OpenAI format:
Dict(
"role" => "tool",
"tool_call_id" => "call_1",
"content" => [Dict("type" => "text", "text" => "Weather in Tokyo: Sunny, 22°C")]
)
12. Error Handling & Recovery Pattern
The framework uses a result-based error handling pattern instead of exceptions for tool call failures. This ensures the LLM always receives a tool result message, giving it the information to recover.
Error Flow
Tool call fails at any phase
│
▼
┌─────────────────────────┐
│ Phase: Prepare │ → immediateOutcome(error_result, true)
│ Phase: Execute │ → executedOutcome(error_result, true)
│ Phase: Finalize (hook) │ → finalizedOutcome(tc, error_result, true)
└─────────────────────────┘
│
▼
emitToolExecutionEnd(finalized, emit)
│
▼
createToolResultMessage(finalized)
│
▼
push!(agent._state.messages, toolResultMessage)
│
▼
formatMsgForLLM() → LLM receives error as tool result
│
▼
LLM can: retry with corrected args, report failure, or ask user for clarification
createErrorToolResult()
Source: agentCore.jl:339-341
function createErrorToolResult(msg::String)::agentToolResult
return agentToolResult([textContent("text", msg)], Dict{Any,Any}())
end
Returns an agentToolResult with a single textContent block containing the error message. This is wrapped in an immediateOutcome, executedOutcome, or finalizedOutcome depending on where the error occurred, then converted to toolResultMessage for the LLM.
Key design: Returning a result instead of throwing allows the LLM to see the error and decide whether to retry, re-issue the call with different arguments, or report failure to the user.
13. Event System — Tool Lifecycle Events
Source: type.jl:480-516
Each tool call emits a three-event lifecycle:
toolExecStartEvent → [zero or more toolExecUpdateEvent] → toolExecEndEvent
Event Types
struct toolExecStartEvent
toolCallId::String # ID of the tool call
toolName::String # Name of the tool
arguments::Dict{String, Any} # Tool arguments
end
struct toolExecUpdateEvent
toolCallId::String # ID of the tool call
toolName::String # Name of the tool
arguments::Dict{String, Any} # Tool arguments
partialResult::Any # The partial result data
end
struct toolExecEndEvent
toolCallId::String # ID of the tool call
toolName::String # Name of the tool
result::agentToolResult # The final tool result
isError::Bool # Whether execution resulted in an error
end
Event Emission Points
| Event | Emitted From | When |
|---|---|---|
toolExecStartEvent |
executeToolCalls*() loop |
Before prepareToolCall() for each tool call |
toolExecUpdateEvent |
executePreparedToolCall() |
Inside onPartialResult callback during tool.execute() |
toolExecEndEvent |
emitToolExecutionEnd() |
After finalizeExecutedToolCall() for each tool call |
Event Sink
The emit function is passed through the entire call chain:
emit = agent.agentEventSink # set during yiemAgent construction
The agentEventSink function is a user-provided callback that receives all events. This is typically used by:
- TUI (Terminal UI): Display real-time progress, tool names, results
- Logging systems: Record tool execution history
- Monitoring: Track tool usage, execution times, error rates
- Audit trails: Log all tool calls with arguments and results
14. Agent Lifecycle Hooks
Hook Types
| Hook | Signature | Called | Purpose |
|---|---|---|---|
prepareContext |
(state::agentState) -> agentContext |
Before each LLM call | Filter tools, inject context, modify system prompt |
formatMsgForLLM |
(ctx::agentContext) -> Dict |
After prepareContext |
Convert to LLM-specific format |
llmCall |
(messages::Dict) -> assistantMessage |
After formatting | Actually invoke the LLM API |
beforeToolCall |
(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict} |
In prepareToolCall |
Ask for user permission, block execution, abort |
afterToolCall |
(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict} |
In finalizeExecutedToolCall |
Mutate result, mask data, flip terminate |
agentEventSink |
(event) -> nothing |
Throughout lifecycle | Emit events for TUI, logging, monitoring |
beforeToolCall Hook
Source: agentCore.jl:530-541
if config.beforeToolCall !== nothing
before = config.beforeToolCall(
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context), signal
)
if signal !== nothing && signal.aborted
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
end
if before !== nothing && before.block
return immediateOutcome(
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
end
end
Return values:
nothing— proceed with executionDict(:block => true, :reason => "...")— block execution, error fed back to LLMDict(:block => false)— proceed (explicit allow)
afterToolCall Hook
Source: agentCore.jl:687-703
if config.afterToolCall !== nothing
try
after = config.afterToolCall(
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
)
if after !== nothing
result = merge(result, dict(
:content => get(after, :content, result.content),
:details => get(after, :details, result.details),
:usage => get(after, :usage, result.usage),
:terminate => get(after, :terminate, result.terminate)
))
isError = get(after, :isError, isError)
end
catch err
result = createErrorToolResult(sprint(showerror, err))
isError = true
end
end
Common use cases:
- Mask sensitive data from result content before the LLM sees it (e.g., removing API keys from error messages)
- Normalize usage tracking data into a consistent format
- Inspect the result and decide to flip
terminate: truebased on business logic - Wrap an error result in a friendlier message for the LLM to understand
prepareContext Hook
Source: utils.jl:111-125
function prepareContext(state::agentState)::agentContext
# TODO: filter tools from state.tools based on user intent
filteredTools = state.tools
# TODO: add filtered tools to the current system prompt / modify systemPrompt
preparedSystemPrompt = state.systemPrompt
# TODO: add system prompt, adjust/modify and inject additional context into messages
preparedMessages = deepcopy(state.messages)
return agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
end
Override points for customization:
- Filter tools based on user intent (e.g., only show "wine" tools when user asks about wine)
- Modify the system prompt dynamically (e.g., inject current time, user preferences)
- Inject additional context (e.g., retrieved documents, current user state)
- Prune or reorder messages before formatting for the LLM
formatMsgForLLM Hook
Source: utils.jl:158-219
Default implementation converts agentContext to OpenAI-compatible format:
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
messages = Vector{Dict{String, Any}}()
# System prompt as system message
if !isempty(ctx.systemPrompt)
push!(messages, Dict(
"role" => "system",
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
))
end
# Conversation messages
for msg in ctx.messages
if msg isa userMessage
push!(messages, _userMessageToOpenAI(msg))
elseif msg isa assistantMessage
push!(messages, _assistantMessageToOpenAI(msg))
elseif msg isa toolResultMessage
push!(messages, _toolResultMessageToOpenAI(msg))
end
end
return Dict("messages" => messages)
end
Override this to produce custom LLM message formats for different APIs/providers (e.g., Anthropic, Google, Ollama).
15. Self-Modifying Tools
The framework supports tools that modify the tool system itself at runtime.
writeTool — Create New Tool Files
Source: tools/writeTool.jl
writeTool is a file writer, not a code generator. The LLM provides the tool logic as executeCode (Julia code body), and writeTool wraps it in Julia boilerplate:
- Converts
inputSchemaDict intoDict{String,Any}(...)string literal - Indents
executeCodewith 4 spaces - Wraps it inside
function executeTool(...)::agentToolResult ... end - Appends
getTool()returning anagentToolstruct - Writes the combined string to
src/tools/<name>.jl
listTool — Discover Available Tools
Source: toolRegistry.jl:55-82
Each toolStore gets its own listTool instance bound to that store via listTool(store), so each agent sees only its own tools. loadTools auto-registers one, so the LLM can discover available tools at runtime. Also useful for collision detection before creating a new tool via writeTool.
Self-Tooling Workflow
1. Agent detects no existing tool handles the user's request
2. Agent calls writeTool with:
- name: "searchWine"
- label: "Wine Search"
- description: "Search a wine database..."
- inputSchema: { ... }
- executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
- (optional) validateCode, prepareCode
3. writeTool generates src/tools/searchWine.jl
4. Agent restarts (or hot-reloads) → loadTools(agent._tool_store, "src/tools") picks up the new file
5. Agent calls searchWine(query="cabernet")
6. Result: "Found 5 cabernet wines..."
writeTool Input Schema
| Field | Type | Required | Description |
|---|---|---|---|
name |
String |
Yes | Valid Julia identifier (letters, digits, underscores) |
label |
String |
Yes | Human-readable tool name |
description |
String |
Yes | What the tool does |
inputSchema |
Dict |
Yes | JSON Schema in MCP format |
executeCode |
String |
Yes | Julia code for executeTool body (NOT wrapped in function) |
validateCode |
String |
No | Custom validation Julia code |
prepareCode |
String |
No | Argument preparation code |
parallel |
Bool |
No | Whether the tool can run in parallel (default: false) |
16. Complete End-to-End Example
Full Lifecycle: User Message to Tool Result
USER SENDS MESSAGE
└─> runAgent(agent, "What's the weather in Tokyo?")
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
└─> _agentLoop: detects msg in inputChannel
└─> Threads.@spawn _process_message(agent)
── _process_message ──────────────────────────────────────────────
│
│ Step 1: Drain inputChannel
│ raw_msg = Dict("role" => "user", "content" => [...])
│ user_msg = OpenAiToUserMessage(raw_msg)
│ push!(agent._state.messages, user_msg)
│
│ Step 2: prepareContext
│ ctx = agent.prepareContext(agent._state)
│ → agentContext(systemPrompt, messages, tools)
│
│ Step 3: formatMsgForLLM
│ formatted = agent.formatMsgForLLM(ctx)
│ → Dict("messages" => [
│ Dict("role" => "system", "content" => [...]),
│ Dict("role" => "user", "content" => [...]),
│ ])
│
│ Step 4: llmCall
│ response = agent.llmCall(formatted)
│ → assistantMessage(content = [
│ Dict(:type => "tool_calls", :tool_calls => [
│ Dict(:id => "call_1", :name => "getWeather",
│ :arguments => Dict("city" => "Tokyo"))
│ ])
│ ])
│
│ Step 5: Extract tool calls
│ tool_call_list = [agentToolCall("function", "call_1", "getWeather", ...)]
│
│ Step 6: Execute tool calls
│ context = agentContext(systemPrompt, messages, tools)
│ config = agentLoopConfig(tools, beforeToolCall, afterToolCall, "sequential")
│ batch = executeToolCalls(context, response, tool_call_list, config, nothing, emit)
│
│ ── executeToolCallsSequential ──────────────────────────────
│ │
│ │ For tc = agentToolCall("call_1", "getWeather", ...):
│ │
│ │ emit(toolExecStartEvent("call_1", "getWeather", {"city": "Tokyo"}))
│ │
│ │ PREPARE:
│ │ tool = context.tools["getWeather"] → found!
│ │ validatedArgs = validateToolArguments(tool, tc)
│ │ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
│ │ beforeToolCall_hook(...) → nothing (skipped)
│ │ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
│ │
│ │ EXECUTE:
│ │ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, onPartialResult)
│ │ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
│ │ → executedOutcome(result, false)
│ │
│ │ FINALIZE:
│ │ afterToolCall_hook(...) → nothing (skipped)
│ │ → finalizedOutcome(tc, result, false)
│ │
│ │ emit(toolExecEndEvent("call_1", "getWeather", result, false))
│ │ msg = createToolResultMessage(finalized)
│ │ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
│ │
│ └─> agentToolCallBatch([msg], false)
│
│ Save results:
│ for tool_result in batch.messages
│ push!(agent._state.messages, tool_result)
│ end
│
│ batch.terminate == false → loop back to Step 1
│
LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE
── _process_message (second iteration) ───────────────────────────
│
│ Step 1: Drain inputChannel → empty
│
│ Step 2-3: prepareContext → formatMsgForLLM
│ → messages now include:
│ [system] "You are a helpful assistant."
│ [user] "What's the weather in Tokyo?"
│ [tool] tool_call_id="call_1", content="Weather in Tokyo: Sunny, 22°C"
│
│ Step 4: llmCall
│ → assistantMessage(content = [Dict(:type => "text", :text => "The weather in Tokyo is sunny, 22°C.")])
│
│ Step 5: Extract tool calls → none
│
│ Step 6: has_tool_calls == false → break, return final_response
│
└─> return final_response
AGENT LOOP: SEND RESPONSE TO USER
└─> put!(agent.outputChannel, final_response)
└─> takeResponse(agent) → assistantMessage("The weather in Tokyo is sunny, 22°C.")
17. Tool File Contract
Each .jl file in src/tools/ must conform to the following contract:
Required Function
function getTool()::agentTool
# Must return an agentTool instance
end
Optional Functions
# Argument preparation (before validation)
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
# Return modified args, or args unchanged
return args
end
# Custom validation (before execution)
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
# Return nothing to pass, or error string to fail
return nothing
end
# Core execution
function executeTool(toolCallId::String,
args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
# Return agentToolResult with content, details, usage, terminate
return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false)
end
File Structure
# src/tools/myTool.jl
using Dates # ← tool declares its own dependencies (registry injects only `using ..type`)
# Optional: helper functions
function helper_function(...)
...
end
# Optional: prepareArguments
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
return args
end
# Optional: validateRequiredArgs
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
return nothing
end
# Required: executeTool
function executeTool(toolCallId::String, args::Dict{String,Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
...
end
# Required: getTool
function getTool()::agentTool
return agentTool(
name = "myTool",
label = "My Tool",
description = "What this tool does",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(...),
"required" => [...]
),
execute = executeTool,
prepareArguments = prepareArguments,
validateRequiredArgs = validateRequiredArgs,
parallelToolExecute = false
)
end
Dependencies
Each tool file declares its own dependencies via using statements at the top of the file. The registry does not inject any standard library packages — if a tool needs Dates, JSON, HTTP, CSV, or any other package, it must include its own using statements.
# src/tools/getTime.jl
using Dates
function executeTool(...)
now() # Dates.now requires `using Dates`
end
# src/tools/myApiTool.jl
using HTTP, JSON
function executeTool(...)
response = HTTP.get("https://api.example.com")
data = JSON.parse(String(response.body))
...
end
Module Isolation
When loadTools() loads a file, it wraps it in a dynamically created submodule. The registry injects only using ..type to make core types (agentTool, textContent, agentToolResult, abortSignal, etc.) available:
# User writes in src/tools/myTool.jl:
using Dates, HTTP, JSON # ← tool's own dependencies
function getTool()::agentTool ... end
# loadTools() creates:
module _tool_myTool
using ..type # ← injected by registry (core types only)
using Dates, HTTP, JSON # ← from tool file
# (user's code here)
end
All functions in the file are scoped under _tool_myTool, preventing name collisions with other tools. The module reference is kept alive by the function objects stored in agentTool, preventing garbage collection of closures.
18. Appendix: Type Reference
Message Types
| Type | Source | Description |
|---|---|---|
messageContent |
type.jl:67 |
Abstract base for message content |
textContent |
type.jl:69 |
Plain text content (text::String) |
imageContent |
type.jl:73 |
Image content (data::String, mimeType::String) |
agentMessage |
type.jl:82 |
Abstract base for all messages |
userMessage |
type.jl:84 |
User message (role, content, timestamp) |
assistantMessage |
type.jl:111 |
LLM response (role, content, api, provider, model, usage, stopReason, errorMessage, timestamp) |
toolResultMessage |
type.jl:152 |
Tool result (role, toolCallId, toolName, content, details, usage, addedToolNames, isError, timestamp) |
Tool Types
| Type | Source | Description |
|---|---|---|
agentTool |
type.jl:261 |
Tool definition (name, label, description, schema, execute, hooks) |
agentToolCall |
type.jl:362 |
Tool call from LLM (type, id, name, arguments) |
agentToolResult |
type.jl:429 |
Tool execution result (content, details, usage, terminate) |
agentToolCallBatch |
type.jl:835 |
Batch of tool results (messages, terminate) |
Lifecycle Outcome Types
| Type | Source | Description |
|---|---|---|
preparedToolCall |
type.jl:681 |
After prepare: (tool, toolCall, args) |
immediateOutcome |
type.jl:719 |
Failed before execution: (result, isError) |
executedOutcome |
type.jl:753 |
After execute, before finalize: (result, isError) |
finalizedOutcome |
type.jl:787 |
After all phases: (toolCall, result, isError) |
Context & Config Types
| Type | Source | Description |
|---|---|---|
agentContext |
type.jl:299 |
Conversation snapshot (systemPrompt, messages, tools) |
agentState |
type.jl:310 |
Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) |
agentLoopConfig |
type.jl:403 |
Loop config (tools, beforeToolCall, afterToolCall, toolExecution) |
abortSignal |
type.jl:416 |
Abort flag (aborted::Bool) |
beforeToolCallContext |
type.jl:445 |
Context for beforeToolCall (message, toolCall, args, context) |
afterToolCallContext |
type.jl:463 |
Context for afterToolCall (message, toolCall, args, result, isError, context) |
Event Types
| Type | Source | Description |
|---|---|---|
toolExecStartEvent |
type.jl:480 |
(toolCallId, toolName, arguments) |
toolExecUpdateEvent |
type.jl:495 |
(toolCallId, toolName, arguments, partialResult) |
toolExecEndEvent |
type.jl:511 |
(toolCallId, toolName, result, isError) |
Agent Types
| Type | Source | Description |
|---|---|---|
agent |
type.jl:522 |
Abstract base type |
yiemAgent |
type.jl:527 |
High-level agent wrapper (state, channels, callbacks, task) |