update
This commit is contained in:
+176
-343
@@ -2,43 +2,6 @@
|
||||
|
||||
Tools allow the agent to perform actions and fetch data. Each tool defines a **schema** (what arguments it accepts) and an **execution function** (what it does).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add a new tool by creating a `.jl` file in `src/tools/`. The file must define a `getTool()` function that returns an `agentTool`:
|
||||
|
||||
```julia
|
||||
# src/tools/my_tool.jl
|
||||
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
|
||||
city = args["city"]
|
||||
return agentToolResult(
|
||||
[textContent("Hello from $(city)!")],
|
||||
Dict{Any,Any}(), nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
function getTool()::agentTool
|
||||
return agentTool(
|
||||
name = "my_tool",
|
||||
label = "My Tool",
|
||||
description = "Says hello to a city.",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string", "description" => "City name")
|
||||
),
|
||||
"required" => ["city"]
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
When `loadTools()` or `registerTool()` is called, the tool becomes available to the agent.
|
||||
|
||||
## Tool Anatomy
|
||||
|
||||
Each tool has 3 main parts:
|
||||
@@ -155,39 +118,94 @@ end
|
||||
|
||||
Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments.
|
||||
|
||||
## Tool Lifecycle — Framework Internals
|
||||
## Tool Discovery and Lifecycle
|
||||
|
||||
This section traces the full code path from the moment the LLM returns tool calls to the final result being fed back into the conversation. All code references are to `agentCore.jl`.
|
||||
The agent iterates through tools via a **discover → execute → loop** cycle. Here is the complete flow from the framework author's perspective:
|
||||
|
||||
### Phase 1: Detect Tool Calls in LLM Response
|
||||
|
||||
After the LLM returns an `assistantMessage`, the loop at `agentCore.jl:220-244` inspects each `content` block:
|
||||
### The Agent Loop
|
||||
|
||||
```julia
|
||||
# agentCore.jl:217-244
|
||||
has_tool_calls = false
|
||||
# agentCore.jl:175 - _process_message()
|
||||
while true
|
||||
# 1. Drain messages from inputChannel
|
||||
while isready(agent.inputChannel)
|
||||
raw_msg = take!(agent.inputChannel)
|
||||
user_msg = OpenAiToUserMessage(raw_msg)
|
||||
push!(agent._state.messages, user_msg)
|
||||
end
|
||||
|
||||
# 2. Format messages for LLM
|
||||
ctx = agent.prepareContext(agent._state)
|
||||
formatted = agent.formatMsgForLLM(ctx)
|
||||
|
||||
# 3. Call LLM
|
||||
response = agent.llmCall(formatted)
|
||||
|
||||
# 4. Check if LLM used tool calls
|
||||
if has_tool_calls(response.content)
|
||||
# 5. Execute tools, feed results back to LLM, loop
|
||||
else
|
||||
# 6. No tool calls — return final response
|
||||
break
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 1: Tool Discovery
|
||||
|
||||
Tools are discovered from `agent._state.tools`, which is a `Vector{agentTool}` populated during agent creation:
|
||||
|
||||
```julia
|
||||
# Loading tools
|
||||
tools = loadTools("src/tools") # returns Vector{agentTool}
|
||||
|
||||
# Passing to agent
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "...",
|
||||
tools = tools, # ← tools stored in agent._state.tools
|
||||
llmCall = my_llm_call,
|
||||
agentEventSink = my_event_sink,
|
||||
)
|
||||
```
|
||||
|
||||
When the LLM response contains tool calls, the agent builds an `agentContext` with those tools:
|
||||
|
||||
```julia
|
||||
context = agentContext(
|
||||
agent._state.systemPrompt,
|
||||
agent._state.messages,
|
||||
agent._state.tools, # ← tools available for discovery
|
||||
)
|
||||
```
|
||||
|
||||
### Step 2: Extract Tool Calls from LLM Response
|
||||
|
||||
The agent inspects the `response.content` blocks for `tool_calls`:
|
||||
|
||||
```julia
|
||||
# agentCore.jl:217-245
|
||||
tool_call_list = agentToolCall[]
|
||||
|
||||
for content_block in response.content
|
||||
if content_block isa Dict
|
||||
# OpenAI-style: type == "tool_calls" with array of tool calls
|
||||
if get(content_block, :type, "") == "tool_calls"
|
||||
# OpenAI format: {"type": "tool_calls", "tool_calls": [...]}
|
||||
for tc_data in get(content_block, :tool_calls, [])
|
||||
tc = agentToolCall(
|
||||
type="function",
|
||||
id=get(tc_data, :id, string(uuid4())),
|
||||
name=get(tc_data, :function, Dict{String,Any}())[:name],
|
||||
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
|
||||
type = "function",
|
||||
id = get(tc_data, :id, string(uuid4())),
|
||||
name = get(tc_data, :function, Dict())[:name],
|
||||
arguments = get(tc_data, :function, Dict())[:arguments],
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
end
|
||||
# Alternative style: type == "tool_call" single dict per block
|
||||
elseif get(content_block, :type, "") == "tool_call"
|
||||
# Alternative format: single tool_call 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}()),
|
||||
type = "function",
|
||||
id = get(content_block, :id, string(uuid4())),
|
||||
name = get(content_block, :name, ""),
|
||||
arguments = get(content_block, :arguments, Dict()),
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
end
|
||||
@@ -195,180 +213,82 @@ for content_block in response.content
|
||||
end
|
||||
```
|
||||
|
||||
Each content block with `type == "tool_calls"` or `type == "tool_call"` extracts an `agentToolCall` (id, name, arguments dict) and collects them into a `Vector{agentToolCall}`.
|
||||
### Step 3: Execute Each Tool Call
|
||||
|
||||
### Phase 2: Dispatch to Sequential or Parallel Execution
|
||||
|
||||
At `agentCore.jl:247`, the framework checks if any tool calls exist and decides execution mode:
|
||||
For each tool call, the agent runs through the **prepare → execute → finalize** pipeline:
|
||||
|
||||
```julia
|
||||
# agentCore.jl:247-265
|
||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||
config = agentLoopConfig(
|
||||
agent._state.tools,
|
||||
agent.beforeToolCall,
|
||||
agent.afterToolCall,
|
||||
agent.parallelToolExecute ? "parallel" : "sequential",
|
||||
)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
```
|
||||
# agentCore.jl:247-302
|
||||
if has_tool_calls && length(tool_call_list) > 0
|
||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, execution_mode)
|
||||
signal = nothing
|
||||
emit = agent.agentEventSink
|
||||
|
||||
`executeToolCalls` (`agentCore.jl:988-1015`) checks:
|
||||
- `config.toolExecution == "sequential"` → sequential mode
|
||||
- Any tool has `parallelToolExecute == false` → sequential mode
|
||||
- Otherwise → parallel mode
|
||||
# Execute all tool calls (sequential or parallel)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
|
||||
### Phase 3: Per-Call Preparation (`prepareToolCall`)
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
push!(agent._state.messages, tool_result)
|
||||
end
|
||||
|
||||
Each tool call goes through `prepareToolCall` (`agentCore.jl:511-547`):
|
||||
|
||||
```
|
||||
1. Look up tool by name: find(t -> t.name == tc.name, context.tools)
|
||||
2. If not found → immediateOutcome("Tool X not found", true)
|
||||
3. Run tool.prepareArguments (if defined) → transforms raw LLM args
|
||||
4. Run validateToolArguments → validateRequiredArgs (hook or default)
|
||||
→ if fails → throws ArgumentError → caught below
|
||||
5. Run beforeToolCall hook (if defined) → can block execution
|
||||
→ if blocked → immediateOutcome("Tool execution was blocked", true)
|
||||
6. Return preparedToolCall(tool, tc, validatedArgs)
|
||||
```
|
||||
|
||||
If any step throws (validation, prepareArguments, beforeToolCall), the catch block at `agentCore.jl:545` converts it to an `immediateOutcome`:
|
||||
|
||||
```julia
|
||||
catch err
|
||||
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
# If any tool requested termination, break the loop
|
||||
if batch.terminate
|
||||
final_response = build_final_response(batch)
|
||||
break
|
||||
end
|
||||
# Otherwise, loop back to step 2 (format + call LLM again)
|
||||
end
|
||||
```
|
||||
|
||||
### Phase 4: Execution (`executePreparedToolCall`)
|
||||
### Step 4: The Per-Call Pipeline
|
||||
|
||||
For each `preparedToolCall`, `executePreparedToolCall` (`agentCore.jl:589-617`) runs:
|
||||
|
||||
```julia
|
||||
function executePreparedToolCall(prep::preparedToolCall, signal, emit)::executedOutcome
|
||||
updateEvents = promise[]
|
||||
accepting = true
|
||||
|
||||
try
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id, prep.args, signal,
|
||||
partialResult -> begin
|
||||
if accepting
|
||||
push!(updateEvents, emit(toolExecUpdateEvent(..., partialResult)))
|
||||
end
|
||||
end
|
||||
)
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(result, false)
|
||||
catch err
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- Calls `tool.execute(id, args, signal, onPartialResult)` — your tool's `executeTool` function
|
||||
- `signal` can be checked inside `executeTool` for cancellation
|
||||
- `onPartialResult` is called for streaming updates, which are emitted as `toolExecutionUpdate` events
|
||||
- `accepting` guard prevents emitting updates after the result is already captured
|
||||
- `wait.(updateEvents)` ensures all streaming updates are delivered before returning
|
||||
- Execution errors are caught and returned as `executedOutcome(isError=true)` — never thrown
|
||||
|
||||
### Phase 5: Finalization (`finalizeExecutedToolCall`)
|
||||
|
||||
After execution, `finalizeExecutedToolCall` (`agentCore.jl:675-706`) runs the `afterToolCall` hook:
|
||||
|
||||
```julia
|
||||
function finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)::finalizedOutcome
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
|
||||
if config.afterToolCall !== nothing
|
||||
try
|
||||
after = config.afterToolCall(afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal)
|
||||
if after !== nothing
|
||||
# Hook can mutate: content, details, usage, terminate, isError
|
||||
result = merge(result, dict(...))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
catch err
|
||||
result = createErrorToolResult(sprint(showerror, err))
|
||||
isError = true
|
||||
end
|
||||
end
|
||||
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
end
|
||||
```
|
||||
|
||||
The hook can:
|
||||
- Mask sensitive data from result content
|
||||
- Normalize usage tracking
|
||||
- Flip `terminate: true` based on business logic
|
||||
- Wrap errors in friendlier messages for the LLM
|
||||
|
||||
If the hook itself throws, the error is caught and converted to an error outcome.
|
||||
|
||||
### Phase 6: Emit Events and Create Result Message
|
||||
|
||||
Each call emits `toolExecutionEnd`:
|
||||
|
||||
```julia
|
||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError))
|
||||
end
|
||||
```
|
||||
|
||||
Then creates the `toolResultMessage` for conversation history (`agentCore.jl:373-379`):
|
||||
|
||||
```julia
|
||||
function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
return toolResultMessage(
|
||||
"toolResult", f.toolCall.id, f.toolCall.name,
|
||||
f.result.content, f.result.details, f.result.usage,
|
||||
get(f.result, :addedToolNames, string[]), f.isError, nowMillis()
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Phase 7: Batch Assembly and Loop Control
|
||||
|
||||
In `executeToolCallsSequential` (`agentCore.jl:795-829`) or `executeToolCallsParallel` (`agentCore.jl:888-936`), all results are collected:
|
||||
|
||||
```julia
|
||||
messages = toolResultMessage[]
|
||||
for finalized in finalizedCalls
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
end
|
||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||
```
|
||||
|
||||
`shouldTerminate` (`agentCore.jl:409`) returns `true` only if ALL tools in the batch set `result.terminate == true`. If `false`, the agent loop at `agentCore.jl:176-308` feeds the tool results back to the LLM for another turn.
|
||||
|
||||
### Data Flow Summary
|
||||
Each tool call goes through three phases:
|
||||
|
||||
```
|
||||
response.content (Vector{Any})
|
||||
└── phase 1: parse content blocks
|
||||
└── tool_call_list :: Vector{agentToolCall}
|
||||
└── phase 2: dispatch to sequential/parallel
|
||||
└── phase 3: prepareToolCall
|
||||
└── preparedToolCall or immediateOutcome
|
||||
└── phase 4: executePreparedToolCall
|
||||
└── executedOutcome
|
||||
└── phase 5: finalizeExecutedToolCall
|
||||
└── finalizedOutcome
|
||||
└── phase 6: createToolResultMessage
|
||||
└── toolResultMessage
|
||||
└── phase 7: agentToolCallBatch
|
||||
└── pushed to agent._state.messages
|
||||
└── loop back to LLM
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PREPARE → prepareToolCall() │
|
||||
│ │
|
||||
│ 1. Find tool by name in context.tools │
|
||||
│ 2. Transform args via tool.prepareArguments (if defined) │
|
||||
│ 3. Validate via tool.validateRequiredArgs (or default) │
|
||||
│ 4. Run beforeToolCall hook (if defined) │
|
||||
│ └── on any failure → return immediateOutcome (skip execution) │
|
||||
│ └── success → return preparedToolCall │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ EXECUTE → executePreparedToolCall() │
|
||||
│ │
|
||||
│ 1. emit toolExecutionStart event │
|
||||
│ 2. call tool.execute(toolCallId, args, signal, onPartialResult)│
|
||||
│ 3. wait for all pending update events │
|
||||
│ └── on error → return executedOutcome(isError=true) │
|
||||
│ └── success → return executedOutcome(isError=false) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ FINALIZE → finalizeExecutedToolCall() │
|
||||
│ │
|
||||
│ 1. Run afterToolCall hook (if defined) │
|
||||
│ - can mutate content, details, usage, terminate, isError │
|
||||
│ 2. emit toolExecutionEnd event │
|
||||
│ 3. createToolResultMessage → adds to conversation history │
|
||||
│ └── return finalizedOutcome │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Step 5: Feed Results Back to LLM
|
||||
|
||||
Tool results are added to `agent._state.messages` as `toolResultMessage` objects. On the next loop iteration, `formatMsgForLLM()` converts them to OpenAI format and the LLM receives the results:
|
||||
|
||||
```
|
||||
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"
|
||||
```
|
||||
|
||||
The LLM then decides: call another tool, or return a final text answer.
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Sequential
|
||||
@@ -424,124 +344,53 @@ tool = getTool() # from your tool module
|
||||
registerTool(tool)
|
||||
```
|
||||
|
||||
## Using Tools with an Agent
|
||||
|
||||
Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow:
|
||||
## Complete Lifecycle Example
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# 1. Load tools from the tools directory
|
||||
tools = loadTools("src/tools")
|
||||
# [toolRegistry] Loading tool from: src/tools/getTime.jl
|
||||
# [toolRegistry] Loaded tool: getTime — Time Lookup
|
||||
# [toolRegistry] Loading tool from: src/tools/getWeather.jl
|
||||
# [toolRegistry] Loaded tool: getWeather — Weather Lookup
|
||||
|
||||
# 2. Define your LLM call function
|
||||
function my_llm_call(messages::Dict)::assistantMessage
|
||||
# Call your LLM API here (OpenAI, Anthropic, local model, etc.)
|
||||
# Return an assistantMessage with the response content
|
||||
# If the LLM wants to call a tool, include tool_call content blocks
|
||||
...
|
||||
end
|
||||
|
||||
# 3. Define your event sink (optional, for logging/debugging)
|
||||
function my_event_sink(event)
|
||||
if event isa toolExecStartEvent
|
||||
println("[EVENT] Tool start: $(event.toolName)")
|
||||
elseif event isa toolExecEndEvent
|
||||
status = event.isError ? "ERROR" : "OK"
|
||||
println("[EVENT] Tool end: $(event.toolName) — $status")
|
||||
end
|
||||
end
|
||||
|
||||
# 4. Create the agent with tools
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant that can check weather and time.",
|
||||
model = my_model,
|
||||
tools = tools, # pass loaded tools
|
||||
llmCall = my_llm_call, # your LLM function
|
||||
agentEventSink = my_event_sink, # event handler
|
||||
)
|
||||
|
||||
# 5. Send a message and get a response
|
||||
# ─── USER SENDS MESSAGE ───────────────────────────────────────────
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
# response.content contains the LLM's reply (with tool results if applicable)
|
||||
# ─── LOOP ITERATION 1 ─────────────────────────────────────────────
|
||||
# Agent formats messages and calls LLM
|
||||
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
|
||||
response = agent.llmCall(formatted)
|
||||
# LLM returns: {"content": [{"type": "tool_calls", "tool_calls": [{"name": "getWeather", "arguments": {"city": "Tokyo"}}]}]}
|
||||
|
||||
# Agent extracts tool call, builds context
|
||||
context = agentContext(systemPrompt, messages, agent._state.tools)
|
||||
tool_call_list = [agentToolCall("call_1", "getWeather", Dict("city" => "Tokyo"))]
|
||||
|
||||
# PREPARE: find tool, validate args
|
||||
tool = find(t -> t.name == "getWeather", context.tools) # found!
|
||||
validateRequiredArgs(Dict("city" => "Tokyo"), tool.inputSchema) # passes
|
||||
beforeToolCall_hook(agentMsgCtx, nothing) # nil, skipped
|
||||
|
||||
# EXECUTE: call tool.execute()
|
||||
result = tool.execute("call_1", Dict("city" => "Tokyo"), nothing, onPartialResult)
|
||||
# Returns: agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], Dict(), nothing, false)
|
||||
|
||||
# FINALIZE: afterToolCall hook, emit events
|
||||
finalized = finalizedOutcome(tc, result, false)
|
||||
emit(toolExecEndEvent("call_1", "getWeather", result, false))
|
||||
msg = createToolResultMessage(finalized) # toolResultMessage for conversation history
|
||||
|
||||
# Add result to conversation
|
||||
push!(agent._state.messages, msg)
|
||||
# Messages now: [user: "What's the weather?", assistant: {tool_calls: getWeather}, tool: "Sunny, 22°C"]
|
||||
|
||||
# ─── LOOP ITERATION 2 ─────────────────────────────────────────────
|
||||
# LLM called again with tool result included
|
||||
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
|
||||
response = agent.llmCall(formatted)
|
||||
# LLM returns: {"content": [{"type": "text", "text": "The weather in Tokyo is sunny, 22°C."}]}
|
||||
|
||||
# No tool calls detected → break loop, return final response
|
||||
return assistantMessage(content=[textContent("The weather in Tokyo is sunny, 22°C.")], ...)
|
||||
|
||||
# ─── USER RECEIVES RESPONSE ───────────────────────────────────────
|
||||
response = take_response(agent)
|
||||
println(response.content)
|
||||
|
||||
# 6. When done, stop the agent
|
||||
stop_agent(agent)
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`.
|
||||
|
||||
2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`.
|
||||
|
||||
3. **LLM is called** via `agent.llmCall(formatted_messages)`. The LLM sees the system prompt, conversation history, and the tool definitions in the prompt (via `formatMsgForLLM`).
|
||||
|
||||
4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent:
|
||||
- Extracts each tool call (name, arguments)
|
||||
- Runs validation (`validateRequiredArgs` or default)
|
||||
- Executes the tool (or returns an error if validation fails)
|
||||
- Feeds the result back as a `toolResultMessage` in the conversation
|
||||
|
||||
5. **LLM is called again** with the tool results. This repeats until the LLM returns a text response with no tool calls.
|
||||
|
||||
6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`.
|
||||
|
||||
### Minimal Working Example
|
||||
|
||||
```julia
|
||||
using .YiemAgent
|
||||
using .toolRegistry
|
||||
|
||||
# Load tools
|
||||
tools = loadTools("src/tools")
|
||||
|
||||
# Mock LLM that echoes back a tool call, then a text response
|
||||
call_count = 0
|
||||
function mock_llm_call(messages::Dict)::assistantMessage
|
||||
global call_count += 1
|
||||
if call_count == 1
|
||||
# First call: LLM decides to use getWeather
|
||||
return assistantMessage(
|
||||
content=[
|
||||
Dict("type" => "tool_calls",
|
||||
"tool_calls" => [Dict("id" => "call_1", "name" => "getWeather",
|
||||
"arguments" => Dict("city" => "Tokyo"))])
|
||||
],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
else
|
||||
# Second call: LLM returns text (after tool result)
|
||||
return assistantMessage(
|
||||
content = [textContent("The weather in Tokyo is sunny, 22°C.")],
|
||||
model = "mock",
|
||||
usage = llmUsage(0, 0)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Create agent
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
tools = tools,
|
||||
llmCall = mock_llm_call,
|
||||
agentEventSink = e -> nothing, # no events
|
||||
)
|
||||
|
||||
# Run
|
||||
run_agent(agent, "What's the weather in Tokyo?")
|
||||
response = take_response(agent)
|
||||
|
||||
stop_agent(agent)
|
||||
# => "[textContent(\"The weather in Tokyo is sunny, 22°C.\")]"
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
@@ -550,19 +399,3 @@ stop_agent(agent)
|
||||
|---|---|---|
|
||||
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
|
||||
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
|
||||
|
||||
## Example: Error Flow
|
||||
|
||||
When the LLM calls a tool with invalid arguments:
|
||||
|
||||
```
|
||||
User: "What's the weather?"
|
||||
└── LLM: call getWeather() with no arguments
|
||||
└── prepareToolCall → validateRequiredArgs → "Missing required arguments: city"
|
||||
└── immediateOutcome → error tool result
|
||||
└── LLM sees: "Missing required arguments: city"
|
||||
└── LLM retries: call getWeather(city="Tokyo")
|
||||
└── executeTool → "Weather in Tokyo: Sunny, 22°C"
|
||||
```
|
||||
|
||||
The agent feeds the error back to the LLM as a tool result message, allowing it to self-correct.
|
||||
|
||||
Reference in New Issue
Block a user