update
This commit is contained in:
+207
-22
@@ -155,33 +155,218 @@ 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.
|
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 Call Lifecycle
|
## Tool Lifecycle — Framework Internals
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# agentCore.jl:217-244
|
||||||
|
has_tool_calls = false
|
||||||
|
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"
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
push!(tool_call_list, tc)
|
||||||
|
end
|
||||||
|
# Alternative style: type == "tool_call" single dict per block
|
||||||
|
elseif get(content_block, :type, "") == "tool_call"
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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}`.
|
||||||
|
|
||||||
|
### Phase 2: Dispatch to Sequential or Parallel Execution
|
||||||
|
|
||||||
|
At `agentCore.jl:247`, the framework checks if any tool calls exist and decides execution mode:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
`executeToolCalls` (`agentCore.jl:988-1015`) checks:
|
||||||
|
- `config.toolExecution == "sequential"` → sequential mode
|
||||||
|
- Any tool has `parallelToolExecute == false` → sequential mode
|
||||||
|
- Otherwise → parallel mode
|
||||||
|
|
||||||
|
### Phase 3: Per-Call Preparation (`prepareToolCall`)
|
||||||
|
|
||||||
|
Each tool call goes through `prepareToolCall` (`agentCore.jl:511-547`):
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM requests tool call
|
1. Look up tool by name: find(t -> t.name == tc.name, context.tools)
|
||||||
└── prepareToolCall (agentCore.jl:511)
|
2. If not found → immediateOutcome("Tool X not found", true)
|
||||||
├── Tool lookup by name
|
3. Run tool.prepareArguments (if defined) → transforms raw LLM args
|
||||||
├── prepareArguments (tool-specific transform, if defined)
|
4. Run validateToolArguments → validateRequiredArgs (hook or default)
|
||||||
├── validateToolArguments (validateRequiredArgs hook or default)
|
→ if fails → throws ArgumentError → caught below
|
||||||
│ └── on failure → immediateOutcome (no execution)
|
5. Run beforeToolCall hook (if defined) → can block execution
|
||||||
├── beforeToolCall hook (if defined)
|
→ if blocked → immediateOutcome("Tool execution was blocked", true)
|
||||||
│ └── on block → immediateOutcome (no execution)
|
6. Return preparedToolCall(tool, tc, validatedArgs)
|
||||||
└── returns preparedToolCall
|
```
|
||||||
|
|
||||||
executed by executeToolCallsSequential or executeToolCallsParallel
|
If any step throws (validation, prepareArguments, beforeToolCall), the catch block at `agentCore.jl:545` converts it to an `immediateOutcome`:
|
||||||
└── executePreparedToolCall (agentCore.jl:589)
|
|
||||||
├── emit toolExecutionStart
|
|
||||||
├── call tool.execute()
|
|
||||||
│ └── on error → executedOutcome(isError=true)
|
|
||||||
└── returns executedOutcome
|
|
||||||
|
|
||||||
finalizeExecutedToolCall (agentCore.jl:675)
|
```julia
|
||||||
├── afterToolCall hook (if defined)
|
catch err
|
||||||
│ └── can mutate result content, usage, terminate, isError
|
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||||
└── returns finalizedOutcome
|
end
|
||||||
|
```
|
||||||
|
|
||||||
emit toolExecutionEnd
|
### Phase 4: Execution (`executePreparedToolCall`)
|
||||||
└── createToolResultMessage → added to conversation history
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Execution Modes
|
## Execution Modes
|
||||||
|
|||||||
Reference in New Issue
Block a user