725 lines
18 KiB
Markdown
725 lines
18 KiB
Markdown
# AgentCore.jl - Examples and Patterns
|
|
|
|
## Quick Start Examples
|
|
|
|
### Example 1: Basic Conversation
|
|
|
|
```julia
|
|
using AgentCore
|
|
|
|
# Create model
|
|
model = Model(
|
|
"gpt-4",
|
|
"GPT-4",
|
|
"openai",
|
|
"openai",
|
|
"https://api.openai.com/v1",
|
|
true,
|
|
["text"],
|
|
ModelCost(0.00003, 0.00006, 0.0, 0.0),
|
|
128000,
|
|
4096,
|
|
)
|
|
|
|
# Create tools
|
|
bash_tool = createBashTool()
|
|
|
|
# Create agent
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:tools => [bash_tool],
|
|
:thinkingLevel => THINKING_MEDIUM,
|
|
:toolExecution => EXECUTION_PARALLEL,
|
|
))
|
|
|
|
# Subscribe to events
|
|
subscribe(agent) do event, signal
|
|
if event isa MessageEndEvent
|
|
println("Agent: $(event.message)")
|
|
end
|
|
end
|
|
|
|
# Start conversation
|
|
prompt(agent, "What's in the current directory?")
|
|
|
|
# Wait for completion
|
|
waitForIdle(agent)
|
|
|
|
# Get final state
|
|
state = get_state(agent)
|
|
println("Total messages: $(length(state.messages))")
|
|
```
|
|
|
|
### Example 2: Conversation with Memory
|
|
|
|
```julia
|
|
# Create session storage
|
|
metadata = JsonlSessionMetadata(
|
|
"session_1",
|
|
"2024-01-01T00:00:00Z",
|
|
"/path/to/project",
|
|
"/path/to/session.jsonl",
|
|
nothing,
|
|
Dict("project" => "my-project"),
|
|
)
|
|
storage = JsonlSessionStorage(metadata, "/path/to/session.jsonl")
|
|
|
|
# Create session
|
|
session = Session(storage)
|
|
|
|
# Add messages to session
|
|
appendMessage(session, UserMessage("user", [TextContent("Hello, my name is Alice.")], Int64(Dates.now(Dates.UTC).datetime)))
|
|
|
|
# Check session stats
|
|
stats = getSessionStats(session)
|
|
println("Messages: $(stats.message_count)")
|
|
println("Total tokens: $(stats.total_tokens)")
|
|
|
|
# Create agent with session
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:tools => [bash_tool],
|
|
:sessionId => getMetadata(session).id,
|
|
))
|
|
```
|
|
|
|
### Example 3: Steering and Follow-Up
|
|
|
|
```julia
|
|
# Start conversation
|
|
prompt(agent, "Create a Python project.")
|
|
|
|
# Queue a steering message (injected after current assistant turn)
|
|
timestamp = Int64(Dates.now(Dates.UTC).datetime)
|
|
steer(agent, UserMessage("user", [TextContent("Actually, let's use Node.js instead")], timestamp))
|
|
|
|
# Wait for redirection
|
|
waitForIdle(agent)
|
|
|
|
# Queue a follow-up message (runs only after agent would otherwise stop)
|
|
followUp(agent, UserMessage("user", [TextContent("Can you add tests?")], timestamp))
|
|
|
|
# Continue until completion
|
|
while hasQueuedMessages(agent)
|
|
waitForIdle(agent)
|
|
end
|
|
```
|
|
|
|
### Example 4: Branching Conversations
|
|
|
|
```julia
|
|
# Initial conversation
|
|
prompt(agent, "I want to build a web app.")
|
|
|
|
# Get the branch at a specific point
|
|
entry_id = "msg_3_id"
|
|
branch = getBranch(session, entry_id)
|
|
println("Branch has $(length(branch)) entries")
|
|
|
|
# Move to a specific entry (creates a branch summary if summary is provided)
|
|
moveTo(session, entry_id, Dict("summary" => "User decided to explore mobile app instead"))
|
|
|
|
# Continue on new branch
|
|
prompt(agent, "Let's build a mobile app instead.")
|
|
|
|
# Check session branch
|
|
branch = getBranch(session)
|
|
println("Current branch has $(length(branch)) entries")
|
|
```
|
|
|
|
## Advanced Patterns
|
|
|
|
### Pattern 1: Token Usage Monitoring
|
|
|
|
```julia
|
|
# Simple token estimation from messages
|
|
function estimateTokens(message::AgentMessage)::Int64
|
|
content = if message isa UserMessage
|
|
join([c.text for c in message.content if c isa TextContent])
|
|
elseif message isa AssistantMessage
|
|
join([c.text for c in message.content if c isa TextContent])
|
|
elseif message isa ToolResultMessage
|
|
join([c.text for c in message.content if c isa TextContent])
|
|
else
|
|
""
|
|
end
|
|
return ceil(Int, length(content) / 4)
|
|
end
|
|
|
|
# Monitor session token usage
|
|
function checkTokenUsage(agent, session)
|
|
state = get_state(agent)
|
|
stats = getSessionStats(session)
|
|
|
|
println("Session tokens: $(stats.total_tokens)")
|
|
println("Messages in state: $(length(state.messages))")
|
|
|
|
total_estimated = sum(estimateTokens, state.messages)
|
|
println("Estimated total tokens: $(total_estimated)")
|
|
|
|
return stats.total_tokens
|
|
end
|
|
|
|
# Agent loop with token monitoring
|
|
function runAgentWithMonitoring(agent, session, max_tokens=120000)
|
|
while true
|
|
total = checkTokenUsage(agent, session)
|
|
if total > max_tokens
|
|
println("Approaching token limit: $(total)")
|
|
break
|
|
end
|
|
|
|
if !hasQueuedMessages(agent) && isnothing(agent.active_run)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Pattern 2: Custom Tool
|
|
|
|
```julia
|
|
# Create a custom tool
|
|
function createCustomTool()
|
|
return AgentTool(
|
|
"custom_tool",
|
|
"custom_tool",
|
|
"A custom tool description.",
|
|
Dict{String, Any}(),
|
|
(tool_call_id, params, signal, on_update, context) -> begin
|
|
# Execute tool logic
|
|
value = params["value"]
|
|
|
|
# Send progress updates
|
|
on_update("Processing $value...")
|
|
|
|
result = processValue(value)
|
|
|
|
return AgentToolResult(
|
|
[TextContent(result)],
|
|
nothing,
|
|
nothing,
|
|
nothing,
|
|
nothing, # terminate
|
|
)
|
|
end,
|
|
nothing,
|
|
EXECUTION_SEQUENTIAL,
|
|
)
|
|
end
|
|
|
|
# Use custom tool
|
|
custom_tool = createCustomTool()
|
|
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:tools => [bash_tool, custom_tool],
|
|
))
|
|
```
|
|
|
|
### Pattern 3: Dynamic Model Selection via Hook
|
|
|
|
```julia
|
|
# Hook to change model based on conversation context
|
|
function dynamicModelSelection(signal)
|
|
# This hook is called between turns to potentially change the model
|
|
# Return AgentLoopTurnUpdate to change model/thinking_level, or nothing to keep current
|
|
return nothing
|
|
end
|
|
|
|
# Configure agent with the hook
|
|
agent = Agent(Dict(
|
|
:prepareNextTurn => dynamicModelSelection,
|
|
))
|
|
|
|
# The hook receives an AgentEvent and AbortSignal.
|
|
# Access conversation context via:
|
|
# context.message - the last assistant message
|
|
# context.tool_results - tool results from the last turn
|
|
# context.context - the full AgentContext
|
|
```
|
|
|
|
### Pattern 4: Tool Call Interception
|
|
|
|
```julia
|
|
# Hook to validate or block tool calls before they execute
|
|
function toolCallValidator(event, signal)
|
|
if event isa ToolExecutionStartEvent
|
|
# Log or validate tool calls
|
|
println("Tool call: $(event.tool_name) with args: $(event.args)")
|
|
|
|
# Block dangerous commands
|
|
if event.tool_name == "bash"
|
|
args = event.args
|
|
if args isa Dict && haskey(args, :command)
|
|
cmd = args[:command]
|
|
if contains(cmd, "rm -rf /")
|
|
println("Blocked dangerous command!")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nothing
|
|
end
|
|
|
|
# Configure with beforeToolCall hook
|
|
agent = Agent(Dict(
|
|
:beforeToolCall => toolCallValidator,
|
|
))
|
|
|
|
# After tool call hook
|
|
function toolCallLogger(event, signal)
|
|
if event isa ToolExecutionEndEvent
|
|
status = event.is_error ? "ERROR" : "OK"
|
|
println("[$status] $(event.tool_name): $(event.tool_call_id)")
|
|
end
|
|
return nothing
|
|
end
|
|
|
|
agent = Agent(Dict(
|
|
:afterToolCall => toolCallLogger,
|
|
))
|
|
```
|
|
|
|
### Pattern 5: Multi-Step Tool Execution
|
|
|
|
```julia
|
|
# Tool that requires multiple steps with progress updates
|
|
function createMultiStepTool()
|
|
return AgentTool(
|
|
"multistep",
|
|
"multistep",
|
|
"Multi-step task",
|
|
Dict{String, Any}(),
|
|
(tool_call_id, params, signal, on_update, context) -> begin
|
|
# Step 1: Prepare
|
|
on_update("Preparing...")
|
|
prepare_result = prepareStep(params)
|
|
|
|
# Step 2: Execute
|
|
on_update("Executing...")
|
|
execute_result = executeStep(prepare_result, params)
|
|
|
|
# Step 3: Finalize
|
|
on_update("Finalizing...")
|
|
finalize_result = finalizeStep(execute_result)
|
|
|
|
return AgentToolResult(
|
|
[TextContent(finalize_result)],
|
|
Dict("steps" => 3),
|
|
nothing,
|
|
nothing,
|
|
nothing,
|
|
)
|
|
end,
|
|
nothing,
|
|
EXECUTION_SEQUENTIAL,
|
|
)
|
|
end
|
|
```
|
|
|
|
### Pattern 6: Image Processing with Read Tool
|
|
|
|
```julia
|
|
# Create read tool with image support
|
|
read_tool = createReadTool(ReadToolOptions(
|
|
auto_resize_images=true,
|
|
image_processor=nothing,
|
|
))
|
|
|
|
# Use with agent that supports image input
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:tools => [read_tool],
|
|
))
|
|
|
|
# Send prompt with image content
|
|
timestamp = Int64(Dates.now(Dates.UTC).datetime)
|
|
image_msg = UserMessage(
|
|
"user",
|
|
[
|
|
TextContent("Analyze this image:"),
|
|
ImageContent(base64_data, "image/png"),
|
|
],
|
|
timestamp,
|
|
)
|
|
prompt(agent, image_msg)
|
|
```
|
|
|
|
### Pattern 7: Session Navigation
|
|
|
|
```julia
|
|
# Navigate to specific entry
|
|
moveTo(session, entry_id)
|
|
|
|
# Get branch from specific point
|
|
branch = getBranch(session, entry_id)
|
|
|
|
# Create label for an entry (links to another entry)
|
|
appendLabel(session, entry_id, "important-decision")
|
|
|
|
# Get the label for a specific entry
|
|
label = getLabel(session, entry_id)
|
|
if !isnothing(label)
|
|
println("Label: $label")
|
|
end
|
|
|
|
# Build session context from current branch
|
|
context = buildSessionContext(session)
|
|
|
|
# Get specific messages from branch entries
|
|
entries = getBranch(session)
|
|
for (i, entry) in enumerate(entries)
|
|
messages = sessionEntryToContextMessages(entry, i, entries)
|
|
for msg in messages
|
|
println("$(msg.role): $(msg)")
|
|
end
|
|
end
|
|
```
|
|
|
|
### Pattern 8: Batch Processing
|
|
|
|
```julia
|
|
# Process multiple prompts sequentially
|
|
prompts = [
|
|
"What is Julia?",
|
|
"What is JavaScript?",
|
|
"What is Python?",
|
|
]
|
|
|
|
results = []
|
|
|
|
for prompt_text in prompts
|
|
# Create fresh agent for each prompt
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:tools => [bash_tool],
|
|
))
|
|
|
|
# Run prompt
|
|
prompt(agent, prompt_text)
|
|
waitForIdle(agent)
|
|
|
|
# Get result
|
|
state = get_state(agent)
|
|
last_message = state.messages[end]
|
|
|
|
push!(results, last_message)
|
|
|
|
# Clean up
|
|
reset!(agent)
|
|
end
|
|
```
|
|
|
|
### Pattern 9: Event Subscription
|
|
|
|
```julia
|
|
# Subscribe to various agent events
|
|
subscribe(agent) do event, signal
|
|
if event isa AgentStartEvent
|
|
println("Agent started")
|
|
elseif event isa TurnStartEvent
|
|
println("Turn started")
|
|
elseif event isa MessageStartEvent
|
|
println("Message started")
|
|
elseif event isa MessageUpdateEvent
|
|
# Partial message update during streaming
|
|
partial = event.assistant_message_event
|
|
# Access partial message content
|
|
elseif event isa MessageEndEvent
|
|
println("Message ended: $(event.message)")
|
|
elseif event isa ToolExecutionStartEvent
|
|
println("Tool exec start: $(event.tool_name)")
|
|
elseif event isa ToolExecutionUpdateEvent
|
|
# Tool progress update
|
|
println("Tool update: $(event.partial_result)")
|
|
elseif event isa ToolExecutionEndEvent
|
|
status = event.is_error ? "error" : "success"
|
|
println("Tool exec end: $(event.tool_name) [$status]")
|
|
elseif event isa TurnEndEvent
|
|
println("Turn ended")
|
|
elseif event isa AgentEndEvent
|
|
println("Agent ended with $(length(event.messages)) messages")
|
|
end
|
|
end
|
|
```
|
|
|
|
### Pattern 10: Error Handling
|
|
|
|
```julia
|
|
# Monitor for errors in conversation
|
|
subscribe(agent) do event, signal
|
|
if event isa MessageEndEvent
|
|
msg = event.message
|
|
if msg isa AssistantMessage
|
|
if msg.stop_reason == "error"
|
|
println("Error: $(msg.error_message)")
|
|
elseif msg.stop_reason == "length"
|
|
println("Response truncated (token limit reached)")
|
|
elseif msg.stop_reason == "aborted"
|
|
println("Request aborted")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Error handling hook
|
|
function errorHandlingHook(signal)
|
|
# This is called between turns
|
|
# Return AgentLoopTurnUpdate to modify behavior, or nothing
|
|
return nothing
|
|
end
|
|
|
|
agent = Agent(Dict(
|
|
:prepareNextTurn => errorHandlingHook,
|
|
))
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Unit Testing
|
|
|
|
```julia
|
|
using Test
|
|
using AgentCore
|
|
|
|
# Test tool creation
|
|
@test createBashTool() isa AgentTool
|
|
@test createReadTool() isa AgentTool
|
|
@test createWriteTool() isa AgentTool
|
|
@test createEditTool() isa AgentTool
|
|
|
|
# Test basic agent creation
|
|
@test_throws ErrorException Agent(Dict(:model => nothing))
|
|
|
|
# Test agent state
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "Test",
|
|
:model => Model("", "", "test", "test", "", false, String[], ModelCost(0,0,0,0), 0, 0),
|
|
))
|
|
state = get_state(agent)
|
|
@test state.system_prompt == "Test"
|
|
@test length(state.messages) == 0
|
|
```
|
|
|
|
### Integration Testing with In-Memory Storage
|
|
|
|
```julia
|
|
using AgentCore
|
|
|
|
# Create in-memory session
|
|
repo = InMemorySessionRepo()
|
|
session = create(repo)
|
|
|
|
# Add messages
|
|
appendMessage(session, UserMessage("user", [TextContent("Hello")], Int64(Dates.now(Dates.UTC).datetime)))
|
|
|
|
# Verify session
|
|
stats = getSessionStats(session)
|
|
@test stats.message_count == 1
|
|
|
|
# Navigate with moveTo
|
|
entry_id = getLeafId(session)
|
|
moveTo(session, entry_id)
|
|
|
|
# Fork from entry
|
|
forked = fork(repo, getMetadata(session), Dict("entryId" => entry_id))
|
|
```
|
|
|
|
## Performance Patterns
|
|
|
|
### Pattern 1: Queue Mode Configuration
|
|
|
|
```julia
|
|
# Configure steering mode (how steering messages are queued)
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:steeringMode => QUEUE_ONE_AT_A_TIME, # Only one steering message processed at a time
|
|
:followUpMode => QUEUE_ALL, # All follow-ups processed in batch
|
|
))
|
|
|
|
# Clear queues as needed
|
|
clearSteeringQueue(agent)
|
|
clearFollowUpQueue(agent)
|
|
clearAllQueues(agent)
|
|
```
|
|
|
|
### Pattern 2: Message Normalization
|
|
|
|
```julia
|
|
# Custom message normalization function
|
|
function customNormalize(messages::Vector{AgentMessage})::Vector{Message}
|
|
return filter(
|
|
(m) -> m.role == "user" || m.role == "assistant" || m.role == "toolResult",
|
|
messages,
|
|
)
|
|
end
|
|
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:convertToLlm => customNormalize,
|
|
))
|
|
```
|
|
|
|
### Pattern 3: Context Transformation
|
|
|
|
```julia
|
|
# Transform context before LLM call
|
|
function transformContextFn(messages::Vector{AgentMessage}, signal)
|
|
# Filter or modify messages before sending to LLM
|
|
filtered = filter(m -> m.role != "toolResult", messages)
|
|
return filtered
|
|
end
|
|
|
|
agent = Agent(Dict(
|
|
:systemPrompt => "You are a helpful assistant.",
|
|
:model => model,
|
|
:transformContext => transformContextFn,
|
|
))
|
|
```
|
|
|
|
## Production Patterns
|
|
|
|
### Pattern 1: Observability via Events
|
|
|
|
```julia
|
|
# Log all agent events for debugging and monitoring
|
|
subscribe(agent) do event, signal
|
|
timestamp = Dates.now(Dates.UTC)
|
|
|
|
if event isa AgentStartEvent
|
|
println("[$timestamp] AgentStart")
|
|
elseif event isa AgentEndEvent
|
|
println("[$timestamp] AgentEnd ($(length(event.messages)) messages)")
|
|
elseif event isa TurnStartEvent
|
|
println("[$timestamp] TurnStart")
|
|
elseif event isa TurnEndEvent
|
|
tool_count = length(event.tool_results)
|
|
println("[$timestamp] TurnEnd ($tool_count tools)")
|
|
elseif event isa ToolExecutionStartEvent
|
|
println("[$timestamp] ToolStart: $(event.tool_name)")
|
|
elseif event isa ToolExecutionEndEvent
|
|
status = event.is_error ? "ERROR" : "OK"
|
|
println("[$timestamp] ToolEnd: $(event.tool_name) [$status]")
|
|
end
|
|
end
|
|
```
|
|
|
|
### Pattern 2: Abort Handling
|
|
|
|
```julia
|
|
# Abort a running agent
|
|
if !isnothing(agent.active_run)
|
|
abort(agent)
|
|
end
|
|
|
|
# Check if agent is idle
|
|
if isnothing(agent.active_run)
|
|
println("Agent is idle")
|
|
end
|
|
```
|
|
|
|
### Pattern 3: Continue from Transcript
|
|
|
|
```julia
|
|
# Continue from the last message in the transcript
|
|
continue!(agent)
|
|
|
|
# The last message must be user or tool-result role.
|
|
# If the last message is assistant, pending steering/follow-up messages
|
|
# are processed first, then an error is thrown if none exist.
|
|
```
|
|
|
|
## Debugging Patterns
|
|
|
|
### Pattern 1: Conversation Trace
|
|
|
|
```julia
|
|
# Trace all messages in the conversation
|
|
trace = []
|
|
|
|
subscribe(agent) do event, signal
|
|
if event isa MessageEndEvent
|
|
msg = event.message
|
|
push!(trace, Dict(
|
|
"role" => msg.role,
|
|
"type" => typeof(msg).name.name,
|
|
))
|
|
end
|
|
end
|
|
|
|
# Run conversation
|
|
prompt(agent, "Hello")
|
|
waitForIdle(agent)
|
|
|
|
# Print trace
|
|
for entry in trace
|
|
println("$(entry["type"]): $(entry["role"])")
|
|
end
|
|
```
|
|
|
|
### Pattern 2: Tool Call Trace
|
|
|
|
```julia
|
|
tool_trace = []
|
|
|
|
subscribe(agent) do event, signal
|
|
if event isa ToolExecutionStartEvent
|
|
push!(tool_trace, Dict(
|
|
"type" => "start",
|
|
"tool" => event.tool_name,
|
|
"id" => event.tool_call_id,
|
|
"args" => event.args,
|
|
))
|
|
elseif event isa ToolExecutionEndEvent
|
|
push!(tool_trace, Dict(
|
|
"type" => "end",
|
|
"tool" => event.tool_name,
|
|
"id" => event.tool_call_id,
|
|
"error" => event.is_error,
|
|
))
|
|
end
|
|
end
|
|
```
|
|
|
|
### Pattern 3: State Dump
|
|
|
|
```julia
|
|
function dumpState(agent)
|
|
state = get_state(agent)
|
|
|
|
println("=== Agent State ===")
|
|
println("System prompt: $(state.system_prompt)")
|
|
println("Model: $(state.model.name)")
|
|
println("Thinking level: $(state.thinking_level)")
|
|
println("Messages: $(length(state.messages))")
|
|
println("Tools: $(length(state.tools))")
|
|
println("==================")
|
|
end
|
|
|
|
# Use after conversation
|
|
prompt(agent, "Hello")
|
|
waitForIdle(agent)
|
|
dumpState(agent)
|
|
```
|
|
|
|
## Best Practices Summary
|
|
|
|
1. **Start simple**, add complexity gradually
|
|
2. **Use hooks for customization**, not core logic
|
|
3. **Test with basic agent** first before adding hooks
|
|
4. **Monitor token usage** for long conversations
|
|
5. **Use branches** for exploration
|
|
6. **Handle errors gracefully** via event subscriptions
|
|
7. **Log important events**
|
|
8. **Clear queues** when not needed
|
|
9. **Use correct Julia naming conventions** (camelCase for functions)
|
|
10. **Pass session as first argument** for session functions
|