This commit is contained in:
2026-09-01 10:36:27 +07:00
parent c1120d811a
commit 096baad09d
9 changed files with 608 additions and 1970 deletions
+104 -9
View File
@@ -96,7 +96,7 @@ or pruning and reordering messages before formatting and calling the LLM.
messages, and tools to be sent to the LLM
# Examples
```julia
```jldoctest
# Default: returns an agentContext with deep copies of system prompt, messages, and tools
prepareContext(state).messages == deepcopy(state.messages)
@@ -146,7 +146,7 @@ LLM message formats for different APIs/providers.
an array of OpenAI-format message dicts
# Examples
```julia
```jldoctest
# Default output:
formatMsgForLLm(ctx) == Dict("messages" => [
Dict("role" => "system", "content" => [...]),
@@ -265,7 +265,7 @@ Common use cases:
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
# Example
```julia
```jldoctest
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
if context.toolCall.name == "deleteFile"
# Block file deletion unless explicitly approved
@@ -320,11 +320,106 @@ function afterToolCall(context::afterToolCallContext, signal::abortSignal
end
#TODO
function eventSink(x)
"""
Create an event sink for logging messages and/or publishing to NATS.
# Fields
- `natsConn`: NATS connection, or `nothing` for file-only sink
- `topic`: NATS topic to publish to, or `nothing` for file-only sink
- `service_name`: Optional prefix added to log entries
- `log_file`: Path to the log file
- `max_log_entries`: Maximum number of entries to keep in the log file
# Callable interface
```julia
(es::eventSink)(msg::String; log=true, publish=true) -> Nothing
```
Write a timestamped log entry to file, publish to NATS, or both.
# Examples
```julia
using NATS
conn = NATS.connect()
# With NATS — logs to file and publishes to "agent.events"
es = eventSink(natsConn=conn, topic="agent.events", service_name="agent1")
es("agent started") # both file + NATS (default)
es("task done"; log=false) # NATS only
es("checkpoint"; publish=false) # file only
# File only — no NATS connection
es2 = eventSink(log_file="./agent.log", service_name="worker")
es2("ready") # file only
NATS.drain(conn)
```
"""
struct eventSink
natsConn::Union{NATS.Connection, Nothing}
topic::Union{String, Nothing}
service_name::String
log_file::String
max_log_entries::Int
end
eventSink(; natsConn=nothing, topic=nothing, service_name="", log_file="./log/eventSink.md", max_log_entries=100) =
eventSink(natsConn, topic, service_name, log_file, max_log_entries)
"""
rotate!(es::eventSink) -> Nothing
Rewrite the log file keeping only the last `max_log_entries` entries.
Call after writing when the entry count reaches the limit.
"""
function rotate!(es::eventSink)
content = read(es.log_file, String)
entries = split(content, r"- \[", keepempty=false)
open(es.log_file, "w") do f
write(f, "- [")
for e in entries[2:end]
write(f, "- [", e)
end
end
end
"""
(es::eventSink)(msg::String; log=true, publish=true) -> Nothing
Write a timestamped log entry to file, publish to NATS, or both.
# Arguments
- `msg`: The message to log or publish.
- `log`: If `true`, append the entry to `es.log_file` (default `true`).
- `publish`: If `true`, publish to the NATS topic (default `true`; no-op if no NATS connection was provided).
"""
function (es::eventSink)(msg::String; log::Bool=true, publish::Bool=true)
timestamp = Dates.format(now(), "yyyy-mm-ddTHH:MM:SS.sss")
svc_prefix = isempty(es.service_name) ? "" : "[$(es.service_name)] "
entry = "- [$timestamp] $svc_prefix$msg\n"
if log
log_dir = dirname(es.log_file)
mkpath(log_dir)
open(es.log_file, "a") do f
write(f, entry)
end
# Rotate after write — keep only last max_log_entries entries
content = read(es.log_file, String)
entries = split(content, r"- \[", keepempty=false)
if length(entries) > es.max_log_entries
rotate!(es)
end
end
if publish && es.topic !== nothing && es.natsConn !== nothing
try
NATS.publish(es.natsConn, es.topic, entry)
catch e
@warn "eventSink NATS publish failed" exception=e
end
end
end
"""
Convert a userMessage to OpenAI message format.
@@ -432,7 +527,7 @@ Convert an OrderedDict of agentTool definitions into OpenAI function tool format
Returns an empty vector when `tools` is `nothing` or empty.
# Examples
```julia
```jldoctest
_toolsToOpenAI(nothing) # => Dict{String, Any}[]
_toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))]
```
@@ -478,7 +573,7 @@ coercion, format validation, cross-field constraints).
- `String` error message listing missing fields otherwise
# Examples
```julia
```jldoctest
schema = Dict("required" => ["city"])
args = Dict{String,Any}()
validateRequiredArgs(args, schema) # => "Missing required arguments: city"
@@ -533,7 +628,7 @@ false `toolExecutionStart` events.
and converted to an `immediateOutcome`
# Examples
```julia
```jldoctest
# With validateRequiredArgs hook set on the tool
validateToolArguments(toolWithHook, tc) # => validated args or throws