update
This commit is contained in:
+4
-4
@@ -152,7 +152,7 @@ result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
||||
**Via agent loop (production):**
|
||||
```
|
||||
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||
→ _agent_loop detects message → @spawn _process_message(agent)
|
||||
→ _agentLoop detects message → @spawn _process_message(agent)
|
||||
→ prepareContext → formatMsgForLLM → llmCall
|
||||
→ LLM returns tool_calls
|
||||
→ executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
@@ -376,7 +376,7 @@ This ensures that `yiemAgent` instances with different `tool_store` references o
|
||||
|
||||
**Source:** `agentCore.jl:35-145`
|
||||
|
||||
The `_agent_loop()` function runs as a background `@spawn` task, created when `yiemAgent` is constructed.
|
||||
The `_agentLoop()` function runs as a background `@spawn` task, created when `yiemAgent` is constructed.
|
||||
|
||||
### Channel Architecture
|
||||
|
||||
@@ -404,7 +404,7 @@ The loop tracks 6 states (documented at `agentCore.jl:39-75`):
|
||||
### Loop Logic (simplified)
|
||||
|
||||
```julia
|
||||
function _agent_loop(agent::yiemAgent)
|
||||
function _agentLoop(agent::yiemAgent)
|
||||
while true
|
||||
# 1. Wait for message from inputChannel (blocking poll)
|
||||
msg = fetch!(agent.inputChannel) # agentCore.jl:84
|
||||
@@ -1332,7 +1332,7 @@ USER SENDS MESSAGE
|
||||
|
||||
|
||||
LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
└─> _agent_loop: detects msg in inputChannel
|
||||
└─> _agentLoop: detects msg in inputChannel
|
||||
└─> Threads.@spawn _process_message(agent)
|
||||
|
||||
── _process_message ──────────────────────────────────────────────
|
||||
|
||||
+92
-55
@@ -1,6 +1,6 @@
|
||||
module agentCore
|
||||
|
||||
export yiemAgent, _agent_loop, OpenAiToUserMessage
|
||||
export yiemAgent, _agentLoop, OpenAiToUserMessage
|
||||
|
||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||
DataFrames, Base.Threads
|
||||
@@ -27,10 +27,10 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
# and all followUp messages.
|
||||
outputChannel::Channel
|
||||
|
||||
_agent_loop::Union{Task, Nothing} # agent loop running in the background
|
||||
_agentLoop::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.
|
||||
# reorder, ...) for a single LLM call in _processMessage()'s loop.
|
||||
# returns new Vector{agentMessage}
|
||||
prepareContext::Union{Function, Nothing}
|
||||
|
||||
@@ -131,7 +131,7 @@ function yiemAgent(
|
||||
)
|
||||
|
||||
# Spawn the background loop and attach it
|
||||
agent._agent_loop = @spawn _agent_loop(agent)
|
||||
agent._agentLoop = @spawn _agentLoop(agent)
|
||||
|
||||
return agent
|
||||
end
|
||||
@@ -141,7 +141,7 @@ end
|
||||
Private agent loop. Runs in a background `@spawn` task.
|
||||
|
||||
Waits on `inputChannel` and `followUpChannel`, processing whichever has a message first.
|
||||
On each iteration, dispatches the message through `_process_message` and sends the result
|
||||
On each iteration, dispatches the message through `_processMessage` and sends the result
|
||||
to `outputChannel`. Exits on `:shutdown` signal.
|
||||
|
||||
# Arguments
|
||||
@@ -160,7 +160,8 @@ to `outputChannel`. Exits on `:shutdown` signal.
|
||||
julia> # Called automatically by yiemAgent constructor
|
||||
```
|
||||
"""
|
||||
function _agent_loop(agent::yiemAgent)
|
||||
function _agentLoop(agent::yiemAgent)
|
||||
processMessageInputCh = Channel(32)
|
||||
try
|
||||
processingTask = nothing
|
||||
|
||||
@@ -208,8 +209,8 @@ function _agent_loop(agent::yiemAgent)
|
||||
while msg === nothing
|
||||
if isready(agent.inputChannel)
|
||||
|
||||
# message will be taken in _process_message()
|
||||
msg = fetch(agent.inputChannel)
|
||||
# message will be taken in _processMessage()
|
||||
msg = take!(agent.inputChannel)
|
||||
agent.agentEventSink("new user msg")
|
||||
else
|
||||
yield()
|
||||
@@ -232,29 +233,50 @@ function _agent_loop(agent::yiemAgent)
|
||||
|
||||
#TODO make sure every running tools ended properly
|
||||
break
|
||||
else
|
||||
agent.agentEventSink("_agentLoop push 1")
|
||||
put!(processMessageInputCh, msg) #WORKING
|
||||
agent.agentEventSink("_agentLoop push 2")
|
||||
end
|
||||
|
||||
# start _process_message loop
|
||||
# start _processMessage loop
|
||||
if agent._state.activeRun == false
|
||||
agent.agentEventSink("_agent_loop 2")
|
||||
agent.agentEventSink("_agentLoop 2")
|
||||
# Dispatch message through the processing pipeline
|
||||
processingTask = Threads.@spawn _process_message(agent)
|
||||
processingTask = @spawn _processMessage(
|
||||
processMessageInputCh,
|
||||
agent.agentEventSink,
|
||||
agent._state.messages,
|
||||
agent._state.systemPrompt,
|
||||
agent._state.tools,
|
||||
agent.prepareContext,
|
||||
agent.formatMsgForLLM,
|
||||
agent.llmCall,
|
||||
agent.beforeToolCall,
|
||||
agent.afterToolCall,
|
||||
agent.parallelToolExecute,
|
||||
)
|
||||
agent._state.activeRun = true
|
||||
agent.agentEventSink("_agent_loop 3")
|
||||
agent.agentEventSink("_agentLoop 3")
|
||||
end
|
||||
|
||||
# during agent runs, check followUp message after _process_message() is done
|
||||
|
||||
# during agent runs, check followUp message after _processMessage() is done
|
||||
if typeof(processingTask) == Task && istaskdone(processingTask) == false
|
||||
agent.agentEventSink("_agentLoop 4")
|
||||
# if followUp message available, add them all to agent.inputChannel
|
||||
if isready(agent.followUpChannel)
|
||||
agent.agentEventSink("_agentLoop 4-1")
|
||||
while isready(agent.followUpChannel)
|
||||
agent.agentEventSink("_agentLoop 4-2")
|
||||
followMsg = take!(agent.followUpChannel)
|
||||
put!(agent.inputChannel, followMsg)
|
||||
end
|
||||
end
|
||||
agent.agentEventSink("_agentLoop 4-3")
|
||||
continue # continue to process user message in the next loop
|
||||
|
||||
elseif typeof(processingTask) == Task && istaskdone(processingTask) == true
|
||||
agent.agentEventSink("_agentLoop 5")
|
||||
# if agent runs is done but followUpChannel has messages, discard all message in it.
|
||||
# when agent work is done it should not accept follow up msg.
|
||||
# user should put new message in inputChannel instead
|
||||
@@ -268,6 +290,7 @@ function _agent_loop(agent::yiemAgent)
|
||||
agent._state.activeRun = false # reset
|
||||
processingTask = nothing # reset
|
||||
end
|
||||
agent.agentEventSink("_agentLoop 6")
|
||||
end
|
||||
catch e
|
||||
# On any error, send error response and exit the loop
|
||||
@@ -303,13 +326,25 @@ should be implemented. Currently a placeholder that echoes back the received mes
|
||||
julia> # Currently returns a placeholder echo response
|
||||
```
|
||||
"""
|
||||
function _process_message(agent::yiemAgent)::assistantMessage
|
||||
agent.agentEventSink("_process_message 1")
|
||||
function _processMessage(
|
||||
inputChannel::Channel,
|
||||
agentEventSink,
|
||||
messages::Vector{agentMessage},
|
||||
systemPrompt::String,
|
||||
tools::OrderedDict{String, agentTool},
|
||||
prepareContext::Function,
|
||||
formatMessagesForLLM::Function,
|
||||
llmCall,
|
||||
beforeToolCall::Union{Function, Nothing},
|
||||
afterToolCall::Union{Function, Nothing},
|
||||
parallelToolExecute::Bool,
|
||||
)::assistantMessage
|
||||
agentEventSink("_processMessage 1")
|
||||
# loop until llmCall() response didn't use tool calls
|
||||
final_response = nothing
|
||||
while true
|
||||
|
||||
""" example message in agent.inputChannel
|
||||
""" example message in inputChannel
|
||||
Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
@@ -323,42 +358,44 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
"""
|
||||
|
||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||
while isready(agent.inputChannel)
|
||||
agent.agentEventSink("_process_message 2")
|
||||
raw_msg = take!(agent.inputChannel)
|
||||
agent.agentEventSink("_process_message 3")
|
||||
while isready(inputChannel)
|
||||
agentEventSink("_processMessage 2")
|
||||
raw_msg = take!(inputChannel)
|
||||
agentEventSink("_processMessage 3")
|
||||
if raw_msg === :shutdown
|
||||
agent.agentEventSink("_process_message 4")
|
||||
agentEventSink("_processMessage 4")
|
||||
# Re-emit shutdown signal for the loop to handle
|
||||
put!(agent.inputChannel, :shutdown)
|
||||
put!(inputChannel, :shutdown)
|
||||
break
|
||||
end
|
||||
agent.agentEventSink("_process_message 5")
|
||||
agentEventSink("_processMessage 5")
|
||||
user_msg = OpenAiToUserMessage(raw_msg)
|
||||
push!(agent._state.messages, user_msg)
|
||||
agent.agentEventSink("_process_message 6")
|
||||
push!(messages, user_msg)
|
||||
agentEventSink("_processMessage 6")
|
||||
end
|
||||
agentEventSink("_processMessage 7")
|
||||
# call prepareContext()
|
||||
state = agentState(systemPrompt, nothing, tools, messages)
|
||||
agentEventSink("_processMessage 8")
|
||||
preparedContext = prepareContext(state)
|
||||
agentEventSink("_processMessage 8")
|
||||
# Call formatMessagesForLLM() to format for LLM
|
||||
formattedMessages = formatMessagesForLLM(preparedContext)
|
||||
|
||||
# call agent.prepareContext()
|
||||
preparedContext = agent.prepareContext(agent._state)
|
||||
|
||||
# Call agent.formatMsgForLLM(agent._state) to format for LLM
|
||||
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
||||
|
||||
agent.agentEventSink("_process_message 7")
|
||||
agentEventSink("_processMessage 10")
|
||||
# Call llmCall() (blocking — the task waits here)
|
||||
response = llmCall(formattedMessages)
|
||||
agentEventSink(response)
|
||||
agentEventSink("_processMessage 11")
|
||||
error("debug marker")
|
||||
response = agent.llmCall(formatted_messages)
|
||||
agent.agentEventSink("_process_message 8")
|
||||
|
||||
#WORKING Check if LLM used tool calls (inspect content for tool_call blocks)
|
||||
has_tool_calls = false
|
||||
tool_call_list = agentToolCall[]
|
||||
# Check if LLM used tool calls (inspect content for tool_call blocks)
|
||||
hasToolCalls = false
|
||||
toolCallList = agentToolCall[]
|
||||
|
||||
for content_block in response.content
|
||||
if content_block isa Dict
|
||||
if get(content_block, :type, "") == "tool_calls"
|
||||
has_tool_calls = true
|
||||
hasToolCalls = true
|
||||
for tc_data in get(content_block, :tool_calls, [])
|
||||
tc = agentToolCall(
|
||||
type="function",
|
||||
@@ -366,10 +403,10 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
name=get(tc_data, :function, Dict{String,Any}())[:name],
|
||||
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
push!(toolCallList, tc)
|
||||
end
|
||||
elseif get(content_block, :type, "") == "tool_call"
|
||||
has_tool_calls = true
|
||||
hasToolCalls = true
|
||||
tc_data = content_block
|
||||
tc = agentToolCall(
|
||||
type="function",
|
||||
@@ -377,35 +414,35 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
name=get(tc_data, :name, ""),
|
||||
arguments=get(tc_data, :arguments, Dict{String,Any}()),
|
||||
)
|
||||
push!(tool_call_list, tc)
|
||||
push!(toolCallList, tc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if has_tool_calls && length(tool_call_list) > 0
|
||||
if hasToolCalls && length(toolCallList) > 0
|
||||
# Build context and config for executeToolCalls
|
||||
context = agentContext(
|
||||
agent._state.systemPrompt,
|
||||
agent._state.messages,
|
||||
agent._state.tools,
|
||||
systemPrompt,
|
||||
messages,
|
||||
tools,
|
||||
)
|
||||
|
||||
config = agentLoopConfig(
|
||||
agent._state.tools,
|
||||
agent.beforeToolCall,
|
||||
agent.afterToolCall,
|
||||
agent.parallelToolExecute ? "parallel" : "sequential",
|
||||
tools,
|
||||
beforeToolCall,
|
||||
afterToolCall,
|
||||
parallelToolExecute ? "parallel" : "sequential",
|
||||
)
|
||||
|
||||
signal = nothing
|
||||
emit = agent.agentEventSink
|
||||
emit = agentEventSink
|
||||
|
||||
# call executeToolCalls()
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
batch = executeToolCalls(context, response, toolCallList, config, signal, emit)
|
||||
|
||||
# save toolResults to agent._state.messages
|
||||
# save toolResults to messages
|
||||
for tool_result in batch.messages
|
||||
push!(agent._state.messages, tool_result)
|
||||
push!(messages, tool_result)
|
||||
end
|
||||
|
||||
if batch.terminate
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ julia> stopAgent(agent)
|
||||
function stopAgent(agent::yiemAgent)
|
||||
put!(agent.inputChannel, :shutdown)
|
||||
try
|
||||
fetch(agent._agent_loop)
|
||||
fetch(agent._agentLoop)
|
||||
catch e
|
||||
if e isa TaskFailedException
|
||||
rethrow(e)
|
||||
|
||||
@@ -192,6 +192,22 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
]
|
||||
),
|
||||
],
|
||||
"tools"=> [
|
||||
Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => "get_weather",
|
||||
"description" => "Get current weather",
|
||||
"parameters" => Dict(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"city" => Dict("type" => "string")
|
||||
),
|
||||
"required" => ["city"]
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
"temperature" => 0.7
|
||||
)
|
||||
"""
|
||||
@@ -217,6 +233,12 @@ function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
end
|
||||
end
|
||||
|
||||
#WORKING convert ctx.tools into openai's tools format.
|
||||
# ctx.tools has the following format
|
||||
# tools = OrderedDict{String, YiemAgent.type.agentTool}("getTime" => YiemAgent.type.agentTool("getTime", "Time Lookup", "Get current local time for a timezone or city.", Dict{String, Any}("properties" => Dict("city" => Dict("type" => "string", "description" => "City name as fallback"), "timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'")), "required" => Any[], "type" => "object"), YiemAgent.toolRegistry._tool_getTime.executeTool, nothing, YiemAgent.toolRegistry._tool_getTime.validateRequiredArgs, false), "getWeather" => YiemAgent.type.agentTool("getWeather", "Weather Lookup", "Fetch current weather and forecast for a given city.", Dict{String, Any}("properties" => Dict{String, Dict{String}}("units" => Dict{String, Any}("default" => "celsius", "type" => "string", "description" => "Temperature scale", "enum" => ["celsius", "fahrenheit"]), "city" => Dict("type" => "string", "description" => "City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'")), "required" => ["city"], "type" => "object"), YiemAgent.toolRegistry._tool_getWeather.executeTool, nothing, nothing, false), "listTools" => YiemAgent.type.agentTool("listTools", "List Tools", "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.", Dict{String, Any}("properties" => Dict{String, Any}(), "required" => Any[], "type" => "object"), YiemAgent.toolRegistry.var"#listTool##0#listTool##1"{YiemAgent.toolRegistry.toolStore}(YiemAgent.toolRegistry.toolStore(OrderedDict{String, YiemAgent.type.agentTool}(#= circular reference @-4 =#), "myagent")), nothing, nothing, false))
|
||||
|
||||
|
||||
|
||||
return Dict("messages" => messages)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user