# 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