This commit is contained in:
2026-08-11 16:43:48 +07:00
parent 5a27630ccf
commit 89885c1583
5 changed files with 206 additions and 185 deletions
+4 -3
View File
@@ -526,10 +526,11 @@ function prepareToolCall(
prepared = prepareToolCallArguments(tool, toolCall)
validatedArgs = validateToolArguments(tool, prepared)
# 2. beforeToolCall hook — can block
#WORKING 2. beforeToolCall hook — can block
if config.beforeToolCall !== nothing
before = config.beforeToolCall(
assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
signal
)
if signal !== nothing && signal.aborted
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
@@ -687,7 +688,7 @@ function finalizeExecutedToolCall(
if config.afterToolCall !== nothing
try
after = config.afterToolCall(
afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
)
if after !== nothing
result = merge(result, dict(:content=>get(after,:content,result.content),
+142 -1
View File
@@ -5,11 +5,152 @@ export prompt
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames
using GeneralUtils
using ..type, ..utils
using ..type, ..utils, ..toolRegistry
# ---------------------------------------------- 100 --------------------------------------------- #
"""
docstring
"""
mutable struct yiemAgent <: agent # High-level agent wrapper
_state::agentState # Current state (prompt, model, messages, tools, etc.)
# user sends prompt message to agent. if agent is idle, it process user message right away.
# if agent is running, it process user message after the current tool call finished.
inputChannel::Channel
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
# messages are handled and the agent is idle (not using a tool call).
followUpChannel::Channel
# agent sends response message to user after processing all user messages in inputChannel
# and all followUp messages.
outputChannel::Channel
_agent_loop::Union{Task, Nothing} # agent loop running in the background
# Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
# reorder, ...) for a single LLM call in _process_message()'s loop.
# returns new Vector{agentMessage}
prepareContext::Union{Function, Nothing}
# Convert prepareContext()'s new Vector{agentMessage} to LLM message format
formatMsgForLLM::Function
# Actually invoke the LLM to get a completion response. The LLM response comes back as an
# assistantMessage whose content is an array of content blocks.
# Each block has a type — "text", "thinking", or "toolCall".
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
llmCall::Function
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
beforeToolCall::Union{Function, Nothing}
executeToolCalls::Function # execute tool calls ()
# Callback invoked after executing a tool call to sanitize tools output so the output is ready
# to be converted into toolResults message
afterToolCall::Union{Function, Nothing}
# prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
sessionId::Union{String, Nothing} # Optional session identifier
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false
agentEventSink::Function # agent emits its status via this function
_tool_store::Any # Reference to the toolStore for runtime registration
end
"""
Create a new yiemAgent instance with a background loop task.
Spawns a background `@spawn` task that runs the agent loop, listening
on `inputChannel` and `followUpChannel` channels concurrently.
# Keyword Arguments
- `systemPrompt::String`: System prompt for the agent
- `model`: LLM model to use
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
- `llmCall::Function`: Function to invoke the LLM (required)
- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`)
- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`)
- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`)
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
- `agentEventSink::Function`: Callback to receive agent events
- `tool_store::Union{Any, Nothing}`: toolStore for runtime tool registration (default: `nothing`)
# Returns
- A new `yiemAgent` instance with an active background task
# Examples
```julia
julia> store = toolStore(name="agent1")
julia> tools = loadTools(store, "src/tools")
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
"""
function yiemAgent(
toolsFolderPath::String,
llmCall::Function,
;
systemPrompt::String="You are helpful assistant.",
model=nothing,
messages::Vector{agentMessage}=agentMessage[],
prepareContext::Function=prepareContext,
formatMsgForLLM::Function=formatMsgForLLM,
beforeToolCall::Function,
afterToolCall::Function,
# prepareNextTurn::Union{Function, Nothing}=nothing,
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
sessionId::Union{String, Nothing}=nothing,
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false,
agentEventSink::Function,
tool_store::Union{Any, Nothing}=nothing,
)
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
inputChannel = Channel(16)
followUp = Channel(32)
outputChannel = Channel(16)
# load tools from toolsFolderPath
toolStore = YiemAgent.toolStore(name="myagent")
loadTools(toolStore, toolsFolderPath)
# Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent(
agentState(systemPrompt, model, getTools(toolStore), messages),
inputChannel,
followUp,
outputChannel,
nothing, # placeholder — replaced below
prepareContext,
formatMsgForLLM,
llmCall,
beforeToolCall,
afterToolCall,
# prepareNextTurn,
# prepareNextTurnWithContext,
sessionId,
maxRetryDelayMs,
parallelToolExecute,
agentEventSink,
tool_store,
)
# Spawn the background loop and attach it
agent._agent_loop = @spawn _agent_loop(agent)
return agent
end
"""
Send a message to the agent's input channel.
+9 -9
View File
@@ -633,7 +633,7 @@ function prepareToolCall(
- On failure: throws `ArgumentError(error_string)`, caught by the try-catch below
4. **Run `beforeToolCall` hook** — if `config.beforeToolCall !== nothing`
- Passes `assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context)` and `signal`
- Passes `beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context)` and `signal`
- Hook can return `nothing` (proceed), or `Dict(:block => true, :reason => "...")` (reject)
- If `signal.aborted == true` → `immediateOutcome(createErrorToolResult("Operation aborted"), true)`
- If `before.block == true` → `immediateOutcome(createErrorToolResult(get(before, :reason, "blocked")), true)`
@@ -722,10 +722,10 @@ function finalizeExecutedToolCall(
```
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
- Passes `afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
- Hook can mutate the result:
```julia
after = config.afterToolCall(afterCtx(...))
after = config.afterToolCall(afterToolCallContext(...))
if after !== nothing
result = merge(result, dict(
:content => get(after, :content, result.content),
@@ -1146,8 +1146,8 @@ The `agentEventSink` function is a user-provided callback that receives all even
| `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::assistantMsgCtx, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
| `afterToolCall` | `(afterCtx::afterCtx, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
| `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
@@ -1157,7 +1157,7 @@ The `agentEventSink` function is a user-provided callback that receives all even
```julia
if config.beforeToolCall !== nothing
before = config.beforeToolCall(
assistantMsgCtx(assistantMsg, toolCall, validatedArgs, context), signal
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context), signal
)
if signal !== nothing && signal.aborted
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
@@ -1182,7 +1182,7 @@ end
if config.afterToolCall !== nothing
try
after = config.afterToolCall(
afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
)
if after !== nothing
result = merge(result, dict(
@@ -1606,8 +1606,8 @@ All functions in the file are scoped under `_tool_myTool`, preventing name colli
| `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`) |
| `assistantMsgCtx` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
| `afterCtx` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
| `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
+9 -141
View File
@@ -13,8 +13,8 @@
# Context types
agentContext, agentState, agentToolCall, prepareNextTurnContext,
# Loop & execution types
agentLoopConfig, abortSignal, agentToolResult,
assistantMsgCtx, afterCtx,
agentLoopConfig, abortSignal, agentToolResult,beforeToolCallContext,
beforeToolCallResult, afterToolCallContext,
# Event types
toolExecStartEvent, toolExecUpdateEvent, toolExecEndEvent,
# Agent
@@ -442,13 +442,18 @@ Context passed to the `beforeToolCall` hook.
- `args::Dict{String,Any}`: Validated tool arguments
- `context::agentContext`: Current conversation context
"""
struct assistantMsgCtx
struct beforeToolCallContext
message::assistantMessage
toolCall::agentToolCall
args::Dict{String,Any}
context::agentContext
end
struct beforeToolCallResult
block::Bool
reason::String
end
"""
Context passed to the `afterToolCall` hook.
@@ -460,7 +465,7 @@ Context passed to the `afterToolCall` hook.
- `isError::Bool`: Whether execution resulted in an error
- `context::agentContext`: Current conversation context
"""
struct afterCtx
struct afterToolCallContext
message::assistantMessage
toolCall::agentToolCall
args::Dict{String,Any}
@@ -521,143 +526,6 @@ end
abstract type agent end
"""
docstring
"""
mutable struct yiemAgent <: agent # High-level agent wrapper
_state::agentState # Current state (prompt, model, messages, tools, etc.)
# user sends prompt message to agent. if agent is idle, it process user message right away.
# if agent is running, it process user message after the current tool call finished.
inputChannel::Channel
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
# messages are handled and the agent is idle (not using a tool call).
followUpChannel::Channel
# agent sends response message to user after processing all user messages in inputChannel
# and all followUp messages.
outputChannel::Channel
_agent_loop::Union{Task, Nothing} # agent loop running in the background
# Preprocess/transform messages and context (modify, filter, prune, inject context from memory,
# reorder, ...) for a single LLM call in _process_message()'s loop.
# returns new Vector{agentMessage}
prepareContext ::Union{Function, Nothing}
# Convert prepareContext()'s new Vector{agentMessage} to LLM message format
formatMsgForLLM::Function
# Actually invoke the LLM to get a completion response. The LLM response comes back as an
# assistantMessage whose content is an array of content blocks.
# Each block has a type — "text", "thinking", or "toolCall".
# The code filters for type === "toolCall" blocks, then passes them to executeToolCalls().
llmCall::Function
# Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..)
beforeToolCall::Union{Function, Nothing}
executeToolCalls::Function # execute tool calls ()
# Callback invoked after executing a tool call to sanitize tools output so the output is ready
# to be converted into toolResults message
afterToolCall::Union{Function, Nothing}
# prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn
# prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context
sessionId::Union{String, Nothing} # Optional session identifier
maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms)
parallelToolExecute::Bool # Default: false
agentEventSink::Function # agent emits its status via this function
_tool_store::Any # Reference to the toolStore for runtime registration
end
"""
Create a new yiemAgent instance with a background loop task.
Spawns a background `@spawn` task that runs the agent loop, listening
on `inputChannel` and `followUpChannel` channels concurrently.
# Keyword Arguments
- `systemPrompt::String`: System prompt for the agent
- `model`: LLM model to use
- `tools::OrderedDict{String, agentTool}`: Available tools keyed by name (default: empty)
- `messages::Vector{agentMessage}`: Initial conversation messages (default: empty)
- `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`)
- `llmCall::Function`: Function to invoke the LLM (required)
- `prepareContext::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`)
- `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`)
- `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`)
- `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`)
- `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`)
- `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`)
- `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`)
- `parallelToolExecute::Bool`: Run tool calls in parallel (default: `false`)
- `agentEventSink::Function`: Callback to receive agent events
- `tool_store::Union{Any, Nothing}`: toolStore for runtime tool registration (default: `nothing`)
# Returns
- A new `yiemAgent` instance with an active background task
# Examples
```julia
julia> store = toolStore(name="agent1")
julia> tools = loadTools(store, "src/tools")
julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model, tools=tools, llmCall=..., tool_store=store)
yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., store)
"""
function yiemAgent(
; systemPrompt::String="You are helpful assistant.",
model=nothing,
tools::OrderedDict{String, agentTool}=OrderedDict{String, agentTool}(),
messages::Vector{agentMessage}=agentMessage[],
prepareContext::Union{Function, Nothing}=nothing,
formatMsgForLLM::Function=defaultformatMsgForLLM,
llmCall::Function,
beforeToolCall::Union{Function, Nothing}=nothing,
afterToolCall::Union{Function, Nothing}=nothing,
# prepareNextTurn::Union{Function, Nothing}=nothing,
# prepareNextTurnWithContext::Union{Function, Nothing}=nothing,
sessionId::Union{String, Nothing}=nothing,
maxRetryDelayMs::Union{Int64, Nothing}=nothing,
parallelToolExecute::Bool=false,
agentEventSink::Function,
tool_store::Union{Any, Nothing}=nothing,
)
# Create channels: input (user -> agent), followUp (async queue), output (agent -> user)
inputChannel = Channel(16)
followUp = Channel(32)
outputChannel = Channel(16)
# Create struct with a placeholder task, then spawn and replace it
agent = yiemAgent(
agentState(systemPrompt, model, tools, messages),
inputChannel,
followUp,
outputChannel,
nothing, # placeholder — replaced below
prepareContext,
formatMsgForLLM,
llmCall,
beforeToolCall,
afterToolCall,
# prepareNextTurn,
# prepareNextTurnWithContext,
sessionId,
maxRetryDelayMs,
parallelToolExecute,
agentEventSink,
tool_store,
)
# Spawn the background loop and attach it
agent._agent_loop = @spawn _agent_loop(agent)
return agent
end
"""
preparedToolCall(tool, toolCall, args)
+12 -1
View File
@@ -1,6 +1,7 @@
module utils
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs, validateToolArguments, _userMessageToOpenAI,
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
validateToolArguments, _userMessageToOpenAI,
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks
using UUIDs, Dates, DataStructures, HTTP, JSON
@@ -219,6 +220,16 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
end
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)::beforeToolCallResult
# final context check
# seek user approval via UI
# other check
return beforeToolCallResult(false, "N/A")
end
"""
Convert a userMessage to OpenAI message format.
"""