This commit is contained in:
2026-08-21 13:13:38 +07:00
parent c59f6bfa61
commit da21790263
8 changed files with 276 additions and 323 deletions
+32 -32
View File
@@ -89,7 +89,7 @@ using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
# Create agent — tools are registered automatically via register_all_tools()
agent = yiemAgent(
llmCall = my_llm_call, # Function that calls the LLM API
agentEventSink = my_event_sink, # Function for TUI/logging
eventSink = my_event_sink, # Function for TUI/logging
)
```
@@ -98,7 +98,7 @@ agent = yiemAgent(
| Parameter | Type | Required | Purpose |
|-----------|------|----------|---------|
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
| `eventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
| `model` | `llmModel` | No | LLM model config |
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
@@ -179,7 +179,7 @@ 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`, `agentEventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `eventSink` | `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.
@@ -222,7 +222,7 @@ end
```julia
execute(toolCallId::String,
args::Dict{String,Any},
args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
```
@@ -430,12 +430,12 @@ function _processMessage(agent::yiemAgent)::assistantMessage
# ── Step 2: Prepare context ─────────────────────────────────
state = agentState(systemPrompt, nothing, tools, messages)
preparedContext = prepareContext(state, agentEventSink)
preparedContext = prepareContext(state, eventSink)
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
# Override point: filter tools, inject context, modify system prompt
# ── Step 3: Format for LLM ──────────────────────────────────
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
formattedMessages = formatMsgForLLM(preparedContext, eventSink)
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
# Wraps systemPrompt as system role, converts each messageContent block
@@ -456,7 +456,7 @@ function _processMessage(agent::yiemAgent)::assistantMessage
signal = abortSignal(false)
# Execute tool calls (sequential or parallel)
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, agentEventSink)
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, eventSink)
# Save results to conversation history
for tool_result in batch.messages
@@ -611,7 +611,7 @@ function prepareToolCall(
function executePreparedToolCall(
prep::preparedToolCall,
signal::Union{Nothing, abortSignal},
agentEventSink,
eventSink,
)::executedOutcome
```
@@ -623,7 +623,7 @@ function executePreparedToolCall(
prep.toolCall.id,
prep.args,
signal,
agentEventSink # serves as onPartialResult callback
eventSink # serves as onPartialResult callback
)
return executedOutcome(result, false)
```
@@ -698,7 +698,7 @@ function executeToolCalls(
toolCalls::Vector{agentToolCall},
config::agentLoopConfig,
signal::Union{Nothing, abortSignal},
agentEventSink,
eventSink,
)::agentToolCallBatch
```
@@ -733,12 +733,12 @@ function executeToolCallsSequential(...)::agentToolCallBatch
messages = toolResultMessage[]
for tc in toolCalls
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
else
executed = executePreparedToolCall(prep, signal, agentEventSink)
executed = executePreparedToolCall(prep, signal, eventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
end
@@ -763,14 +763,14 @@ function executeToolCallsParallel(...)::agentToolCallBatch
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
for tc in toolCalls
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
if prep isa immediateOutcome
finalized = finalizedOutcome(tc, prep.result, prep.isError)
push!(entries, finalized) # immediate outcome — no task
else
task = task() do
executed = executePreparedToolCall(prep, signal, agentEventSink)
executed = executePreparedToolCall(prep, signal, eventSink)
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
return finalized
end
@@ -845,7 +845,7 @@ From the type documentation (`type.jl:803-815`):
**Source:** `agentCore.jl:266-307`
```julia
batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
# Save results to conversation history
for tool_result in batch.messages
@@ -1037,13 +1037,13 @@ end
### Event Sink
The `agentEventSink` function is passed through the entire call chain:
The `eventSink` function is passed through the entire call chain:
```julia
agentEventSink = agent.agentEventSink # set during yiemAgent construction
eventSink = agent.eventSink # set during yiemAgent construction
```
The `agentEventSink` function is a user-provided callback that receives all events. This is typically used by:
The `eventSink` 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
@@ -1057,12 +1057,12 @@ The `agentEventSink` function is a user-provided callback that receives all even
| Hook | Signature | Called | Purpose |
|------|-----------|--------|---------|
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> 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 |
| `eventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring |
### `beforeToolCall` Hook
@@ -1125,7 +1125,7 @@ end
**Source:** `utils.jl:111-125`
```julia
function prepareContext(state::agentState, agentEventSink)::agentContext
function prepareContext(state::agentState, eventSink)::agentContext
# TODO: filter tools from state.tools based on user intent
filteredTools = state.tools
@@ -1152,7 +1152,7 @@ end
Default implementation converts `agentContext` to OpenAI-compatible format:
```julia
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any}
messages = Vector{Dict{String, Any}}()
# System prompt as system message
@@ -1284,7 +1284,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
│ Step 6: Execute tool calls
│ context = agentContext(systemPrompt, messages, tools)
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink)
LOOP ITERATION 1 — executeToolCallsSequential
@@ -1299,7 +1299,7 @@ LOOP ITERATION 1 — executeToolCallsSequential
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
│ EXECUTE:
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, eventSink)
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
│ → executedOutcome(result, false)
@@ -1365,20 +1365,20 @@ end
```julia
# Argument preparation (before validation)
function <name>PrepareArguments(args::Dict{String,Any})::Dict{String,Any}
function <name>PrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
# Return modified args, or args unchanged
return args
end
# Custom validation (before execution)
function <name>ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
function <name>ValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
# Return nothing to pass, or error string to fail
return nothing
end
# Core execution
function <name>Execute(toolCallId::String,
args::Dict{String,Any},
args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
# Return agentToolResult with content, details, usage, terminate
@@ -1400,17 +1400,17 @@ function helper_function(...)
end
# Optional: prepareArguments
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
function myToolPrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any}
return args
end
# Optional: validateRequiredArgs
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
function myToolValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String}
return nothing
end
# Required: execute function
function myToolExecute(toolCallId::String, args::Dict{String,Any},
function myToolExecute(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal},
onPartialResult::Function)::agentToolResult
...
@@ -1481,7 +1481,7 @@ To add a new tool (e.g., `searchWine.jl`):
using .type
# using AdditionalPkg # add if needed
function searchWineExecute(toolCallId::String, args::Dict{String,Any},
function searchWineExecute(toolCallId::String, args::AbstractDict{String, Any},
signal::Union{Nothing,abortSignal}, onPartialResult)
query = get(args, "query", "")
result = search_wine_db(query)