From 1e5f7b8ff20104b6c450212f1060c31b819ed7eb Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 28 Jul 2026 07:12:13 +0700 Subject: [PATCH] update --- .../julia_implementation/IMPLEMENTATION.md | 119 ++ .../agent/julia_implementation/Manifest.toml | 25 + .../agent/julia_implementation/Project.toml | 22 + packages/agent/julia_implementation/README.md | 179 +++ .../julia_implementation/src/AgentCore.jl | 149 +++ .../agent/julia_implementation/src/agent.jl | 416 +++++++ .../julia_implementation/src/agent_loop.jl | 861 +++++++++++++ .../julia_implementation/src/harness_types.jl | 1083 +++++++++++++++++ .../julia_implementation/src/messages.jl | 183 +++ .../src/prompt_templates.jl | 335 +++++ .../src/session/jsonl_repo.jl | 148 +++ .../src/session/jsonl_storage.jl | 290 +++++ .../src/session/memory_repo.jl | 133 ++ .../src/session/memory_storage.jl | 227 ++++ .../src/session/repo_utils.jl | 65 + .../src/session/session.jl | 422 +++++++ .../agent/julia_implementation/src/skills.jl | 375 ++++++ .../julia_implementation/src/stream_fn.jl | 45 + .../julia_implementation/src/system_prompt.jl | 56 + .../julia_implementation/src/tools/bash.jl | 49 + .../julia_implementation/src/tools/edit.jl | 32 + .../src/tools/edit_diff.jl | 67 + .../src/tools/file_mutation_queue.jl | 59 + .../julia_implementation/src/tools/image.jl | 66 + .../julia_implementation/src/tools/index.jl | 35 + .../src/tools/path_utils.jl | 44 + .../julia_implementation/src/tools/read.jl | 35 + .../julia_implementation/src/tools/write.jl | 26 + .../agent/julia_implementation/src/types.jl | 588 +++++++++ 29 files changed, 6134 insertions(+) create mode 100644 packages/agent/julia_implementation/IMPLEMENTATION.md create mode 100644 packages/agent/julia_implementation/Manifest.toml create mode 100644 packages/agent/julia_implementation/Project.toml create mode 100644 packages/agent/julia_implementation/README.md create mode 100644 packages/agent/julia_implementation/src/AgentCore.jl create mode 100644 packages/agent/julia_implementation/src/agent.jl create mode 100644 packages/agent/julia_implementation/src/agent_loop.jl create mode 100644 packages/agent/julia_implementation/src/harness_types.jl create mode 100644 packages/agent/julia_implementation/src/messages.jl create mode 100644 packages/agent/julia_implementation/src/prompt_templates.jl create mode 100644 packages/agent/julia_implementation/src/session/jsonl_repo.jl create mode 100644 packages/agent/julia_implementation/src/session/jsonl_storage.jl create mode 100644 packages/agent/julia_implementation/src/session/memory_repo.jl create mode 100644 packages/agent/julia_implementation/src/session/memory_storage.jl create mode 100644 packages/agent/julia_implementation/src/session/repo_utils.jl create mode 100644 packages/agent/julia_implementation/src/session/session.jl create mode 100644 packages/agent/julia_implementation/src/skills.jl create mode 100644 packages/agent/julia_implementation/src/stream_fn.jl create mode 100644 packages/agent/julia_implementation/src/system_prompt.jl create mode 100644 packages/agent/julia_implementation/src/tools/bash.jl create mode 100644 packages/agent/julia_implementation/src/tools/edit.jl create mode 100644 packages/agent/julia_implementation/src/tools/edit_diff.jl create mode 100644 packages/agent/julia_implementation/src/tools/file_mutation_queue.jl create mode 100644 packages/agent/julia_implementation/src/tools/image.jl create mode 100644 packages/agent/julia_implementation/src/tools/index.jl create mode 100644 packages/agent/julia_implementation/src/tools/path_utils.jl create mode 100644 packages/agent/julia_implementation/src/tools/read.jl create mode 100644 packages/agent/julia_implementation/src/tools/write.jl create mode 100644 packages/agent/julia_implementation/src/types.jl diff --git a/packages/agent/julia_implementation/IMPLEMENTATION.md b/packages/agent/julia_implementation/IMPLEMENTATION.md new file mode 100644 index 00000000..ccb21b58 --- /dev/null +++ b/packages/agent/julia_implementation/IMPLEMENTATION.md @@ -0,0 +1,119 @@ +# Julia Implementation - AgentCore + +This directory contains a Julia reimplementation of the `@earendil-works/pi-agent-core` package. + +## Project Structure + +``` +julia_implementation/ +├── src/ +│ ├── AgentCore.jl # Main module entry point +│ ├── types.jl # Core type definitions +│ ├── stream_fn.jl # Stream function utilities +│ ├── agent_loop.jl # Low-level agent loop +│ ├── agent.jl # High-level Agent struct +│ ├── harness_types.jl # Extended types for AgentHarness +│ ├── messages.jl # Custom message types +│ ├── system_prompt.jl # System prompt formatting +│ ├── skills.jl # Skill loading and formatting +│ ├── prompt_templates.jl # Prompt template handling +│ ├── agent_harness.jl # AgentHarness implementation +│ │ +│ ├── session/ +│ │ ├── session.jl # Session class +│ │ ├── jsonl_storage.jl # JSONL storage +│ │ ├── jsonl_repo.jl # JSONL repository +│ │ ├── memory_storage.jl # In-memory storage +│ │ ├── memory_repo.jl # In-memory repository +│ │ └── repo_utils.jl # Repository utilities +│ │ +│ ├── tools/ +│ │ ├── index.jl # Tool exports +│ │ ├── bash.jl # Bash execution tool +│ │ ├── read.jl # File read tool +│ │ ├── write.jl # File write tool +│ │ ├── edit.jl # File edit tool +│ │ ├── edit_diff.jl # Diff computation +│ │ ├── image.jl # Image utilities +│ │ ├── path_utils.jl # Path resolution +│ │ └── file_mutation_queue.jl # File mutation serialization +│ │ +│ ├── compaction/ +│ │ ├── compaction.jl # Context compaction +│ │ ├── utils.jl # Compaction utilities +│ │ └── branch_summarization.jl # Branch summarization +│ │ +│ ├── utils/ +│ │ ├── truncate.jl # Output truncation +│ │ └── shell_output.jl # Shell output capture +│ │ +│ ├── proxy.jl # Proxy stream function +│ └── utils.jl # Utility functions +│ +├── test/ +├── Project.toml +├── Manifest.toml +└── README.md +``` + +## Key Features + +### Core Architecture + +The implementation follows the same layered architecture as the TypeScript version: + +1. **Low-level (agent_loop.jl)**: Pure agent loop logic that works with `AgentMessage[]` +2. **High-level (agent.jl)**: Stateful wrapper with event streaming and queueing +3. **Harness (agent_harness.jl)**: Session persistence, resource management, hooks +4. **Session (session/)**: Conversation history with compaction and branching +5. **Tools (tools/)**: Built-in execution tools (bash, read, write, edit) + +### Julia-Specific Features + +- **Type system**: Uses Julia's parametric types for type-safe tool definitions +- **Multiple dispatch**: Extensible via multiple dispatch for custom message types +- **Async primitives**: Leverages Julia's `@async` and `@spawn` for concurrent operations +- **Error handling**: Julia exceptions with typed error codes + +## Building + +```julia +using Pkg +Pkg.activate("julia_implementation") +Pkg.instantiate() +``` + +## Usage Example + +```julia +using AgentCore + +# Create an agent +agent = Agent() + +# Subscribe to events +subscribe(agent) do event, signal + if event isa MessageEndEvent + println("Message: $(event.message)") + end +end + +# Run a prompt +prompt(agent, "Hello, world!") +``` + +## Compatibility + +This implementation aims for API compatibility with the TypeScript version while providing idiomatic Julia abstractions. + +## Status + +This is an active implementation. Core functionality is in place, with ongoing work on: + +- Complete tool implementations +- Full session repository functionality +- Test suite + +## License + +MIT diff --git a/packages/agent/julia_implementation/Manifest.toml b/packages/agent/julia_implementation/Manifest.toml new file mode 100644 index 00000000..d67c0900 --- /dev/null +++ b/packages/agent/julia_implementation/Manifest.toml @@ -0,0 +1,25 @@ +name = "AgentCore" +uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" +authors = ["Mario Zechner "] +version = "0.1.0" + +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" +JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" +Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" +Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" +Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" +UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" + +[extras] +Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" + +[targets] +test = ["Test"] + +[compat] +julia = "1.9" diff --git a/packages/agent/julia_implementation/Project.toml b/packages/agent/julia_implementation/Project.toml new file mode 100644 index 00000000..42d1f303 --- /dev/null +++ b/packages/agent/julia_implementation/Project.toml @@ -0,0 +1,22 @@ +name = "AgentCore" +uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f" +authors = ["Mario Zechner "] +version = "0.1.0" + +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc09409a37d3" +JSON3 = "0f8b85d8-8d2f-5481-9e3b-d9a10a9b6c53" +Libdl = "8f399da3-355a-58d1-55dd-a8cd37d21846" +Markdown = "d6f4372e-7a37-5ca6-90db-23e40208355e" +Mmap = "a63ad114-7ff6-5b6b-903e-90ddba579e5d" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872" +UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca" + +[extras] +Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d" + +[targets] +test = ["Test"] diff --git a/packages/agent/julia_implementation/README.md b/packages/agent/julia_implementation/README.md new file mode 100644 index 00000000..2908eae8 --- /dev/null +++ b/packages/agent/julia_implementation/README.md @@ -0,0 +1,179 @@ +# AgentCore.jl - Julia Implementation of Pi Agent Core + +A Julia reimplementation of the `@earendil-works/pi-agent-core` package, providing a stateful agent framework for LLM interactions. + +## Overview + +This package provides: +- Low-level `agentLoop` for stateful LLM interactions with tool execution +- High-level `Agent` struct with state management, event streaming, and queueing +- `AgentHarness` for session persistence, resource management, and extension hooks +- Built-in tools for file operations (read, write, edit) and bash execution +- Session management with JSONL-based storage, compaction, and branch navigation + +## Architecture + +The Julia implementation follows the same layered architecture as the TypeScript version: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AgentHarness │ +│ (Session persistence, resource management) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ Agent │ +│ (State management, event streaming, queueing) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ AgentLoop │ +│ (Low-level loop, tool execution) │ +└─────────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────▼───────────────────────────────────────┐ +│ Session │ +│ (Conversation history, compaction, branching) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Installation + +```julia +using Pkg +Pkg.add("AgentCore") +``` + +## Quick Start + +```julia +using AgentCore + +# Create an agent with default configuration +agent = Agent() + +# Subscribe to events +subscribe(agent) do event, signal + if event isa MessageEndEvent + println("Received message: $(event.message)") + end +end + +# Run a prompt +prompt(agent, "Hello, how are you?") +``` + +## Core Concepts + +### Agent + +The `Agent` struct provides a high-level interface for interacting with LLMs. It manages: +- Conversation state (messages, tools, system prompt) +- Event streaming and lifecycle management +- Steering and follow-up message queues +- Abort handling + +### AgentLoop + +The `agentLoop` function implements the core agent loop that: +- Transforms `AgentMessage[]` to `Message[]` at the LLM call boundary +- Executes tool calls (parallel or sequential) +- Emits lifecycle events +- Handles steering and follow-up messages + +### AgentHarness + +The `AgentHarness` provides: +- Session persistence with JSONL storage +- Resource management (skills, prompt templates) +- Extension hooks system +- Tool execution with context +- Branch navigation and compaction + +### Sessions + +Sessions track conversation history using a tree-based structure: +- Branch-based history with compaction +- Tree navigation (moveTo, navigateTree) +- Message and metadata persistence + +## Built-in Tools + +### Bash Tool + +Execute shell commands with output capture and truncation. + +```julia +bash_tool = createBashTool() +``` + +### Read Tool + +Read files with support for text and images. + +```julia +read_tool = createReadTool() +``` + +### Write Tool + +Write content to files with automatic directory creation. + +```julia +write_tool = createWriteTool() +``` + +### Edit Tool + +Edit files using exact text replacement. + +```julia +edit_tool = createEditTool() +``` + +## Session Storage + +AgentCore supports two session storage backends: + +1. **JsonlSessionStorage** - File-based storage using JSONL format +2. **InMemorySessionStorage** - In-memory storage for testing + +## Compaction + +The compaction system manages context window usage by: +- Summarizing old conversation history +- Retaining recent messages +- Supporting iterative updates to summaries + +## Event System + +AgentCore uses a rich event system for monitoring and control: + +- `AgentStartEvent` / `AgentEndEvent` - Agent lifecycle +- `TurnStartEvent` / `TurnEndEvent` - Conversation turns +- `MessageStartEvent` / `MessageEndEvent` - Message lifecycle +- `ToolExecutionStartEvent` / `ToolExecutionEndEvent` - Tool execution + +## Examples + +See the `examples/` directory for more detailed examples. + +## Differences from TypeScript + +While maintaining API compatibility where possible, this Julia implementation: +- Uses Julia's type system for better compile-time guarantees +- Leverages Julia's multiple dispatch for extensibility +- Uses Julia's async primitives for concurrent operations +- Provides more idiomatic Julia error handling + +## Contributing + +Contributions are welcome! Please see `CONTRIBUTING.md` for details. + +## License + +MIT + +## Acknowledgments + +This is a reimplementation of the [Pi Agent Core](https://github.com/earendil-works/pi/packages/agent) package in Julia. diff --git a/packages/agent/julia_implementation/src/AgentCore.jl b/packages/agent/julia_implementation/src/AgentCore.jl new file mode 100644 index 00000000..420c85fd --- /dev/null +++ b/packages/agent/julia_implementation/src/AgentCore.jl @@ -0,0 +1,149 @@ +# AgentCore.jl - A Julia implementation of the Pi Agent Core framework +# +# This is a reimplementation of the TypeScript pi-agent-core package in idiomatic Julia. +# +# The AgentCore package provides: +# - Low-level `agentLoop` for stateful LLM interactions with tool execution +# - High-level `Agent` struct with state management, event streaming, and queueing +# - `AgentHarness` for session persistence, resource management, and extension hooks +# - Built-in tools for file operations (read, write, edit) and bash execution +# - Session management with JSONL-based storage, compaction, and branch navigation +# +# For more information about the original TypeScript implementation, see: +# https://github.com/earendil-works/pi/packages/agent + +module AgentCore + +# Core modules +include("types.jl") +include("stream_fn.jl") +include("agent_loop.jl") +include("agent.jl") + +# Harness modules +include("harness_types.jl") +include("messages.jl") +include("system_prompt.jl") +include("skills.jl") +include("prompt_templates.jl") +include("agent_harness.jl") + +# Session modules +include("session/session.jl") +include("session/jsonl_storage.jl") +include("session/jsonl_repo.jl") +include("session/memory_storage.jl") +include("session/memory_repo.jl") +include("session/repo_utils.jl") + +# Tool modules +include("tools/index.jl") +include("tools/bash.jl") +include("tools/read.jl") +include("tools/write.jl") +include("tools/edit.jl") +include("tools/edit_diff.jl") +include("tools/image.jl") +include("tools/path_utils.jl") +include("tools/file_mutation_queue.jl") + +# Compaction modules +include("compaction/compaction.jl") +include("compaction/utils.jl") +include("compaction/branch_summarization.jl") + +# Utility modules +include("utils/truncate.jl") +include("utils/shell_output.jl") +include("proxy.jl") + +# Re-export public API +export + # Core types + AgentMessage, + AgentTool, + AgentContext, + AgentEvent, + ThinkingLevel, + ToolExecutionMode, + QueueMode, + AgentState, + + # Agent + Agent, + AgentOptions, + + # AgentLoop + AgentLoopConfig, + agentLoop, + agentLoopContinue, + runAgentLoop, + runAgentLoopContinue, + + # AgentHarness + AgentHarness, + AgentHarnessOptions, + AgentHarnessEvent, + AgentHarnessResources, + AgentHarnessSystemPrompt, + + # Session + Session, + SessionStorage, + SessionRepo, + JsonlSessionStorage, + JsonlSessionRepo, + InMemorySessionStorage, + InMemorySessionRepo, + + # Tools + createBashTool, + createReadTool, + createWriteTool, + createEditTool, + ExecutionEnv, + + # Compaction + compact, + prepareCompaction, + DEFAULT_COMPACTION_SETTINGS, + generateSummary, + generateBranchSummary, + + # Utils + truncateHead, + truncateTail, + formatSize, + DEFAULT_MAX_LINES, + DEFAULT_MAX_BYTES, + + # Messages + convertToLlm, + bashExecutionToText, + + # System prompt + formatSkillsForSystemPrompt, + + # Skills + loadSkills, + formatSkillInvocation, + + # Prompt templates + loadPromptTemplates, + formatPromptTemplateInvocation, + parseCommandArgs, + substituteArgs, + + # Proxy + streamProxy, + ProxyStreamOptions, + + # Stream + setDefaultStreamFn, + getDefaultStreamFn, + + # Utility functions + uuidv7, + create_timestamp + +end diff --git a/packages/agent/julia_implementation/src/agent.jl b/packages/agent/julia_implementation/src/agent.jl new file mode 100644 index 00000000..009114ae --- /dev/null +++ b/packages/agent/julia_implementation/src/agent.jl @@ -0,0 +1,416 @@ +""" + agent.jl - High-level Agent struct + +This module implements the high-level Agent wrapper around the low-level agent loop, +providing state management, event streaming, and queueing for steering and follow-up messages. +""" + +module Agent + +using ..Types: * +using ..AgentLoop: * +using ..StreamFn: * + +# ============================================================================ +# Default convertToLlm function +# ============================================================================ + +function defaultConvertToLlm(messages::Vector{AgentMessage})::Vector{Message} + return filter( + (m) -> m.role == "user" || m.role == "assistant" || m.role == "toolResult", + messages, + ) +end + +# ============================================================================ +# Empty usage constant +# ============================================================================ + +const EMPTY_USAGE = Usage( + 0, 0, 0, 0, 0, UsageCost(0.0, 0.0, 0.0, 0.0, 0.0) +) + +# ============================================================================ +# Pending message queue +# ============================================================================ + +mutable struct PendingMessageQueue + messages::Vector{AgentMessage} + mode::QueueMode + + function PendingMessageQueue(mode::QueueMode) + new(AgentMessage[], mode) + end +end + +function enqueue!(queue::PendingMessageQueue, message::AgentMessage) + push!(queue.messages, message) +end + +function hasItems(queue::PendingMessageQueue)::Bool + return !isempty(queue.messages) +end + +function drain(queue::PendingMessageQueue)::Vector{AgentMessage} + if queue.mode == QUEUE_ALL + result = copy(queue.messages) + empty!(queue.messages) + return result + else + if isempty(queue.messages) + return AgentMessage[] + end + first = popfirst!(queue.messages) + return [first] + end +end + +function clear!(queue::PendingMessageQueue) + empty!(queue.messages) +end + +# ============================================================================ +# Active run state +# ============================================================================ + +mutable struct ActiveRun + promise::Promise + abort_controller::Base.Atomic{Union{Base.AbstractLock, Nothing}} +end + +# ============================================================================ +# Agent struct +# ============================================================================ + +mutable struct Agent + _state::AgentState + listeners::Set{Tuple{Function, Ref{Bool}}} + steering_queue::PendingMessageQueue + follow_up_queue::PendingMessageQueue + + convert_to_llm::Function + transform_context::Union{Function, Nothing} + stream_function::StreamFn + get_api_key::Union{Function, Nothing} + on_payload::Union{Function, Nothing} + on_response::Union{Function, Nothing} + before_tool_call::Union{Function, Nothing} + after_tool_call::Union{Function, Nothing} + prepare_next_turn::Union{Function, Nothing} + prepare_next_turn_with_context::Union{Function, Nothing} + active_run::Union{ActiveRun, Nothing} + session_id::Union{String, Nothing} + thinking_budgets::Union{Dict{String, Int64}, Nothing} + transport::String + max_retry_delay_ms::Union{Int64, Nothing} + tool_execution::ToolExecutionMode + + function Agent(options::Dict{Symbol, Any}=Dict{Symbol, Any}()) + runtime_options = merge( + Dict{Symbol, Any}( + :stream_fn => getDefaultStreamFn(), + :convertToLlm => defaultConvertToLlm, + :steeringMode => QUEUE_ONE_AT_A_TIME, + :followUpMode => QUEUE_ONE_AT_A_TIME, + :toolExecution => EXECUTION_PARALLEL, + :transport => "auto", + ), + options, + ) + + state = AgentState( + get(runtime_options, :systemPrompt, ""), + get(runtime_options, :model, Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0)), + get(runtime_options, :thinkingLevel, THINKING_OFF), + get(runtime_options, :tools, AgentTool[]), + get(runtime_options, :messages, AgentMessage[]), + ) + + new( + state, + Set{Tuple{Function, Ref{Bool}}}(), + PendingMessageQueue(QUEUE_ONE_AT_A_TIME), + PendingMessageQueue(QUEUE_ONE_AT_A_TIME), + get(runtime_options, :convertToLlm, defaultConvertToLlm), + get(runtime_options, :transformContext, nothing), + get(runtime_options, :stream_fn, getDefaultStreamFn()), + get(runtime_options, :getApiKey, nothing), + get(runtime_options, :onPayload, nothing), + get(runtime_options, :onResponse, nothing), + get(runtime_options, :beforeToolCall, nothing), + get(runtime_options, :afterToolCall, nothing), + get(runtime_options, :prepareNextTurn, nothing), + get(runtime_options, :prepareNextTurnWithContext, nothing), + nothing, + get(runtime_options, :sessionId, nothing), + get(runtime_options, :thinkingBudgets, nothing), + get(runtime_options, :transport, "auto"), + get(runtime_options, :maxRetryDelayMs, nothing), + get(runtime_options, :toolExecution, EXECUTION_PARALLEL), + ) + end +end + +# ============================================================================ +# Agent methods +# ============================================================================ + +""" + subscribe(agent, listener) + +Subscribe to agent lifecycle events. + +# Arguments +- `agent`: The agent instance +- `listener`: A function that takes (event::AgentEvent, signal::AbortSignal) + +# Returns +- A function that unsubscribes the listener +""" +function subscribe(agent::Agent, listener::Function)::Function + push!(agent.listeners, (listener, Ref{Bool}(true))) + return () -> begin + filter!(x -> x[1] != listener, agent.listeners) + end +end + +""" + get_state(agent) + +Get the current agent state. +""" +function get_state(agent::Agent)::AgentState + return agent._state +end + +""" + steer(agent, message) + +Queue a message to be injected after the current assistant turn finishes. +""" +function steer(agent::Agent, message::AgentMessage) + enqueue!(agent.steering_queue, message) +end + +""" + followUp(agent, message) + +Queue a message to run only after the agent would otherwise stop. +""" +function followUp(agent::Agent, message::AgentMessage) + enqueue!(agent.follow_up_queue, message) +end + +""" + clearSteeringQueue(agent) + +Remove all queued steering messages. +""" +function clearSteeringQueue(agent::Agent) + clear!(agent.steering_queue) +end + +""" + clearFollowUpQueue(agent) + +Remove all queued follow-up messages. +""" +function clearFollowUpQueue(agent::Agent) + clear!(agent.follow_up_queue) +end + +""" + clearAllQueues(agent) + +Remove all queued steering and follow-up messages. +""" +function clearAllQueues(agent::Agent) + clearSteeringQueue(agent) + clearFollowUpQueue(agent) +end + +""" + hasQueuedMessages(agent) + +Returns true when either queue still contains pending messages. +""" +function hasQueuedMessages(agent::Agent)::Bool + return hasItems(agent.steering_queue) || hasItems(agent.follow_up_queue) +end + +""" + abort(agent) + +Abort the current run, if one is active. +""" +function abort(agent::Agent) + if !isnothing(agent.active_run) + # TODO: Implement abort signal + end +end + +""" + waitForIdle(agent) + +Resolve when the current run and all awaited event listeners have finished. +""" +function waitForIdle(agent::Agent)::Promise + if isnothing(agent.active_run) + return Promise() + end + return agent.active_run.promise +end + +""" + reset(agent) + +Clear transcript state, runtime state, and queued messages. +""" +function reset!(agent::Agent) + agent._state.messages = AgentMessage[] + agent._state.is_streaming = false + agent._state.streaming_message = nothing + agent._state.pending_tool_calls = Set{String}() + agent._state.error_message = nothing + clearFollowUpQueue(agent) + clearSteeringQueue(agent) +end + +""" + prompt(agent, input[, images]) + +Start a new prompt from text, a single message, or a batch of messages. +""" +function prompt(agent::Agent, input::Union{String, AgentMessage, Vector{AgentMessage}}, images::Vector{ImageContent}=ImageContent[])::Nothing + if !isnothing(agent.active_run) + throw(ErrorException( + "Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion." + )) + end + messages = normalizePromptInput(agent, input, images) + runPromptMessages(agent, messages) +end + +function normalizePromptInput(agent::Agent, input::Vector{AgentMessage}, images::Vector{ImageContent})::Vector{AgentMessage} + return input +end + +function normalizePromptInput(agent::Agent, input::AgentMessage, images::Vector{ImageContent})::Vector{AgentMessage} + return [input] +end + +function normalizePromptInput(agent::Agent, input::String, images::Vector{ImageContent})::Vector{AgentMessage} + content::Vector{MessageContent} = [TextContent(input)] + if !isempty(images) + append!(content, images) + end + return [UserMessage("user", content, Int64(Dates.now(Dates.UTC).datetime))] +end + +function runPromptMessages(agent::Agent, messages::Vector{AgentMessage})::Nothing + # TODO: Implement run with lifecycle + return nothing +end + +""" + continue(agent) + +Continue from the current transcript. The last message must be a user or tool-result message. +""" +function continue!(agent::Agent)::Nothing + if !isnothing(agent.active_run) + throw(ErrorException("Agent is already processing. Wait for completion before continuing.")) + end + + last_message = agent._state.messages[end] + if isnothing(last_message) + throw(ErrorException("No messages to continue from")) + end + + if last_message.role == "assistant" + queued_steering = drain(agent.steering_queue) + if !isempty(queued_steering) + runPromptMessages(agent, queued_steering) + return nothing + end + + queued_follow_ups = drain(agent.follow_up_queue) + if !isempty(queued_follow_ups) + runPromptMessages(agent, queued_follow_ups) + return nothing + end + + throw(ErrorException("Cannot continue from message role: assistant")) + end + + # TODO: Implement run continuation + return nothing +end + +""" + createContextSnapshot(agent) + +Create a snapshot of the current context for use in the agent loop. +""" +function createContextSnapshot(agent::Agent)::AgentContext + return AgentContext( + agent._state.system_prompt, + copy(agent._state.messages), + copy(agent._state.tools), + ) +end + +""" + createLoopConfig(agent, options) + +Create the loop configuration for the agent. +""" +function createLoopConfig(agent::Agent, options::Dict{String, Any}=Dict{String, Any}())::AgentLoopConfig + skip_initial_steering_poll = get(options, "skipInitialSteeringPoll", false) + return AgentLoopConfig( + agent._state.model, + agent._state.thinking_level == THINKING_OFF ? nothing : agent._state.thinking_level, + agent.session_id, + agent.on_payload, + agent.on_response, + agent.transport, + agent.thinking_budgets, + agent.max_retry_delay_ms, + agent.tool_execution, + agent.before_tool_call, + agent.after_tool_call, + isnothing(agent.prepare_next_turn_with_context) && isnothing(agent.prepare_next_turn) ? nothing : function(context) + if !isnothing(agent.prepare_next_turn_with_context) + return agent.prepare_next_turn_with_context(context, getSignal(agent)) + end + return isnothing(agent.prepare_next_turn) ? nothing : agent.prepare_next_turn(getSignal(agent)) + end, + agent.convert_to_llm, + agent.transform_context, + agent.get_api_key, + function() + if skip_initial_steering_poll + skip_initial_steering_poll = false + return AgentMessage[] + end + return drain(agent.steering_queue) + end, + function() + return drain(agent.follow_up_queue) + end, + ) +end + +""" + getSignal(agent) + +Get the active abort signal for the current run, if any. +""" +function getSignal(agent::Agent)::Union{Nothing, Base.Atomic{Bool}} + if isnothing(agent.active_run) + return nothing + end + return agent.active_run.abort_controller +end + +end diff --git a/packages/agent/julia_implementation/src/agent_loop.jl b/packages/agent/julia_implementation/src/agent_loop.jl new file mode 100644 index 00000000..456cec26 --- /dev/null +++ b/packages/agent/julia_implementation/src/agent_loop.jl @@ -0,0 +1,861 @@ +""" + agent_loop.jl - Low-level agent loop implementation + +This module implements the core agentLoop functionality that works with AgentMessage +throughout, transforming to Message[] only at the LLM call boundary. +""" + +module AgentLoop + +using ..Types: * +using ..StreamFn: * + +# ============================================================================ +# Event sink type +# ============================================================================ + +const AgentEventSink = Function + +# ============================================================================ +# Main agent loop function +# ============================================================================ + +function agentLoop( + prompts::Vector{AgentMessage}, + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::EventStream + stream = createAgentStream() + + Threads.@spawn begin + messages = runAgentLoop( + prompts, + context, + config, + (event) -> push!(stream, event), + signal, + stream_fn, + ) + end(stream, messages) + end + + return stream +end + +# ============================================================================ +# Continue agent loop function +# ============================================================================ + +function agentLoopContinue( + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::EventStream + if isempty(context.messages) + throw(ErrorException("Cannot continue: no messages in context")) + end + + if context.messages[end].role == "assistant" + throw(ErrorException("Cannot continue from message role: assistant")) + end + + stream = createAgentStream() + + Threads.@spawn begin + messages = runAgentLoopContinue( + context, + config, + (event) -> push!(stream, event), + signal, + stream_fn, + ) + end(stream, messages) + end + + return stream +end + +# ============================================================================ +# Run agent loop function +# ============================================================================ + +function runAgentLoop( + prompts::Vector{AgentMessage}, + context::AgentContext, + config::AgentLoopConfig, + emit::AgentEventSink, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::Vector{AgentMessage} + new_messages::Vector{AgentMessage} = copy(prompts) + current_context::AgentContext = AgentContext( + context.system_prompt, + vcat(context.messages, copy(prompts)), + context.tools, + ) + + emit(AgentStartEvent()) + emit(TurnStartEvent()) + for prompt in prompts + emit(MessageStartEvent(prompt)) + emit(MessageEndEvent(prompt)) + end + + runLoop( + current_context, + new_messages, + config, + signal, + emit, + stream_fn, + ) + return new_messages +end + +# ============================================================================ +# Run agent loop continue function +# ============================================================================ + +function runAgentLoopContinue( + context::AgentContext, + config::AgentLoopConfig, + emit::AgentEventSink, + signal::Union{Nothing, AbortSignal}, + stream_fn::StreamFn, +)::Vector{AgentMessage} + if isempty(context.messages) + throw(ErrorException("Cannot continue: no messages in context")) + end + + if context.messages[end].role == "assistant" + throw(ErrorException("Cannot continue from message role: assistant")) + end + + new_messages::Vector{AgentMessage} = [] + current_context::AgentContext = context + + emit(AgentStartEvent()) + emit(TurnStartEvent()) + + runLoop( + current_context, + new_messages, + config, + signal, + emit, + stream_fn, + ) + return new_messages +end + +# ============================================================================ +# Create agent stream function +# ============================================================================ + +function createAgentStream()::EventStream + return EventStream( + (event::AgentEvent) -> event isa AgentEndEvent, + (event::AgentEvent) -> event isa AgentEndEvent ? event.messages : AgentMessage[], + ) +end + +# ============================================================================ +# Main loop logic shared by agentLoop and agentLoopContinue +# ============================================================================ + +function runLoop( + initial_context::AgentContext, + new_messages::Vector{AgentMessage}, + initial_config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, + stream_function::StreamFn, +)::Nothing + current_context::AgentContext = initial_context + config::AgentLoopConfig = initial_config + first_turn::Bool = true + pending_messages::Vector{AgentMessage} = getSteeringMessages(config) do + get_steering_messages(config) + end + + while true + has_more_tool_calls::Bool = true + + while has_more_tool_calls || !isempty(pending_messages) + if !first_turn + emit(TurnStartEvent()) + else + first_turn = false + end + + if !isempty(pending_messages) + for message in pending_messages + emit(MessageStartEvent(message)) + emit(MessageEndEvent(message)) + push!(current_context.messages, message) + push!(new_messages, message) + end + pending_messages = AgentMessage[] + end + + message = streamAssistantResponse( + current_context, + config, + signal, + emit, + stream_function, + ) + push!(new_messages, message) + + if message.stop_reason in ("error", "aborted") + emit(TurnEndEvent(message, ToolResultMessage[])) + emit(AgentEndEvent(new_messages)) + return + end + + tool_calls = filter( + (c) -> c isa ToolCall, + message.content, + ) + + tool_results::Vector{ToolResultMessage} = [] + has_more_tool_calls = false + if !isempty(tool_calls) + executed_tool_batch = + message.stop_reason == "length" + ? failToolCallsFromTruncatedMessage(tool_calls, emit) + : executeToolCalls( + current_context, + message, + config, + signal, + emit, + ) + append!(tool_results, executed_tool_batch.messages) + has_more_tool_calls = !executed_tool_batch.terminate + + for result in tool_results + push!(current_context.messages, result) + push!(new_messages, result) + end + end + + emit(TurnEndEvent(message, tool_results)) + + next_turn_context = PrepareNextTurnContext( + message, + tool_results, + current_context, + new_messages, + ) + next_turn_snapshot = prepare_next_turn(config, next_turn_context) + + if !isnothing(next_turn_snapshot) + current_context = next_turn_snapshot.context + config = AgentLoopConfig( + model = next_turn_snapshot.model, + reasoning = next_turn_snapshot.thinking_level, + convert_to_llm = config.convert_to_llm, + transform_context = config.transform_context, + get_api_key = config.get_api_key, + should_stop_after_turn = config.should_stop_after_turn, + prepare_next_turn = config.prepare_next_turn, + get_steering_messages = config.get_steering_messages, + get_follow_up_messages = config.get_follow_up_messages, + tool_execution = config.tool_execution, + before_tool_call = config.before_tool_call, + after_tool_call = config.after_tool_call, + max_tokens = config.max_tokens, + temperature = config.temperature, + reasoning = config.reasoning, + cache_retention = config.cache_retention, + session_id = config.session_id, + headers = config.headers, + metadata = config.metadata, + transport = config.transport, + signal = signal, + api_key = config.api_key, + on_payload = config.on_payload, + on_response = config.on_response, + max_retry_delay_ms = config.max_retry_delay_ms, + ) + end + + if should_stop_after_turn(config, next_turn_context) + emit(AgentEndEvent(new_messages)) + return + end + + pending_messages = getSteeringMessages(config) do + get_steering_messages(config) + end + end + + follow_up_messages = getFollowUpMessages(config) do + get_follow_up_messages(config) + end + + if !isempty(follow_up_messages) + pending_messages = follow_up_messages + continue + end + + break + end + + emit(AgentEndEvent(new_messages)) +end + +# ============================================================================ +# Helper types +# ============================================================================ + +struct PrepareNextTurnContext + message::AssistantMessage + tool_results::Vector{ToolResultMessage} + context::AgentContext + new_messages::Vector{AgentMessage} +end + +struct AgentLoopTurnUpdate + context::Union{AgentContext, Nothing} + model::Union{Model, Nothing} + thinking_level::Union{ThinkingLevel, Nothing} +end + +# ============================================================================ +# Helper functions for getting messages from queues +# ============================================================================ + +macro getSteeringMessages(config) + :(get_steering_messages($(esc(config)))) +end + +macro getFollowUpMessages(config) + :(get_follow_up_messages($(esc(config)))) +end + +function get_steering_messages(config::AgentLoopConfig)::Vector{AgentMessage} + return isnothing(config.get_steering_messages) ? AgentMessage[] : config.get_steering_messages() +end + +function get_follow_up_messages(config::AgentLoopConfig)::Vector{AgentMessage} + return isnothing(config.get_follow_up_messages) ? AgentMessage[] : config.get_follow_up_messages() +end + +function prepare_next_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Union{AgentLoopTurnUpdate, Nothing} + return isnothing(config.prepare_next_turn) ? nothing : config.prepare_next_turn(context) +end + +function should_stop_after_turn(config::AgentLoopConfig, context::PrepareNextTurnContext)::Bool + return isnothing(config.should_stop_after_turn) ? false : config.should_stop_after_turn(context) +end + +# ============================================================================ +# Stream assistant response function +# ============================================================================ + +function streamAssistantResponse( + context::AgentContext, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, + stream_function::StreamFn, +)::AssistantMessage + messages::Vector{AgentMessage} = context.messages + + if !isnothing(config.transform_context) + messages = config.transform_context(messages, signal) + end + + llm_messages::Vector{Message} = config.convert_to_llm(messages) + + llm_context::Context = Context( + context.system_prompt, + llm_messages, + context.tools, + ) + + resolved_api_key::Union{String, Nothing} = + !isnothing(config.get_api_key) + ? config.get_api_key(config.model.provider) + : nothing + + response = stream_function( + config.model, + llm_context, + merge( + config, + Dict(:apiKey => resolved_api_key, :signal => signal), + ), + ) + + partial_message::Union{AssistantMessage, Nothing} = nothing + added_partial::Bool = false + + for event in response + if event.type == "start" + partial_message = event.partial + push!(context.messages, partial_message) + added_partial = true + emit(MessageStartEvent(copy(partial_message))) + elseif event.type in ("text_start", "text_delta", "text_end", "thinking_start", "thinking_delta", "thinking_end", "toolcall_start", "toolcall_delta", "toolcall_end") + if !isnothing(partial_message) + partial_message = event.partial + context.messages[end] = partial_message + emit(MessageUpdateEvent(copy(partial_message), event)) + end + elseif event.type in ("done", "error") + final_message = response.result() + if added_partial + context.messages[end] = final_message + else + push!(context.messages, final_message) + end + if !added_partial + emit(MessageStartEvent(copy(final_message))) + end + emit(MessageEndEvent(final_message)) + return final_message + end + end + + final_message = response.result() + if added_partial + context.messages[end] = final_message + else + push!(context.messages, final_message) + emit(MessageStartEvent(copy(final_message))) + end + emit(MessageEndEvent(final_message)) + return final_message +end + +# ============================================================================ +# Fail tool calls from truncated message +# ============================================================================ + +struct ExecutedToolCallBatch + messages::Vector{ToolResultMessage} + terminate::Bool +end + +function failToolCallsFromTruncatedMessage( + tool_calls::Vector{ToolCall}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + messages::Vector{ToolResultMessage} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + finalized = FinalizedToolCallOutcome( + tool_call, + createErrorToolResult( + "Tool call \"$(tool_call.name)\" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.", + ), + true, + ) + + emitToolExecutionEnd(finalized, emit) + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(messages, tool_result_message) + end + + return ExecutedToolCallBatch(messages, false) +end + +# ============================================================================ +# Execute tool calls +# ============================================================================ + +function executeToolCalls( + current_context::AgentContext, + assistant_message::AssistantMessage, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + tool_calls = filter( + (c) -> c isa ToolCall, + assistant_message.content, + ) + + has_sequential_tool_call = any( + (tc) -> begin + tool = findfirst((t) -> t.name == tc.name, current_context.tools) + !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL + end, + tool_calls, + ) + + if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential_tool_call + return executeToolCallsSequential( + current_context, + assistant_message, + tool_calls, + config, + signal, + emit, + ) + end + return executeToolCallsParallel( + current_context, + assistant_message, + tool_calls, + config, + signal, + emit, + ) +end + +# ============================================================================ +# Execute tool calls sequentially +# ============================================================================ + +function executeToolCallsSequential( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_calls::Vector{ToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + finalized_calls::Vector{FinalizedToolCallOutcome} = [] + messages::Vector{ToolResultMessage} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) + + finalized = if preparation.kind == "immediate" + FinalizedToolCallOutcome(tool_call, preparation.result, preparation.is_error) + else + executed = executePreparedToolCall(preparation, signal, emit) + finalizeExecutedToolCall( + current_context, + assistant_message, + preparation, + executed, + config, + signal, + ) + end + + emitToolExecutionEnd(finalized, emit) + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(finalized_calls, finalized) + push!(messages, tool_result_message) + + if !isnothing(signal) && signal.aborted + break + end + end + + return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(finalized_calls)) +end + +# ============================================================================ +# Execute tool calls in parallel +# ============================================================================ + +function executeToolCallsParallel( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_calls::Vector{ToolCall}, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallBatch + finalized_calls::Vector{Union{FinalizedToolCallOutcome, Function}} = [] + + for tool_call in tool_calls + emit(ToolExecutionStartEvent(tool_call.id, tool_call.name, tool_call.arguments)) + + preparation = prepareToolCall(current_context, assistant_message, tool_call, config, signal) + + if preparation.kind == "immediate" + finalized = FinalizedToolCallOutcome( + tool_call, + preparation.result, + preparation.is_error, + ) + emitToolExecutionEnd(finalized, emit) + push!(finalized_calls, finalized) + if !isnothing(signal) && signal.aborted + break + end + continue + end + + push!(finalized_calls, () -> begin + executed = executePreparedToolCall(preparation, signal, emit) + finalized = finalizeExecutedToolCall( + current_context, + assistant_message, + preparation, + executed, + config, + signal, + ) + emitToolExecutionEnd(finalized, emit) + return finalized + end) + + if !isnothing(signal) && signal.aborted + break + end + end + + ordered_finalized_calls = map( + (entry) -> if entry isa Function + entry() + else + entry + end, + finalized_calls, + ) + + messages::Vector{ToolResultMessage} = [] + for finalized in ordered_finalized_calls + tool_result_message = createToolResultMessage(finalized) + emitToolResultMessage(tool_result_message, emit) + push!(messages, tool_result_message) + end + + return ExecutedToolCallBatch(messages, shouldTerminateToolBatch(ordered_finalized_calls)) +end + +# ============================================================================ +# Prepared tool call types +# ============================================================================ + +struct PreparedToolCall + kind::String + tool_call::ToolCall + tool::AgentTool + args::Any +end + +struct ImmediateToolCallOutcome + kind::String + result::AgentToolResultMutable + is_error::Bool +end + +struct ExecutedToolCallOutcome + result::AgentToolResultMutable + is_error::Bool +end + +struct FinalizedToolCallOutcome + tool_call::ToolCall + result::AgentToolResultMutable + is_error::Bool +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function shouldTerminateToolBatch(finalized_calls::Vector{FinalizedToolCallOutcome})::Bool + return !isempty(finalized_calls) && all( + (finalized) -> finalized.result.terminate === true, + finalized_calls, + ) +end + +function prepareToolCallArguments(tool::AgentTool, tool_call::ToolCall)::ToolCall + if isnothing(tool.prepare_arguments) + return tool_call + end + prepared_arguments = tool.prepare_arguments(tool_call.arguments) + if prepared_arguments === tool_call.arguments + return tool_call + end + return ToolCall( + tool_call.type, + tool_call.id, + tool_call.name, + prepared_arguments, + tool_call.partial_json, + ) +end + +function prepareToolCall( + current_context::AgentContext, + assistant_message::AssistantMessage, + tool_call::ToolCall, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, +)::Union{PreparedToolCall, ImmediateToolCallOutcome} + tool = findfirst((t) -> t.name == tool_call.name, current_context.tools) + if isnothing(tool) + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Tool $(tool_call.name) not found"), true) + end + + try + prepared_tool_call = prepareToolCallArguments(tool, tool_call) + validated_args = validateToolArguments(tool, prepared_tool_call) + + if !isnothing(config.before_tool_call) + before_result = config.before_tool_call( + BeforeToolCallContext(assistant_message, tool_call, validated_args, current_context), + signal, + ) + if !isnothing(signal) && signal.aborted + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) + end + if !isnothing(before_result) && before_result.block + reason = isnothing(before_result.reason) ? "Tool execution was blocked" : before_result.reason + return ImmediateToolCallOutcome("immediate", createErrorToolResult(reason), true) + end + end + + if !isnothing(signal) && signal.aborted + return ImmediateToolCallOutcome("immediate", createErrorToolResult("Operation aborted"), true) + end + + return PreparedToolCall("prepared", tool_call, tool, validated_args) + catch error + return ImmediateToolCallOutcome("immediate", createErrorToolResult(string(error)), true) + end +end + +function executePreparedToolCall( + prepared::PreparedToolCall, + signal::Union{Nothing, AbortSignal}, + emit::AgentEventSink, +)::ExecutedToolCallOutcome + update_events::Vector{Future} = [] + accepting_updates::Bool = true + + try + result = prepared.tool.execute( + prepared.tool_call.id, + prepared.args, + signal, + (partial_result) -> begin + if !accepting_updates + return + end + push!( + update_events, + Threads.@spawn begin + emit( + ToolExecutionUpdateEvent( + prepared.tool_call.id, + prepared.tool_call.name, + prepared.tool_call.arguments, + partial_result, + ), + ) + end, + ) + end, + ) + accepting_updates = false + wait.(update_events) + return ExecutedToolCallOutcome(result, false) + catch error + accepting_updates = false + wait.(update_events) + return ExecutedToolCallOutcome(createErrorToolResult(string(error)), true) + finally + accepting_updates = false + end +end + +function finalizeExecutedToolCall( + current_context::AgentContext, + assistant_message::AssistantMessage, + prepared::PreparedToolCall, + executed::ExecutedToolCallOutcome, + config::AgentLoopConfig, + signal::Union{Nothing, AbortSignal}, +)::FinalizedToolCallOutcome + result = executed.result + is_error = executed.is_error + + if !isnothing(config.after_tool_call) + try + after_result = config.after_tool_call( + AfterToolCallContext( + assistant_message, + prepared.tool_call, + prepared.args, + result, + is_error, + current_context, + ), + signal, + ) + if !isnothing(after_result) + result = AgentToolResultMutable( + isnothing(after_result.content) ? result.content : after_result.content, + isnothing(after_result.details) ? result.details : after_result.details, + isnothing(after_result.usage) ? result.usage : after_result.usage, + result.added_tool_names, + isnothing(after_result.terminate) ? result.terminate : after_result.terminate, + ) + is_error = isnothing(after_result.is_error) ? is_error : after_result.is_error + end + catch error + result = createErrorToolResult(string(error)) + is_error = true + end + end + + return FinalizedToolCallOutcome(prepared.tool_call, result, is_error) +end + +function createErrorToolResult(message::String)::AgentToolResultMutable + return AgentToolResultMutable([TextContent(message)], Dict{String, Any}(), nothing, nothing, nothing) +end + +function emitToolExecutionEnd(finalized::FinalizedToolCallOutcome, emit::AgentEventSink)::Nothing + emit(ToolExecutionEndEvent( + finalized.tool_call.id, + finalized.tool_call.name, + finalized.result, + finalized.is_error, + )) + return nothing +end + +function createToolResultMessage(finalized::FinalizedToolCallOutcome)::ToolResultMessage + return ToolResultMessage( + "toolResult", + finalized.tool_call.id, + finalized.tool_call.name, + isnothing(finalized.result.content) ? MessageContent[] : finalized.result.content, + finalized.result.details, + finalized.result.usage, + finalized.result.added_tool_names, + finalized.is_error, + Dates.now(Dates.UTC).datetime, + ) +end + +function emitToolResultMessage(tool_result_message::ToolResultMessage, emit::AgentEventSink)::Nothing + emit(MessageStartEvent(tool_result_message)) + emit(MessageEndEvent(tool_result_message)) + return nothing +end + +# ============================================================================ +# Validation helper +# ============================================================================ + +function validateToolArguments(tool::AgentTool, tool_call::ToolCall)::Any + # Simplified validation - in a full implementation, this would use TypeBox-like validation + return tool_call.arguments +end + +end diff --git a/packages/agent/julia_implementation/src/harness_types.jl b/packages/agent/julia_implementation/src/harness_types.jl new file mode 100644 index 00000000..0a583cdd --- /dev/null +++ b/packages/agent/julia_implementation/src/harness_types.jl @@ -0,0 +1,1083 @@ +""" + harness_types.jl - Extended types for AgentHarness + +This module defines the extended types used by the AgentHarness. +""" + +module HarnessTypes + +using ..Types: * +using ..Session: Session + +# ============================================================================ +# Result type +# ============================================================================ + +abstract type Result{TValue, TError} end + +struct Ok{TValue, TError} <: Result{TValue, TError} + value::TValue +end + +struct Err{TValue, TError} <: Result{TValue, TError} + error::TError +end + +function ok{TValue, TError}(value::TValue)::Ok{TValue, TError} + return Ok{TValue, TError}(value) +end + +function err{TValue, TError}(error::TError)::Err{TValue, TError} + return Err{TValue, TError}(error) +end + +function getOrThrow{TValue, TError}(result::Result{TValue, TError})::TValue + if result isa Ok + return result.value + else + throw(result.error) + end +end + +function getOrUndefined{TValue<:AbstractDict, TError}(result::Result{TValue, TError})::Union{TValue, Nothing} + if result isa Ok + return result.value + else + return nothing + end +end + +function toError(error::Any)::Error + if error isa Error + return error + elseif error isa AbstractString + return ErrorException(error) + else + try + return ErrorException(string(error)) + catch + return ErrorException("Unknown error") + end + end +end + +# ============================================================================ +# Skill types +# ============================================================================ + +mutable struct Skill + name::String + description::String + content::String + filePath::String + disableModelInvocation::Bool +end + +mutable struct PromptTemplate + name::String + description::Union{String, Nothing} + content::String +end + +mutable struct AgentHarnessResources{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + promptTemplates::Union{Vector{TPromptTemplate}, Nothing} + skills::Union{Vector{TSkill}, Nothing} +end + +# ============================================================================ +# Tool types +# ============================================================================ + +mutable struct AgentHarnessTool{TContext, TParameters, TDetails} + name::String + label::String + description::String + parameters::TParameters + execute::Function + prepareArguments::Union{Function, Nothing} + executionMode::Union{ToolExecutionMode, Nothing} +end + +mutable struct AgentHarnessToolContextSource{TContext} + context::Union{TContext, Function} +end + +# ============================================================================ +# Stream options +# ============================================================================ + +mutable struct AgentHarnessStreamOptions + transport::Union{String, Nothing} + timeout_ms::Union{Int64, Nothing} + max_retries::Union{Int64, Nothing} + max_retry_delay_ms::Union{Int64, Nothing} + headers::Union{Dict{String, String}, Nothing} + metadata::Union{Dict{String, Any}, Nothing} + cache_retention::Union{String, Nothing} +end + +mutable struct AgentHarnessStreamOptionsPatch + transport::Union{String, Nothing} + timeout_ms::Union{Int64, Nothing} + max_retries::Union{Int64, Nothing} + max_retry_delay_ms::Union{Int64, Nothing} + cache_retention::Union{String, Nothing} + headers::Union{Dict{String, String}, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# File system types +# ============================================================================ + +const FileKind = String +const FILE_KIND_FILE = "file" +const FILE_KIND_DIRECTORY = "directory" +const FILE_KIND_SYMLINK = "symlink" + +const FileErrorCode = String +const FILE_ERROR_ABORTED = "aborted" +const FILE_ERROR_NOT_FOUND = "not_found" +const FILE_ERROR_PERMISSION_DENIED = "permission_denied" +const FILE_ERROR_NOT_DIRECTORY = "not_directory" +const FILE_ERROR_IS_DIRECTORY = "is_directory" +const FILE_ERROR_INVALID = "invalid" +const FILE_ERROR_NOT_SUPPORTED = "not_supported" +const FILE_ERROR_UNKNOWN = "unknown" + +mutable struct FileError <: Exception + code::FileErrorCode + message::String + path::Union{String, Nothing} + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Execution error types +# ============================================================================ + +const ExecutionErrorCode = String +const EXECUTION_ERROR_ABORTED = "aborted" +const EXECUTION_ERROR_TIMEOUT = "timeout" +const EXECUTION_ERROR_SHELL_UNAVAILABLE = "shell_unavailable" +const EXECISION_ERROR_SPAWN_ERROR = "spawn_error" +const EXECUTION_ERROR_CALLBACK_ERROR = "callback_error" +const EXECUTION_ERROR_UNKNOWN = "unknown" + +mutable struct ExecutionError <: Exception + code::ExecutionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Compaction error types +# ============================================================================ + +const CompactionErrorCode = String +const COMPACTION_ERROR_ABORTED = "aborted" +const COMPACTION_ERROR_SUMMARIZATION_FAILED = "summarization_failed" +const COMPACTION_ERROR_INVALID_SESSION = "invalid_session" +const COMPACTION_ERROR_UNKNOWN = "unknown" + +mutable struct CompactionError <: Exception + code::CompactionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Branch summary error types +# ============================================================================ + +const BranchSummaryErrorCode = String +const BRANCH_SUMMARY_ERROR_ABORTED = "aborted" +const BRANCH_SUMMARY_ERROR_SUMMARIZATION_FAILED = "summarization_failed" +const BRANCH_SUMMARY_ERROR_INVALID_SESSION = "invalid_session" + +mutable struct BranchSummaryError <: Exception + code::BranchSummaryErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Session error types +# ============================================================================ + +const SessionErrorCode = String +const SESSION_ERROR_NOT_FOUND = "not_found" +const SESSION_ERROR_INVALID_SESSION = "invalid_session" +const SESSION_ERROR_INVALID_ENTRY = "invalid_entry" +const SESSION_ERROR_INVALID_FORK_TARGET = "invalid_fork_target" +const SESSION_ERROR_STORAGE = "storage" +const SESSION_ERROR_UNKNOWN = "unknown" + +mutable struct SessionError <: Exception + code::SessionErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Agent harness error types +# ============================================================================ + +const AgentHarnessErrorCode = String +const AGENT_HARNESS_ERROR_BUSY = "busy" +const AGENT_HARNESS_ERROR_INVALID_STATE = "invalid_state" +const AGENT_HARNESS_ERROR_INVALID_ARGUMENT = "invalid_argument" +const AGENT_HARNESS_ERROR_SESSION = "session" +const AGENT_HARNESS_ERROR_HOOK = "hook" +const AGENT_HARNESS_ERROR_AUTH = "auth" +const AGENT_HARNESS_ERROR_COMPACTION = "compaction" +const AGENT_HARNESS_ERROR_BRANCH_SUMMARY = "branch_summary" +const AGENT_HARNESS_ERROR_UNKNOWN = "unknown" + +mutable struct AgentHarnessError <: Exception + code::AgentHarnessErrorCode + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# File system interface +# ============================================================================ + +abstract type FileSystem end + +function absolutePath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "absolutePath not implemented", path, nothing)) +end + +function joinPath(fs::FileSystem, parts::Vector{String}, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "joinPath not implemented", nothing, nothing)) +end + +function readTextFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "readTextFile not implemented", path, nothing)) +end + +function readTextLines( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Vector{String}, FileError} + return err(FileError("not_supported", "readTextLines not implemented", path, nothing)) +end + +function readBinaryFile(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{UInt8}, FileError} + return err(FileError("not_supported", "readBinaryFile not implemented", path, nothing)) +end + +function writeFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} + return err(FileError("not_supported", "writeFile not implemented", path, nothing)) +end + +function appendFile(fs::FileSystem, path::String, content::Union{String, Vector{UInt8}}, abortSignal::Union{Nothing, Any})::Result{Nothing, FileError} + return err(FileError("not_supported", "appendFile not implemented", path, nothing)) +end + +function fileInfo(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{FileInfo, FileError} + return err(FileError("not_supported", "fileInfo not implemented", path, nothing)) +end + +function listDir(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Vector{FileInfo}, FileError} + return err(FileError("not_supported", "listDir not implemented", path, nothing)) +end + +function canonicalPath(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "canonicalPath not implemented", path, nothing)) +end + +function exists(fs::FileSystem, path::String, abortSignal::Union{Nothing, Any})::Result{Bool, FileError} + return err(FileError("not_supported", "exists not implemented", path, nothing)) +end + +function createDir( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Nothing, FileError} + return err(FileError("not_supported", "createDir not implemented", path, nothing)) +end + +function remove( + fs::FileSystem, + path::String, + options::Dict{String, Any}, +)::Result{Nothing, FileError} + return err(FileError("not_supported", "remove not implemented", path, nothing)) +end + +function createTempDir(fs::FileSystem, prefix::String="tmp-", abortSignal::Union{Nothing, Any})::Result{String, FileError} + return err(FileError("not_supported", "createTempDir not implemented", nothing, nothing)) +end + +function createTempFile(fs::FileSystem, options::Dict{String, Any})::Result{String, FileError} + return err(FileError("not_supported", "createTempFile not implemented", nothing, nothing)) +end + +function cleanup(fs::FileSystem)::Nothing + return nothing +end + +# ============================================================================ +# Shell interface +# ============================================================================ + +mutable struct ShellExecOptions + cwd::Union{String, Nothing} + env::Union{Dict{String, String}, Nothing} + inheritEnv::Bool + timeout::Union{Int64, Nothing} + abortSignal::Union{Any, Nothing} + onStdout::Union{Function, Nothing} + onStderr::Union{Function, Nothing} +end + +abstract type Shell end + +function exec(shell::Shell, command::String, options::Dict{String, Any})::Result{Dict{String, Any}, ExecutionError} + return err(ExecutionError("not_supported", "exec not implemented", nothing)) +end + +function cleanup(shell::Shell)::Nothing + return nothing +end + +# ============================================================================ +# Execution environment +# ============================================================================ + +abstract type ExecutionEnv <: FileSystem, Shell end + +# ============================================================================ +# Session tree entry types +# ============================================================================ + +abstract type SessionTreeEntry end + +struct MessageEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + message::AgentMessage +end + +struct ThinkingLevelChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + thinking_level::String +end + +struct ModelChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + provider::String + model_id::String +end + +struct ActiveToolsChangeEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + active_tool_names::Vector{String} +end + +struct CompactionEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{Any, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct BranchSummaryEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + from_id::String + summary::String + details::Union{Any, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct CustomEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + custom_type::String + data::Union{Any, Nothing} +end + +struct CustomMessageEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + custom_type::String + content::String + details::Union{Any, Nothing} + display::Bool +end + +struct LabelEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + target_id::String + label::Union{String, Nothing} +end + +struct SessionInfoEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + name::Union{String, Nothing} +end + +struct LeafEntry <: SessionTreeEntry + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String + target_id::Union{String, Nothing} +end + +# ============================================================================ +# Session context +# ============================================================================ + +struct SessionContext + messages::Vector{AgentMessage} + thinking_level::String + model::Union{Dict{String, String}, Nothing} + active_tool_names::Union{Vector{String}, Nothing} +end + +# ============================================================================ +# Session stats +# ============================================================================ + +struct SessionStats + message_count::Int64 + cached_tokens::Int64 + uncached_tokens::Int64 + total_tokens::Int64 + cost_total::Float64 +end + +# ============================================================================ +# Session metadata +# ============================================================================ + +mutable struct SessionMetadata + id::String + created_at::String +end + +mutable struct JsonlSessionMetadata <: SessionMetadata + id::String + created_at::String + cwd::String + path::String + parent_session_path::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# Session storage interface +# ============================================================================ + +abstract type SessionStorage{T<:SessionMetadata} end + +function getMetadata(storage::SessionStorage)::Promise{T} + return Promise() +end + +function getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}} + return Promise() +end + +function setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing} + return Promise() +end + +function createEntryId(storage::SessionStorage)::Promise{String} + return Promise() +end + +function appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing} + return Promise() +end + +function getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}} + return Promise() +end + +function findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +function getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}} + return Promise() +end + +function getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}} + return Promise() +end + +function getSessionStats(storage::SessionStorage)::Promise{SessionStats} + return Promise() +end + +function getPathToRootOrCompaction(storage::SessionStorage, leaf_id::String)::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +function getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}} + return Promise() +end + +# ============================================================================ +# Session repo interface +# ============================================================================ + +abstract type SessionRepo< + TMetadata<:SessionMetadata, + TCreateOptions, + TListOptions +> end + +function create(repo::SessionRepo, options::TCreateOptions)::Promise{Session} + return Promise() +end + +function open(repo::SessionRepo, metadata::TMetadata)::Promise{Session} + return Promise() +end + +function list(repo::SessionRepo, options::TListOptions)::Promise{Vector{TMetadata}} + return Promise() +end + +function delete(repo::SessionRepo, metadata::TMetadata)::Promise{Nothing} + return Promise() +end + +function fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Promise{Session} + return Promise() +end + +# ============================================================================ +# Pending session write +# ============================================================================ + +mutable struct PendingSessionWrite + type::String + message::Union{AgentMessage, Nothing} + provider::Union{String, Nothing} + model_id::Union{String, Nothing} + thinking_level::Union{String, Nothing} + active_tool_names::Union{Vector{String}, Nothing} + custom_type::Union{String, Nothing} + data::Union{Any, Nothing} + content::Union{String, Nothing} + display::Union{Bool, Nothing} + target_id::Union{String, Nothing} + label::Union{String, Nothing} + name::Union{String, Nothing} +end + +# ============================================================================ +# Queue update event +# ============================================================================ + +mutable struct QueueUpdateEvent + type::String + steer::Vector{AgentMessage} + followUp::Vector{AgentMessage} + nextTurn::Vector{AgentMessage} +end + +# ============================================================================ +# Save point event +# ============================================================================ + +mutable struct SavePointEvent + type::String + had_pending_mutations::Bool +end + +# ============================================================================ +# Abort event +# ============================================================================ + +mutable struct AbortEvent + type::String + cleared_steer::Vector{AgentMessage} + cleared_follow_up::Vector{AgentMessage} +end + +# ============================================================================ +# Settled event +# ============================================================================ + +mutable struct SettledEvent + type::String + next_turn_count::Int64 +end + +# ============================================================================ +# Before agent start event +# ============================================================================ + +mutable struct BeforeAgentStartEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + type::String + prompt::String + images::Union{Vector{ImageContent}, Nothing} + system_prompt::String + resources::AgentHarnessResources{TSkill, TPromptTemplate} +end + +# ============================================================================ +# Context event +# ============================================================================ + +mutable struct ContextEvent + type::String + messages::Vector{AgentMessage} +end + +# ============================================================================ +# Before provider request event +# ============================================================================ + +mutable struct BeforeProviderRequestEvent + type::String + model::Model + session_id::String + stream_options::AgentHarnessStreamOptions +end + +# ============================================================================ +# Before provider payload event +# ============================================================================ + +mutable struct BeforeProviderPayloadEvent + type::String + model::Model + payload::Any +end + +# ============================================================================ +# After provider response event +# ============================================================================ + +mutable struct AfterProviderResponseEvent + type::String + status::Int64 + headers::Dict{String, String} +end + +# ============================================================================ +# Tool call event +# ============================================================================ + +mutable struct ToolCallEvent + type::String + tool_call_id::String + tool_name::String + input::Dict{String, Any} +end + +# ============================================================================ +# Tool result event +# ============================================================================ + +mutable struct ToolResultEvent + type::String + tool_call_id::String + tool_name::String + input::Dict{String, Any} + content::Vector{MessageContent} + details::Any + is_error::Bool + usage::Union{Usage, Nothing} +end + +# ============================================================================ +# Session before compact event +# ============================================================================ + +mutable struct SessionBeforeCompactEvent + type::String + preparation::Any + branch_entries::Vector{SessionTreeEntry} + custom_instructions::Union{String, Nothing} + signal::Any +end + +# ============================================================================ +# Session compact event +# ============================================================================ + +mutable struct SessionCompactEvent + type::String + compaction_entry::CompactionEntry + from_hook::Bool +end + +# ============================================================================ +# Session before tree event +# ============================================================================ + +mutable struct SessionBeforeTreeEvent + type::String + preparation::Any + signal::Any +end + +# ============================================================================ +# Session tree event +# ============================================================================ + +mutable struct SessionTreeEvent + type::String + new_leaf_id::Union{String, Nothing} + old_leaf_id::Union{String, Nothing} + summary_entry::Union{BranchSummaryEntry, Nothing} + from_hook::Union{Bool, Nothing} +end + +# ============================================================================ +# Retry scheduled event +# ============================================================================ + +mutable struct RetryScheduledEvent + type::String + operation::String + attempt::Int64 + max_attempts::Int64 + delay_ms::Int64 + error_message::String +end + +# ============================================================================ +# Retry attempt start event +# ============================================================================ + +mutable struct RetryAttemptStartEvent + type::String + operation::String +end + +# ============================================================================ +# Retry finished event +# ============================================================================ + +mutable struct RetryFinishedEvent + type::String + operation::String +end + +# ============================================================================ +# Model update event +# ============================================================================ + +mutable struct ModelUpdateEvent + type::String + model::Model + previous_model::Union{Model, Nothing} + source::String +end + +# ============================================================================ +# Thinking level update event +# ============================================================================ + +mutable struct ThinkingLevelUpdateEvent + type::String + level::ThinkingLevel + previous_level::ThinkingLevel +end + +# ============================================================================ +# Tools update event +# ============================================================================ + +mutable struct ToolsUpdateEvent + type::String + tool_names::Vector{String} + previous_tool_names::Vector{String} + active_tool_names::Vector{String} + previous_active_tool_names::Vector{String} + source::String +end + +# ============================================================================ +# Resources update event +# ============================================================================ + +mutable struct ResourcesUpdateEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} + type::String + resources::AgentHarnessResources{TSkill, TPromptTemplate} + previous_resources::AgentHarnessResources{TSkill, TPromptTemplate} +end + +# ============================================================================ +# Agent harness own events +# ============================================================================ + +abstract type AgentHarnessOwnEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} end + +# ============================================================================ +# Agent harness event +# ============================================================================ + +abstract type AgentHarnessEvent{TSkill<:Skill, TPromptTemplate<:PromptTemplate} <: AgentEvent, AgentHarnessOwnEvent{TSkill, TPromptTemplate} end + +# ============================================================================ +# Before agent start result +# ============================================================================ + +mutable struct BeforeAgentStartResult + messages::Union{Vector{AgentMessage}, Nothing} + system_prompt::Union{String, Nothing} +end + +# ============================================================================ +# Context result +# ============================================================================ + +mutable struct ContextResult + messages::Vector{AgentMessage} +end + +# ============================================================================ +# Before provider request result +# ============================================================================ + +mutable struct BeforeProviderRequestResult + stream_options::Union{AgentHarnessStreamOptionsPatch, Nothing} +end + +# ============================================================================ +# Before provider payload result +# ============================================================================ + +mutable struct BeforeProviderPayloadResult + payload::Any +end + +# ============================================================================ +# Tool call result +# ============================================================================ + +mutable struct ToolCallResult + block::Union{Bool, Nothing} + reason::Union{String, Nothing} +end + +# ============================================================================ +# Tool result patch +# ============================================================================ + +mutable struct ToolResultPatch + content::Union{Vector{MessageContent}, Nothing} + details::Union{Any, Nothing} + is_error::Union{Bool, Nothing} + usage::Union{Usage, Nothing} + terminate::Union{Bool, Nothing} +end + +# ============================================================================ +# Session before compact result +# ============================================================================ + +mutable struct SessionBeforeCompactResult + cancel::Union{Bool, Nothing} + compaction::Union{CompactResult, Nothing} +end + +# ============================================================================ +# Session before tree result +# ============================================================================ + +mutable struct SessionBeforeTreeResult + cancel::Union{Bool, Nothing} + summary::Union{Dict{String, Any}, Nothing} + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + label::Union{String, Nothing} +end + +# ============================================================================ +# Agent harness event result map +# ============================================================================ + +# ============================================================================ +# Agent harness prompt options +# ============================================================================ + +mutable struct AgentHarnessPromptOptions + images::Union{Vector{ImageContent}, Nothing} +end + +# ============================================================================ +# Abort result +# ============================================================================ + +mutable struct AbortResult + cleared_steer::Vector{AgentMessage} + cleared_follow_up::Vector{AgentMessage} +end + +# ============================================================================ +# Compact result +# ============================================================================ + +mutable struct CompactResult + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + usage::Union{Usage, Nothing} + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{Any, Nothing} +end + +# ============================================================================ +# Navigate tree result +# ============================================================================ + +mutable struct NavigateTreeResult + cancelled::Bool + editor_text::Union{String, Nothing} + summary_entry::Union{BranchSummaryEntry, Nothing} +end + +# ============================================================================ +# Compaction settings +# ============================================================================ + +mutable struct CompactionSettings + enabled::Bool + reserve_tokens::Int64 + keep_recent_tokens::Int64 +end + +const DEFAULT_COMPACTION_SETTINGS = CompactionSettings(true, 16384, 20000) + +# ============================================================================ +# Compaction preparation +# ============================================================================ + +mutable struct CompactionPreparation + first_kept_entry_id::String + messages_to_summarize::Vector{AgentMessage} + turn_prefix_messages::Vector{AgentMessage} + retained_tail::Vector{AgentMessage} + is_split_turn::Bool + tokens_before::Int64 + previous_summary::Union{String, Nothing} + file_ops::Any + settings::CompactionSettings +end + +# ============================================================================ +# File operations +# ============================================================================ + +mutable struct FileOperations + read::Set{String} + written::Set{String} + edited::Set{String} +end + +# ============================================================================ +# Tree preparation +# ============================================================================ + +mutable struct TreePreparation + target_id::String + old_leaf_id::Union{String, Nothing} + common_ancestor_id::Union{String, Nothing} + entries_to_summarize::Vector{SessionTreeEntry} + user_wants_summary::Bool + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + label::Union{String, Nothing} +end + +# ============================================================================ +# Generate branch summary options +# ============================================================================ + +mutable struct GenerateBranchSummaryOptions + model::Model + api_key::String + headers::Union{Dict{String, String}, Nothing} + signal::Any + custom_instructions::Union{String, Nothing} + replace_instructions::Union{Bool, Nothing} + reserve_tokens::Int64 +end + +# ============================================================================ +# Branch summary result +# ============================================================================ + +mutable struct BranchSummaryResult + summary::String + usage::Union{Usage, Nothing} + read_files::Vector{String} + modified_files::Vector{String} +end + +# ============================================================================ +# Agent harness system prompt +# ============================================================================ + +mutable struct AgentHarnessSystemPrompt{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} + value::Union{String, Function} +end + +# ============================================================================ +# Agent harness options +# ============================================================================ + +mutable struct AgentHarnessOptions{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool} + session::Session + models::Any + tools::Union{Vector{TTool}, Nothing} + resources::Union{AgentHarnessResources{TSkill, TPromptTemplate}, Nothing} + system_prompt::Union{AgentHarnessSystemPrompt{TC, TSkill, TPromptTemplate, TTool}, Nothing} + stream_options::Union{AgentHarnessStreamOptions, Nothing} + retry::Union{Any, Nothing} + model::Model + thinking_level::Union{ThinkingLevel, Nothing} + active_tool_names::Union{Vector{String}, Nothing} + steering_mode::Union{QueueMode, Nothing} + follow_up_mode::Union{QueueMode, Nothing} + tool_context::Union{AgentHarnessToolContextSource{TC}, Nothing} +end + +end diff --git a/packages/agent/julia_implementation/src/messages.jl b/packages/agent/julia_implementation/src/messages.jl new file mode 100644 index 00000000..bf29b07f --- /dev/null +++ b/packages/agent/julia_implementation/src/messages.jl @@ -0,0 +1,183 @@ +""" + messages.jl - Custom message types and LLM conversion + +This module provides custom message types and the convertToLlm function. +""" + +module Messages + +using ..Types: * + +const COMPACTION_SUMMARY_PREFIX = """The conversation history before this point was compacted into the following summary: + + +""" + +const COMPACTION_SUMMARY_SUFFIX = """ +""" + +const BRANCH_SUMMARY_PREFIX = """The following is a summary of a branch that this conversation came back from: + + +""" + +const BRANCH_SUMMARY_SUFFIX = """""" + +# ============================================================================ +# Custom message types +# ============================================================================ + +mutable struct BashExecutionMessage + role::String + command::String + output::String + exit_code::Union{Int64, Nothing} + cancelled::Bool + truncated::Bool + full_output_path::Union{String, Nothing} + timestamp::Timestamp + exclude_from_context::Bool +end + +mutable struct CustomMessage{T} + role::String + custom_type::String + content::Union{String, Vector{MessageContent}} + display::Bool + details::Union{T, Nothing} + timestamp::Timestamp +end + +mutable struct BranchSummaryMessage + role::String + summary::String + from_id::String + timestamp::Timestamp +end + +mutable struct CompactionSummaryMessage + role::String + summary::String + tokens_before::Int64 + timestamp::Timestamp +end + +# ============================================================================ +# Bash execution to text conversion +# ============================================================================ + +function bashExecutionToText(msg::BashExecutionMessage)::String + text = "Ran `$(msg.command)`\n" + if !isempty(msg.output) + text *= "```\n$(msg.output)\n```" + else + text *= "(no output)" + end + if msg.cancelled + text *= "\n\n(command cancelled)" + elseif !isnothing(msg.exit_code) && msg.exit_code != 0 + text *= "\n\nCommand exited with code $(msg.exit_code)" + end + if msg.truncated && !isnothing(msg.full_output_path) + text *= "\n\n[Output truncated. Full output: $(msg.full_output_path)]" + end + return text +end + +# ============================================================================ +# Message creation functions +# ============================================================================ + +function createBranchSummaryMessage(summary::String, from_id::String, timestamp::String)::BranchSummaryMessage + return BranchSummaryMessage( + "branchSummary", + summary, + from_id, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +function createCompactionSummaryMessage(summary::String, tokens_before::Int64, timestamp::String)::CompactionSummaryMessage + return CompactionSummaryMessage( + "compactionSummary", + summary, + tokens_before, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +function createCustomMessage(custom_type::String, content::Union{String, Vector{MessageContent}}, display::Bool, details::Union{Any, Nothing}, timestamp::String)::CustomMessage + return CustomMessage( + "custom", + custom_type, + content, + display, + details, + Int64(Dates.now(Dates.UTC).datetime), + ) +end + +# ============================================================================ +# Convert to LLM messages +# ============================================================================ + +function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} + result::Vector{Message} = Message[] + + for m in messages + converted = convertToLlmMessage(m) + if !isnothing(converted) + push!(result, converted) + end + end + + return result +end + +function convertToLlmMessage(m::BashExecutionMessage)::Union{UserMessage, Nothing} + if m.exclude_from_context + return nothing + end + return UserMessage( + "user", + [TextContent(bashExecutionToText(m))], + m.timestamp, + ) +end + +function convertToLlmMessage(m::CustomMessage)::Union{UserMessage, Nothing} + content = if m.content isa String + [TextContent(m.content)] + else + m.content + end + return UserMessage("user", content, m.timestamp) +end + +function convertToLlmMessage(m::BranchSummaryMessage)::UserMessage + text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX + return UserMessage("user", [TextContent(text)], m.timestamp) +end + +function convertToLlmMessage(m::CompactionSummaryMessage)::UserMessage + text = COMPACTION_SUMMARY_PREFIX * m.summary * COMPACTION_SUMMARY_SUFFIX + return UserMessage("user", [TextContent(text)], m.timestamp) +end + +function convertToLlmMessage(m::UserMessage)::UserMessage + return m +end + +function convertToLlmMessage(m::AssistantMessage)::AssistantMessage + return m +end + +function convertToLlmMessage(m::ToolResultMessage)::ToolResultMessage + return m +end + +function convertToLlmMessage(m::AgentMessage)::Union{Message, Nothing} + return nothing +end + +end diff --git a/packages/agent/julia_implementation/src/prompt_templates.jl b/packages/agent/julia_implementation/src/prompt_templates.jl new file mode 100644 index 00000000..1defbdd9 --- /dev/null +++ b/packages/agent/julia_implementation/src/prompt_templates.jl @@ -0,0 +1,335 @@ +""" + prompt_templates.jl - Prompt template loading and formatting + +This module provides utilities for loading prompt templates and formatting invocations. +""" + +module PromptTemplates + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, toError, Result, ok, err + +# ============================================================================ +# Prompt template diagnostic types +# ============================================================================ + +const PromptTemplateDiagnosticCode = String +const PROMPT_TEMPLATE_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_LIST_FAILED = "list_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_READ_FAILED = "read_failed" +const PROMPT_TEMPLATE_DIAGNOSTIC_PARSE_FAILED = "parse_failed" + +mutable struct PromptTemplateDiagnostic + type::String + code::PromptTemplateDiagnosticCode + message::String + path::String +end + +# ============================================================================ +# Prompt template frontmatter +# ============================================================================ + +mutable struct PromptTemplateFrontmatter + description::Union{String, Nothing} + argument_hint::Union{String, Nothing} + extra::Dict{String, Any} +end + +# ============================================================================ +# Load prompt templates from paths +# ============================================================================ + +function loadPromptTemplates( + env::ExecutionEnv, + paths::Union{String, Vector{String}}, +)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} + prompt_templates::Vector{PromptTemplate} = PromptTemplate[] + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + path_list = if paths isa String + [paths] + else + paths + end + + for path in path_list + info_result = fileInfo(env, path, nothing) + if !info_result.ok + if info_result.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + info_result.error.message, + path, + )) + end + continue + end + + info = info_result.value + kind = getFileKind(env, info, diagnostics) + + if kind == "directory" + result = loadTemplatesFromDir(env, info.path) + append!(prompt_templates, result.prompt_templates) + append!(diagnostics, result.diagnostics) + elseif kind == "file" && endswith(info.name, ".md") + result = loadTemplateFromFile(env, info.path) + if !isnothing(result.prompt_template) + push!(prompt_templates, result.prompt_template) + end + append!(diagnostics, result.diagnostics) + end + end + + return prompt_templates, diagnostics +end + +function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{PromptTemplateDiagnostic})::Union{String, Nothing} + if info.kind == "file" || info.kind == "directory" + return info.kind + end + + canonical_path = canonicalPath(env, info.path, nothing) + if !canonical_path.ok + if canonical_path.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + canonical_path.error.message, + info.path, + )) + end + return nothing + end + + target = fileInfo(env, canonical_path.value, nothing) + if !target.ok + if target.error.code != "not_found" + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "file_info_failed", + target.error.message, + info.path, + )) + end + return nothing + end + + if target.value.kind == "file" || target.value.kind == "directory" + return target.value.kind + end + + return nothing +end + +# ============================================================================ +# Load templates from directory +# ============================================================================ + +function loadTemplatesFromDir( + env::ExecutionEnv, + dir::String, +)::Tuple{Vector{PromptTemplate}, Vector{PromptTemplateDiagnostic}} + prompt_templates::Vector{PromptTemplate} = PromptTemplate[] + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + entries_result = listDir(env, dir, nothing) + if !entries_result.ok + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "list_failed", + entries_result.error.message, + dir, + )) + return prompt_templates, diagnostics + end + + entries = entries_result.value + + for entry in sort(entries, by=e -> e.name) + kind = getFileKind(env, entry, diagnostics) + if kind != "file" || !endswith(entry.name, ".md") + continue + end + + result = loadTemplateFromFile(env, entry.path) + if !isnothing(result.prompt_template) + push!(prompt_templates, result.prompt_template) + end + append!(diagnostics, result.diagnostics) + end + + return prompt_templates, diagnostics +end + +# ============================================================================ +# Load template from file +# ============================================================================ + +function loadTemplateFromFile( + env::ExecutionEnv, + file_path::String, +)::Tuple{Union{PromptTemplate, Nothing}, Vector{PromptTemplateDiagnostic}} + diagnostics::Vector{PromptTemplateDiagnostic} = PromptTemplateDiagnostic[] + + raw_content = readTextFile(env, file_path, nothing) + if !raw_content.ok + push!(diagnostics, PromptTemplateDiagnostic( + "warning", + "read_failed", + raw_content.error.message, + file_path, + )) + return nothing, diagnostics + end + + # TODO: Parse frontmatter + # parsed = parseFrontmatter(rawContent.value); + # if !parsed.ok { + # diagnostics.push({ + # type: "warning", + # code: "parse_failed", + # message: parsed.error.message, + # path: filePath, + # }); + # return { promptTemplate: null, diagnostics }; + # } + + # const { frontmatter, body } = parsed.value; + # const firstLine = body.split("\n").find((line) => line.trim()); + # let description = typeof frontmatter.description === "string" ? frontmatter.description : ""; + # if (!description && firstLine) { + # description = firstLine.slice(0, 60); + # if (firstLine.length > 60) description += "..."; + # } + + # return { + # promptTemplate: { + # name: basenameEnvPath(filePath).replace(/\.md$/i, ""), + # description, + # content: body, + # }, + # diagnostics, + # }; + + return nothing, diagnostics +end + +# ============================================================================ +# Parse command arguments +# ============================================================================ + +function parseCommandArgs(args_string::String)::Vector{String} + args::Vector{String} = String[] + current::String = "" + in_quote::Union{String, Nothing} = nothing + + for i in 1:length(args_string) + char = args_string[i] + if !isnothing(in_quote) + if char == in_quote + in_quote = nothing + else + current *= char + end + elseif char == '"' || char == '\'' + in_quote = char + elseif char == ' ' || char == '\t' + if !isempty(current) + push!(args, current) + current = "" + end + else + current *= char + end + end + + if !isempty(current) + push!(args, current) + end + + return args +end + +# ============================================================================ +# Substitute arguments +# ============================================================================ + +function substituteArgs(content::String, args::Vector{String})::String + result = content + + # Substitute $1, $2, etc. + result = replace(result, r"\$(\d+)" => s -> begin + idx = parse(Int, s[1]) + if idx > 0 && idx <= length(args) + return args[idx] + end + return "" + end) + + # Substitute ${@:N} and ${@:N:L} + result = replace(result, r"\$\{@:(\d+)(?::(\d+))?\}" => s -> begin + m = match(r"\$\{@:(\d+)(?::(\d+))?\}", s) + if !isnothing(m) + start = parse(Int, m.captures[1]) - 1 + if start < 0 + start = 0 + end + if !isnothing(m.captures[2]) + length = parse(Int, m.captures[2]) + return join(args[start+1:start+length], " ") + end + return join(args[start+1:end], " ") + end + return s + end) + + # Substitute $ARGUMENTS and $@ + all_args = join(args, " ") + result = replace(result, "$ARGUMENTS" => all_args) + result = replace(result, "$@" => all_args) + + return result +end + +# ============================================================================ +# Format prompt template invocation +# ============================================================================ + +function formatPromptTemplateInvocation(template::PromptTemplate, args::Vector{String}=String[])::String + return substituteArgs(template.content, args) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function basenameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) + return normalized + end + return normalized[slash_index+1:end] +end + +function findlast(pattern::Char, s::String)::Union{Int64, Nothing} + for i in length(s):-1:1 + if s[i] == pattern + return i + end + end + return nothing +end + +function rtrim(s::String, chars::String)::String + idx = length(s) + while idx >= 1 && s[idx] in chars + idx -= 1 + end + return s[1:idx] +end + +end diff --git a/packages/agent/julia_implementation/src/session/jsonl_repo.jl b/packages/agent/julia_implementation/src/session/jsonl_repo.jl new file mode 100644 index 00000000..1cc8a0d6 --- /dev/null +++ b/packages/agent/julia_implementation/src/session/jsonl_repo.jl @@ -0,0 +1,148 @@ +""" + session/jsonl_repo.jl - JSONL session repository + +This module provides a JSONL-based session repository implementation. +""" + +module JsonlRepo + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..JsonlStorage: JsonlSessionStorage, headerToSessionMetadata +using ..MemoryRepo: createSessionId, createTimestamp, getEntriesToFork, toSession +using ..HarnessTypes: SessionRepo, SessionForkOptions + +# ============================================================================ +# JSONL session repository +# ============================================================================ + +mutable struct JsonlSessionRepo <: SessionRepo{ + JsonlSessionMetadata, + JsonlSessionCreateOptions, + JsonlSessionListOptions +} + fs::Any + sessions_root_input::String + sessions_root::Union{String, Nothing} + + function JsonlSessionRepo(; sessions_root::String, fs::Any) + new(fs, sessions_root, nothing) + end +end + +# ============================================================================ +# Session repo methods +# ============================================================================ + +function create(repo::JsonlSessionRepo, options::JsonlSessionCreateOptions)::Session + id = if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end + created_at = createTimestamp() + + session_dir = getSessionDir(repo, options.cwd) + + file_path = createSessionFilePath(repo, options.cwd, id, created_at) + + storage = JsonlSessionStorage( + file_path, + SessionHeader( + "session", + 3, + id, + created_at, + options.cwd, + get(options, :parentSessionPath, nothing), + get(options, :metadata, nothing), + ), + SessionTreeEntry[], + nothing, + ) + + return toSession(storage) +end + +function open(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Session + # TODO: Open existing file + return toSession(JsonlSessionStorage( + metadata.path, + SessionHeader( + "session", + 3, + metadata.id, + metadata.created_at, + metadata.cwd, + metadata.parent_session_path, + metadata.metadata, + ), + SessionTreeEntry[], + nothing, + )) +end + +function list(repo::JsonlSessionRepo, options::JsonlSessionListOptions=JsonlSessionListOptions())::Vector{JsonlSessionMetadata} + # TODO: List sessions + return JsonlSessionMetadata[] +end + +function delete(repo::JsonlSessionRepo, metadata::JsonlSessionMetadata)::Nothing + # TODO: Delete session file + return nothing +end + +function fork(repo::JsonlSessionRepo, source::JsonlSessionMetadata, options::Dict{String, Any})::Session + # TODO: Fork session + return create(repo, JsonlSessionCreateOptions( + cwd=get(options, "cwd", ""), + id=get(options, "id", createSessionId()), + )) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function getSessionsRoot(repo::JsonlSessionRepo)::String + if isnothing(repo.sessions_root) + repo.sessions_root = getFileSystemResultOrThrow( + absolutePath(repo.fs, repo.sessions_root_input), + "Failed to resolve sessions root $(repo.sessions_root_input)", + ) + end + return repo.sessions_root +end + +function getSessionDir(repo::JsonlSessionRepo, cwd::String)::String + return getFileSystemResultOrThrow( + joinPath(repo.fs, [getSessionsRoot(repo), encodeCwd(cwd)]), + "Failed to resolve session directory for $(cwd)", + ) +end + +function encodeCwd(cwd::String)::String + result = replace(cwd, r"^[/\\]" => "") + result = replace(result, r"[/\\:]" => "-") + return "--$(result)--" +end + +function createSessionFilePath(repo::JsonlSessionRepo, cwd::String, session_id::String, timestamp::String)::String + return getFileSystemResultOrThrow( + joinPath(repo.fs, [ + getSessionDir(repo, cwd), + "$(replace(timestamp, r"[:.]" => "-"))_$(session_id).jsonl", + ]), + "Failed to resolve session file path for $(session_id)", + ) +end + +function getFileSystemResultOrThrow(result::Result, message::String) + if !result.ok + code = result.error.code == "not_found" ? "not_found" : "storage" + throw(SessionError(code, "$(message): $(result.error.message)", result.error)) + end + return result.value +end + +end diff --git a/packages/agent/julia_implementation/src/session/jsonl_storage.jl b/packages/agent/julia_implementation/src/session/jsonl_storage.jl new file mode 100644 index 00000000..8c9b88a3 --- /dev/null +++ b/packages/agent/julia_implementation/src/session/jsonl_storage.jl @@ -0,0 +1,290 @@ +""" + session/jsonl_storage.jl - JSONL session storage + +This module provides JSONL-based session storage implementation. +""" + +module JsonlStorage + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata + +# ============================================================================ +# Session header +# ============================================================================ + +mutable struct SessionHeader + type::String + version::Int64 + id::String + timestamp::String + cwd::String + parent_session::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# JSONL session storage +# ============================================================================ + +mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T} + file_path::String + metadata::T + entries::Vector{SessionTreeEntry} + by_id::Dict{String, SessionTreeEntry} + labels_by_id::Dict{String, String} + current_leaf_id::Union{String, Nothing} + + function JsonlSessionStorage{T}( + file_path::String, + header::SessionHeader, + entries::Vector{SessionTreeEntry}, + leaf_id::Union{String, Nothing}, + ) where T + by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) + labels_by_id = Dict{String, String}() + + for entry in entries + if entry isa LabelEntry && !isnothing(entry.label) + labels_by_id[entry.target_id] = entry.label + end + end + + new( + file_path, + header, + entries, + by_id, + labels_by_id, + leaf_id, + ) + end +end + +# ============================================================================ +# Session storage methods +# ============================================================================ + +function getMetadata(storage::JsonlSessionStorage)::T + return storage.metadata +end + +function getLeafId(storage::JsonlSessionStorage)::Union{String, Nothing} + if !isnothing(storage.current_leaf_id) && !haskey(storage.by_id, storage.current_leaf_id) + throw(SessionError("invalid_session", "Entry $(storage.current_leaf_id) not found")) + end + return storage.current_leaf_id +end + +function setLeafId(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Nothing + if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + entry = LeafEntry( + "leaf", + generateEntryId(storage.by_id), + storage.current_leaf_id, + create_timestamp(), + leaf_id, + ) + + # TODO: Write to file + # getFileSystemResultOrThrow( + # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + # `Failed to append session leaf ${entry.id}`, + # ); + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + storage.current_leaf_id = leaf_id + return nothing +end + +function createEntryId(storage::JsonlSessionStorage)::String + return generateEntryId(storage.by_id) +end + +function appendEntry(storage::JsonlSessionStorage, entry::SessionTreeEntry)::Nothing + # TODO: Write to file + # getFileSystemResultOrThrow( + # await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), + # `Failed to append session entry ${entry.id}`, + # ); + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + + if entry isa LabelEntry + updateLabelCache(storage.labels_by_id, entry) + end + + storage.current_leaf_id = leafIdAfterEntry(entry) + return nothing +end + +function getEntry(storage::JsonlSessionStorage, id::String)::Union{SessionTreeEntry, Nothing} + return get(storage.by_id, id, nothing) +end + +function findEntries(storage::JsonlSessionStorage, type::String)::Vector{SessionTreeEntry} + return filter(entry -> entry.type == type, storage.entries) +end + +function getLabel(storage::JsonlSessionStorage, id::String)::Union{String, Nothing} + return get(storage.labels_by_id, id, nothing) +end + +function getSessionName(storage::JsonlSessionStorage)::Union{String, Nothing} + entries = findEntries(storage, "session_info") + if isempty(entries) + return nothing + end + return strip(entries[end].name) +end + +function getSessionStats(storage::JsonlSessionStorage)::SessionStats + message_count = 0 + cached_tokens = 0 + uncached_tokens = 0 + total_tokens = 0 + cost_total = 0.0 + + for entry in storage.entries + if entry isa MessageEntry + message_count += 1 + end + + usage = if entry isa MessageEntry && entry.message.role == "assistant" + entry.message.usage + elseif entry isa CompactionEntry || entry isa BranchSummaryEntry + entry.usage + else + nothing + end + + if !isnothing(usage) && + usage.input isa Int64 && + usage.output isa Int64 && + usage.cache_read isa Int64 && + usage.cache_write isa Int64 && + usage.cost.total isa Float64 + + cached_tokens += usage.cache_read + uncached_tokens += usage.input + usage.cache_write + total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write + cost_total += usage.cost.total + end + end + + return SessionStats( + message_count, + cached_tokens, + uncached_tokens, + total_tokens, + cost_total, + ) +end + +function getPathToRootOrCompaction(storage::JsonlSessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} + if isnothing(leaf_id) + return SessionTreeEntry[] + end + + path::Vector{SessionTreeEntry} = SessionTreeEntry[] + stop_at_entry_id::Union{String, Nothing} = nothing + current = get(storage.by_id, leaf_id, nothing) + + if isnothing(current) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + while !isnothing(current) + unshift!(path, current) + + if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id + break + end + + if current isa CompactionEntry + if !isnothing(current.retained_tail) + break + end + stop_at_entry_id = current.first_kept_entry_id + end + + if isnothing(current.parent_id) + break + end + + parent = get(storage.by_id, current.parent_id, nothing) + if isnothing(parent) + throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) + end + + current = parent + end + + return path +end + +function getEntries(storage::JsonlSessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + start = get(options, "afterEntrySeq", 0) + end_idx = if haskey(options, "limit") + start + options["limit"] + else + nothing + end + + if isnothing(end_idx) + return copy(storage.entries[start+1:end]) + end + + return copy(storage.entries[start+1:end_idx]) +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function updateLabelCache(labels_by_id::Dict{String, String}, entry::SessionTreeEntry)::Nothing + if entry isa LabelEntry + label = strip(get(entry, :label, nothing)) + if !isnothing(label) && !isempty(label) + labels_by_id[entry.target_id] = label + else + delete!(labels_by_id, entry.target_id) + end + end + return nothing +end + +function generateEntryId(by_id::Dict{String, SessionTreeEntry})::String + for i in 1:100 + id = uuidv7()[end-7:end] + if !haskey(by_id, id) + return id + end + end + return uuidv7() +end + +function leafIdAfterEntry(entry::SessionTreeEntry)::Union{String, Nothing} + if entry isa LeafEntry + return entry.target_id + end + return entry.id +end + +function headerToSessionMetadata(header::SessionHeader, path::String)::JsonlSessionMetadata + return JsonlSessionMetadata( + header.id, + header.timestamp, + header.cwd, + path, + header.parent_session, + header.metadata, + ) +end + +end diff --git a/packages/agent/julia_implementation/src/session/memory_repo.jl b/packages/agent/julia_implementation/src/session/memory_repo.jl new file mode 100644 index 00000000..d9012485 --- /dev/null +++ b/packages/agent/julia_implementation/src/session/memory_repo.jl @@ -0,0 +1,133 @@ +""" + session/memory_repo.jl - In-memory session repository + +This module provides an in-memory session repository implementation for testing. +""" + +module MemoryRepo + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..MemoryStorage: InMemorySessionStorage + +# ============================================================================ +# In-memory session repository +# ============================================================================ + +mutable struct InMemorySessionRepo <: SessionRepo{SessionMetadata, Dict{String, Any}, Nothing} + sessions::Dict{String, Session} + + function InMemorySessionRepo() + new(Dict{String, Session}()) + end +end + +# ============================================================================ +# Session repo methods +# ============================================================================ + +function create(repo::InMemorySessionRepo, options::Dict{String, Any}=Dict{String, Any}())::Session + metadata = SessionMetadata( + if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end, + createTimestamp(), + ) + + storage = InMemorySessionStorage{SessionMetadata}(metadata=metadata) + session = toSession(storage) + + repo.sessions[metadata.id] = session + + return session +end + +function open(repo::InMemorySessionRepo, metadata::SessionMetadata)::Session + session = get(repo.sessions, metadata.id, nothing) + if isnothing(session) + throw(SessionError("not_found", "Session not found: $(metadata.id)")) + end + return session +end + +function list(repo::InMemorySessionRepo)::Vector{SessionMetadata} + return [getMetadata(session) for session in values(repo.sessions)] +end + +function delete(repo::InMemorySessionRepo, metadata::SessionMetadata)::Nothing + delete!(repo.sessions, metadata.id) + return nothing +end + +function fork(repo::InMemorySessionRepo, source::SessionMetadata, options::Dict{String, Any})::Session + source_session = open(repo, source) + forked_entries = getEntriesToFork(getStorage(source_session), options) + + metadata = SessionMetadata( + if haskey(options, :id) && !isnothing(options[:id]) + options[:id] + else + createSessionId() + end, + createTimestamp(), + ) + + storage = InMemorySessionStorage{SessionMetadata}( + entries=forked_entries, + metadata=metadata, + ) + + session = toSession(storage) + repo.sessions[metadata.id] = session + + return session +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function createSessionId()::String + return uuidv7() +end + +function createTimestamp()::String + return create_timestamp() +end + +function toSession(storage::SessionStorage)::Session + return Session(storage) +end + +function getEntriesToFork(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + if !haskey(options, :entryId) || isnothing(options[:entryId]) + return getEntries(storage, Dict{String, Any}()) + end + + target = getEntry(storage, options[:entryId]) + if isnothing(target) + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) + end + + effective_leaf_id::Union{String, Nothing} + position = get(options, "position", "before") + + if position == "at" + effective_leaf_id = target.id + else + if target isa MessageEntry && target.message.role != "user" + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) + end + effective_leaf_id = target.parent_id + end + + return getPathToRootOrCompaction(storage, effective_leaf_id) +end + +function getStorage(session::Session)::SessionStorage + return session.storage +end + +end diff --git a/packages/agent/julia_implementation/src/session/memory_storage.jl b/packages/agent/julia_implementation/src/session/memory_storage.jl new file mode 100644 index 00000000..235efc1d --- /dev/null +++ b/packages/agent/julia_implementation/src/session/memory_storage.jl @@ -0,0 +1,227 @@ +""" + session/memory_storage.jl - In-memory session storage + +This module provides an in-memory session storage implementation for testing and temporary use. +""" + +module MemoryStorage + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..JsonlStorage: updateLabelCache, generateEntryId, leafIdAfterEntry + +# ============================================================================ +# In-memory session storage +# ============================================================================ + +mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T} + metadata::T + entries::Vector{SessionTreeEntry} + by_id::Dict{String, SessionTreeEntry} + labels_by_id::Dict{String, String} + leaf_id::Union{String, Nothing} + + function InMemorySessionStorage{T}(; + entries::Vector{SessionTreeEntry}=SessionTreeEntry[], + metadata::Union{T, Nothing]=nothing, + ) where T + by_id = Dict{String, SessionTreeEntry}((e.id, e) for e in entries) + labels_by_id = Dict{String, String}() + + leaf_id = nothing + for entry in entries + if entry isa LabelEntry + updateLabelCache(labels_by_id, entry) + end + leaf_id = leafIdAfterEntry(entry) + end + + if !isnothing(leaf_id) && !haskey(by_id, leaf_id) + throw(SessionError("invalid_session", "Entry $(leaf_id) not found")) + end + + new( + if isnothing(metadata) + T(uuidv7(), create_timestamp()) + else + metadata + end, + copy(entries), + by_id, + labels_by_id, + leaf_id, + ) + end +end + +# ============================================================================ +# Session storage methods +# ============================================================================ + +function getMetadata(storage::InMemorySessionStorage)::T + return storage.metadata +end + +function getLeafId(storage::InMemorySessionStorage)::Union{String, Nothing} + if !isnothing(storage.leaf_id) && !haskey(storage.by_id, storage.leaf_id) + throw(SessionError("invalid_session", "Entry $(storage.leaf_id) not found")) + end + return storage.leaf_id +end + +function setLeafId(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Nothing + if !isnothing(leaf_id) && !haskey(storage.by_id, leaf_id) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + entry = LeafEntry( + "leaf", + generateEntryId(storage.by_id), + storage.leaf_id, + create_timestamp(), + leaf_id, + ) + + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + storage.leaf_id = leaf_id + return nothing +end + +function createEntryId(storage::InMemorySessionStorage)::String + return generateEntryId(storage.by_id) +end + +function appendEntry(storage::InMemorySessionStorage, entry::SessionTreeEntry)::Nothing + push!(storage.entries, entry) + storage.by_id[entry.id] = entry + + if entry isa LabelEntry + updateLabelCache(storage.labels_by_id, entry) + end + + storage.leaf_id = leafIdAfterEntry(entry) + return nothing +end + +function getEntry(storage::InMemorySessionStorage, id::String)::Union{SessionTreeEntry, Nothing} + return get(storage.by_id, id, nothing) +end + +function findEntries(storage::InMemorySessionStorage, type::String)::Vector{SessionTreeEntry} + return filter(entry -> entry.type == type, storage.entries) +end + +function getLabel(storage::InMemorySessionStorage, id::String)::Union{String, Nothing} + return get(storage.labels_by_id, id, nothing) +end + +function getSessionName(storage::InMemorySessionStorage)::Union{String, Nothing} + entries = findEntries(storage, "session_info") + if isempty(entries) + return nothing + end + return strip(entries[end].name) +end + +function getSessionStats(storage::InMemorySessionStorage)::SessionStats + message_count = 0 + cached_tokens = 0 + uncached_tokens = 0 + total_tokens = 0 + cost_total = 0.0 + + for entry in storage.entries + if entry isa MessageEntry + message_count += 1 + end + + usage = if entry isa MessageEntry && entry.message.role == "assistant" + entry.message.usage + elseif entry isa CompactionEntry || entry isa BranchSummaryEntry + entry.usage + else + nothing + end + + if !isnothing(usage) && + usage.input isa Int64 && + usage.output isa Int64 && + usage.cache_read isa Int64 && + usage.cache_write isa Int64 && + usage.cost.total isa Float64 + + cached_tokens += usage.cache_read + uncached_tokens += usage.input + usage.cache_write + total_tokens += usage.input + usage.output + usage.cache_read + usage.cache_write + cost_total += usage.cost.total + end + end + + return SessionStats( + message_count, + cached_tokens, + uncached_tokens, + total_tokens, + cost_total, + ) +end + +function getPathToRootOrCompaction(storage::InMemorySessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry} + if isnothing(leaf_id) + return SessionTreeEntry[] + end + + path::Vector{SessionTreeEntry} = SessionTreeEntry[] + stop_at_entry_id::Union{String, Nothing} = nothing + current = get(storage.by_id, leaf_id, nothing) + + if isnothing(current) + throw(SessionError("not_found", "Entry $(leaf_id) not found")) + end + + while !isnothing(current) + unshift!(path, current) + + if !isnothing(stop_at_entry_id) && current.id == stop_at_entry_id + break + end + + if current isa CompactionEntry + if !isnothing(current.retained_tail) + break + end + stop_at_entry_id = current.first_kept_entry_id + end + + if isnothing(current.parent_id) + break + end + + parent = get(storage.by_id, current.parent_id, nothing) + if isnothing(parent) + throw(SessionError("invalid_session", "Entry $(current.parent_id) not found")) + end + + current = parent + end + + return path +end + +function getEntries(storage::InMemorySessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry} + start = get(options, "afterEntrySeq", 0) + end_idx = if haskey(options, "limit") + start + options["limit"] + else + nothing + end + + if isnothing(end_idx) + return copy(storage.entries[start+1:end]) + end + + return copy(storage.entries[start+1:end_idx]) +end + +end diff --git a/packages/agent/julia_implementation/src/session/repo_utils.jl b/packages/agent/julia_implementation/src/session/repo_utils.jl new file mode 100644 index 00000000..fb79916a --- /dev/null +++ b/packages/agent/julia_implementation/src/session/repo_utils.jl @@ -0,0 +1,65 @@ +""" + session/repo_utils.jl - Session repository utilities + +This module provides shared utilities for session repository implementations. +""" + +module RepoUtils + +using ..Types: * +using ..SessionStorage: SessionStorage, SessionMetadata +using ..Session: Session + +# ============================================================================ +# Helper functions +# ============================================================================ + +function createSessionId()::String + return uuidv7() +end + +function createTimestamp()::String + return create_timestamp() +end + +function toSession{T<:SessionMetadata}(storage::SessionStorage{T})::Session{T} + return Session(storage) +end + +function getFileSystemResultOrThrow{TValue}(result::Result{TValue, FileError}, message::String)::TValue + if !result.ok + code = result.error.code == "not_found" ? "not_found" : "storage" + throw(SessionError(code, "$(message): $(result.error.message)", result.error)) + end + return result.value +end + +function getEntriesToFork( + storage::SessionStorage, + options::Dict{String, Any}, +)::Vector{SessionTreeEntry} + if !haskey(options, :entryId) || isnothing(options[:entryId]) + return getEntries(storage, Dict{String, Any}()) + end + + target = getEntry(storage, options[:entryId]) + if isnothing(target) + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) not found")) + end + + effective_leaf_id::Union{String, Nothing} + position = get(options, "position", "before") + + if position == "at" + effective_leaf_id = target.id + else + if target isa MessageEntry && target.message.role != "user" + throw(SessionError("invalid_fork_target", "Entry $(options[:entryId]) is not a user message")) + end + effective_leaf_id = target.parent_id + end + + return getPathToRootOrCompaction(storage, effective_leaf_id) +end + +end diff --git a/packages/agent/julia_implementation/src/session/session.jl b/packages/agent/julia_implementation/src/session/session.jl new file mode 100644 index 00000000..72efce94 --- /dev/null +++ b/packages/agent/julia_implementation/src/session/session.jl @@ -0,0 +1,422 @@ +""" + session/session.jl - Session management + +This module provides the Session class for managing conversation history with branch support. +""" + +module Session + +using ..Types: * +using ..SessionStorage: SessionStorage +using ..Messages: * +using ..HarnessTypes: * + +# ============================================================================ +# Session context build options +# ============================================================================ + +mutable struct SessionContextBuildOptions + entry_transforms::Union{Vector{Function}, Nothing} + entry_projectors::Union{Dict{String, Function}, Nothing} +end + +# ============================================================================ +# Default context entry transform +# ============================================================================ + +function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry} + compaction = nothing + for entry in path_entries + if entry isa CompactionEntry + compaction = entry + break + end + end + + if isnothing(compaction) + return copy(path_entries) + end + + entries::Vector{SessionTreeEntry} = [compaction] + compaction_idx = findfirst( + (entry) -> entry isa CompactionEntry && entry.id == compaction.id, + path_entries, + ) + + if !isnothing(compaction.retained_tail) + for i in compaction_idx+1:length(path_entries) + push!(entries, path_entries[i]) + end + return entries + end + + if !isnothing(compaction.first_kept_entry_id) + found_first_kept = false + for i in 1:compaction_idx-1 + entry = path_entries[i] + if entry.id == compaction.first_kept_entry_id + found_first_kept = true + end + if found_first_kept + push!(entries, entry) + end + end + end + + for i in compaction_idx+1:length(path_entries) + push!(entries, path_entries[i]) + end + + return entries +end + +# ============================================================================ +# Build context entries +# ============================================================================ + +function buildContextEntries( + path_entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::Vector{SessionTreeEntry} + entries = defaultContextEntryTransform(path_entries) + + if !isnothing(options.entry_transforms) + for transform in options.entry_transforms + entries = transform(entries) + end + end + + return entries +end + +# ============================================================================ +# Session entry to context messages +# ============================================================================ + +function sessionEntryToContextMessages( + entry::SessionTreeEntry, + index::Int64, + entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::Vector{AgentMessage} + if entry isa MessageEntry + return [entry.message] + end + + if entry isa CustomMessageEntry + return [createCustomMessage( + entry.custom_type, + entry.content, + entry.display, + entry.details, + entry.timestamp, + )] + end + + if entry isa CompactionEntry + messages = [createCompactionSummaryMessage( + entry.summary, + entry.tokens_before, + entry.timestamp, + )] + if !isnothing(entry.retained_tail) + append!(messages, entry.retained_tail) + end + return messages + end + + if entry isa BranchSummaryEntry + return [createBranchSummaryMessage( + entry.summary, + entry.from_id, + entry.timestamp, + )] + end + + if entry isa CustomEntry + if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type) + projector = options.entry_projectors[entry.custom_type] + return projector(entry, index, entries) + end + return AgentMessage[] + end + + return AgentMessage[] +end + +# ============================================================================ +# Build session context +# ============================================================================ + +function buildSessionContext( + path_entries::Vector{SessionTreeEntry}, + options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), +)::SessionContext + state = deriveSessionContextState(path_entries) + context_entries = buildContextEntries(path_entries, options) + messages = SessionTreeEntry[] + for (i, entry) in enumerate(context_entries) + append!(messages, sessionEntryToContextMessages(entry, i, context_entries, options)) + end + return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names) +end + +function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any} + thinking_level = "off" + model = nothing + active_tool_names = nothing + + for entry in path_entries + if entry isa ThinkingLevelChangeEntry + thinking_level = entry.thinking_level + elseif entry isa ModelChangeEntry + model = Dict{String, String}("provider" => entry.provider, "modelId" => entry.model_id) + elseif entry isa MessageEntry && entry.message.role == "assistant" + model = Dict{String, String}("provider" => entry.message.provider, "modelId" => entry.message.model) + elseif entry isa ActiveToolsChangeEntry + active_tool_names = copy(entry.active_tool_names) + end + end + + return Dict{String, Any}( + "thinking_level" => thinking_level, + "model" => model, + "active_tool_names" => active_tool_names, + ) +end + +# ============================================================================ +# Session class +# ============================================================================ + +mutable struct Session{T<:SessionMetadata} + storage::SessionStorage{T} + context_build_options::SessionContextBuildOptions + + function Session( + storage::SessionStorage, + context_build_options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing), + ) + new{typeof(storage.metadata)}(storage, context_build_options) + end +end + +# ============================================================================ +# Session methods +# ============================================================================ + +function getMetadata(session::Session)::T + return getMetadata(session.storage) +end + +function getStorage(session::Session)::SessionStorage + return session.storage +end + +function getLeafId(session::Session)::Union{String, Nothing} + return getLeafId(session.storage) +end + +function getEntry(session::Session, id::String)::Union{SessionTreeEntry, Nothing} + return getEntry(session.storage, id) +end + +function getEntries(session::Session, options::Dict{String, Any}=Dict{String, Any}())::Vector{SessionTreeEntry} + return getEntries(session.storage, options) +end + +function getBranch(session::Session, from_id::Union{String, Nothing}=nothing)::Vector{SessionTreeEntry} + leaf_id = if isnothing(from_id) + getLeafId(session.storage) + else + from_id + end + return getPathToRootOrCompaction(session.storage, leaf_id) +end + +function buildContextEntries(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::Vector{SessionTreeEntry} + return buildContextEntries(getBranch(session), mergeContextBuildOptions(session, options)) +end + +function buildContext(session::Session, options::SessionContextBuildOptions=SessionContextBuildOptions())::SessionContext + return buildSessionContext(getBranch(session), mergeContextBuildOptions(session, options)) +end + +function mergeContextBuildOptions(session::Session, options::SessionContextBuildOptions)::SessionContextBuildOptions + return SessionContextBuildOptions( + vcat( + isnothing(session.context_build_options.entry_transforms) ? [] : session.context_build_options.entry_transforms, + isnothing(options.entry_transforms) ? [] : options.entry_transforms, + ), + merge( + isnothing(session.context_build_options.entry_projectors) ? Dict{String, Any}() : session.context_build_options.entry_projectors, + isnothing(options.entry_projectors) ? Dict{String, Any}() : options.entry_projectors, + promote=true, + ), + ) +end + +function getLabel(session::Session, id::String)::Union{String, Nothing} + return getLabel(session.storage, id) +end + +function getSessionStats(session::Session)::SessionStats + return getSessionStats(session.storage) +end + +function getSessionName(session::Session)::Union{String, Nothing} + return getSessionName(session.storage) +end + +function appendMessage(session::Session, message::AgentMessage)::String + return appendTypedEntry(session, MessageEntry( + "message", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + message, + )) +end + +function appendThinkingLevelChange(session::Session, thinking_level::String)::String + return appendTypedEntry(session, ThinkingLevelChangeEntry( + "thinking_level_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + thinking_level, + )) +end + +function appendModelChange(session::Session, provider::String, model_id::String)::String + return appendTypedEntry(session, ModelChangeEntry( + "model_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + provider, + model_id, + )) +end + +function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String + return appendTypedEntry(session, ActiveToolsChangeEntry( + "active_tools_change", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + active_tool_names, + )) +end + +function appendCompaction( + session::Session, + summary::String, + first_kept_entry_id::Union{String, Nothing}, + tokens_before::Int64, + details::Union{Any, Nothing}=nothing, + from_hook::Bool=false, + usage::Union{Usage, Nothing}=nothing, + retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing, +)::String + return appendTypedEntry(session, CompactionEntry( + "compaction", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + summary, + first_kept_entry_id, + tokens_before, + retained_tail, + details, + usage, + from_hook, + )) +end + +function appendCustomEntry(session::Session, custom_type::String, data::Union{Any, Nothing}=nothing)::String + return appendTypedEntry(session, CustomEntry( + "custom", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + custom_type, + data, + )) +end + +function appendCustomMessageEntry( + session::Session, + custom_type::String, + content::String, + display::Bool, + details::Union{Any, Nothing}=nothing, +)::String + return appendTypedEntry(session, CustomMessageEntry( + "custom_message", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + custom_type, + content, + details, + display, + )) +end + +function appendLabel(session::Session, target_id::String, label::Union{String, Nothing})::String + if isnothing(getEntry(session, target_id)) + throw(SessionError("not_found", "Entry $(target_id) not found")) + end + return appendTypedEntry(session, LabelEntry( + "label", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + target_id, + label, + )) +end + +function appendSessionName(session::Session, name::String)::String + sanitizedName = replace(name, r"[\r\n]+" => " ") + return appendTypedEntry(session, SessionInfoEntry( + "session_info", + createEntryId(session.storage), + getLeafId(session.storage), + create_timestamp(), + sanitizedName, + )) +end + +function moveTo( + session::Session, + entry_id::Union{String, Nothing}, + summary::Union{Dict{String, Any}, Nothing}=nothing, +)::Union{String, Nothing + if !isnothing(entry_id) && isnothing(getEntry(session, entry_id)) + throw(SessionError("not_found", "Entry $(entry_id) not found")) + end + setLeafId(session.storage, entry_id) + if isnothing(summary) + return nothing + end + return appendTypedEntry(session, BranchSummaryEntry( + "branch_summary", + createEntryId(session.storage), + entry_id, + create_timestamp(), + entry_id, + summary["summary"], + get(summary, "details", nothing), + get(summary, "usage", nothing), + get(summary, "from_hook", false), + )) +end + +function appendTypedEntry(session::Session, entry::SessionTreeEntry)::String + appendEntry(session.storage, entry) + return entry.id +end + +end diff --git a/packages/agent/julia_implementation/src/skills.jl b/packages/agent/julia_implementation/src/skills.jl new file mode 100644 index 00000000..ae83a7f9 --- /dev/null +++ b/packages/agent/julia_implementation/src/skills.jl @@ -0,0 +1,375 @@ +""" + skills.jl - Skill loading and formatting + +This module provides utilities for loading skills from SKILL.md files and formatting skill invocations. +""" + +module Skills + +using ..Types: * +using ..HarnessTypes: Skill, ExecutionEnv, FileSystem, toError, FileError, Result, ok, err + +const MAX_NAME_LENGTH = 64 +const MAX_DESCRIPTION_LENGTH = 1024 +const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"] + +# ============================================================================ +# Skill diagnostic types +# ============================================================================ + +const SkillDiagnosticCode = String +const SKILL_DIAGNOSTIC_FILE_INFO_FAILED = "file_info_failed" +const SKILL_DIAGNOSTIC_LIST_FAILED = "list_failed" +const SKILL_DIAGNOSTIC_READ_FAILED = "read_failed" +const SKILL_DIAGNOSTIC_PARSE_FAILED = "parse_failed" +const SKILL_DIAGNOSTIC_INVALID_METADATA = "invalid_metadata" + +mutable struct SkillDiagnostic + type::String + code::SkillDiagnosticCode + message::String + path::String +end + +# ============================================================================ +# Skill frontmatter +# ============================================================================ + +mutable struct SkillFrontmatter + name::Union{String, Nothing} + description::Union{String, Nothing} + disable_model_invocation::Union{Bool, Nothing} + extra::Dict{String, Any} +end + +# ============================================================================ +# Format skill invocation +# ============================================================================ + +function formatSkillInvocation(skill::Skill, additional_instructions::Union{String, Nothing})::String + skill_block = "\nReferences are relative to $(dirnameEnvPath(skill.filePath)).\n\n$(skill.content)\n" + if isnothing(additional_instructions) + return skill_block + end + return "$(skill_block)\n\n$(additional_instructions)" +end + +# ============================================================================ +# Load skills from directories +# ============================================================================ + +function loadSkills(env::ExecutionEnv, dirs::Union{String, Vector{String}})::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} + skills::Vector{Skill} = Skill[] + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + dir_list = if dirs isa String + [dirs] + else + dirs + end + + for dir in dir_list + root_info_result = fileInfo(env, dir, nothing) + if !root_info_result.ok + if root_info_result.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + root_info_result.error.message, + dir, + )) + end + continue + end + + root_info = root_info_result.value + if !isDirectory(env, root_info, diagnostics) + continue + end + + result = loadSkillsFromDirInternal(env, root_info.path, true, Dict{String, Any}(), root_info.path) + append!(skills, result.skills) + append!(diagnostics, result.diagnostics) + end + + return skills, diagnostics +end + +function isDirectory(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool + return info.kind == "directory" +end + +function loadSkillsFromDirInternal( + env::ExecutionEnv, + dir::String, + include_root_files::Bool, + ignore_matcher::Dict{String, Any}, + root_dir::String, +)::Tuple{Vector{Skill}, Vector{SkillDiagnostic}} + skills::Vector{Skill} = Skill[] + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + dir_info_result = fileInfo(env, dir, nothing) + if !dir_info_result.ok + if dir_info_result.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + dir_info_result.error.message, + dir, + )) + end + return skills, diagnostics + end + + dir_info = dir_info_result.value + if !isDirectory(env, dir_info, diagnostics) + return skills, diagnostics + end + + # TODO: Implement ignore rules + # await addIgnoreRules(env, ignoreMatcher, dir, rootDir, diagnostics); + + entries_result = listDir(env, dir, nothing) + if !entries_result.ok + push!(diagnostics, SkillDiagnostic( + "warning", + "list_failed", + entries_result.error.message, + dir, + )) + return skills, diagnostics + end + + entries = entries_result.value + + # Look for SKILL.md + for entry in entries + if entry.name != "SKILL.md" + continue + end + + full_path = entry.path + if !isFile(env, entry, diagnostics) + continue + end + + result = loadSkillFromFile(env, full_path) + if !isnothing(result.skill) + push!(skills, result.skill) + end + append!(diagnostics, result.diagnostics) + return skills, diagnostics + end + + # Process other files + for entry in sort(entries, by=e -> e.name) + if startswith(entry.name, ".") || entry.name == "node_modules" + continue + end + + full_path = entry.path + kind = getFileKind(env, entry, diagnostics) + if isnothing(kind) + continue + end + + rel_path = relativeEnvPath(root_dir, full_path) + ignore_path = kind == "directory" ? "$(rel_path)/" : rel_path + + if !isnothing(ignore_matcher) && haskey(ignore_matcher, ignore_path) + continue + end + + if kind == "directory" + result = loadSkillsFromDirInternal(env, full_path, false, ignore_matcher, root_dir) + append!(skills, result.skills) + append!(diagnostics, result.diagnostics) + continue + end + + if kind != "file" || !include_root_files || !endswith(entry.name, ".md") + continue + end + + result = loadSkillFromFile(env, full_path) + if !isnothing(result.skill) + push!(skills, result.skill) + end + append!(diagnostics, result.diagnostics) + end + + return skills, diagnostics +end + +function isFile(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Bool + return info.kind == "file" +end + +function getFileKind(env::ExecutionEnv, info::FileInfo, diagnostics::Vector{SkillDiagnostic})::Union{String, Nothing} + if info.kind == "file" || info.kind == "directory" + return info.kind + end + + canonical_path = canonicalPath(env, info.path, nothing) + if !canonical_path.ok + if canonical_path.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + canonical_path.error.message, + info.path, + )) + end + return nothing + end + + target = fileInfo(env, canonical_path.value, nothing) + if !target.ok + if target.error.code != "not_found" + push!(diagnostics, SkillDiagnostic( + "warning", + "file_info_failed", + target.error.message, + info.path, + )) + end + return nothing + end + + if target.value.kind == "file" || target.value.kind == "directory" + return target.value.kind + end + + return nothing +end + +# ============================================================================ +# Load skill from file +# ============================================================================ + +function loadSkillFromFile(env::ExecutionEnv, file_path::String)::Tuple{Union{Skill, Nothing}, Vector{SkillDiagnostic}} + diagnostics::Vector{SkillDiagnostic} = SkillDiagnostic[] + + raw_content = readTextFile(env, file_path, nothing) + if !raw_content.ok + push!(diagnostics, SkillDiagnostic( + "warning", + "read_failed", + raw_content.error.message, + file_path, + )) + return nothing, diagnostics + end + + # TODO: Parse frontmatter + # parsed = parseFrontmatter(rawContent.value); + # if !parsed.ok { + # diagnostics.push({ type: "warning", code: "parse_failed", message: parsed.error.message, path: filePath }); + # return { skill: null, diagnostics }; + # } + + # const { frontmatter, body } = parsed.value; + # const skillDir = dirnameEnvPath(filePath); + # const parentDirName = basenameEnvPath(skillDir); + # const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; + + # for (const error of validateDescription(description)) { + # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); + # } + + # const frontmatterName = typeof frontmatter.name === "string" ? frontmatter.name : undefined; + # const name = frontmatterName || parentDirName; + # for (const error of validateName(name, parentDirName)) { + # diagnostics.push({ type: "warning", code: "invalid_metadata", message: error, path: filePath }); + # } + + # if (!description || description.trim() === "") { + # return { skill: null, diagnostics }; + # } + + # return { + # skill: { + # name, + # description, + # content: body, + # filePath, + # disableModelInvocation: frontmatter["disable-model-invocation"] === true, + # }, + # diagnostics, + # }; + + return nothing, diagnostics +end + +# ============================================================================ +# Path utility functions +# ============================================================================ + +function joinEnvPath(base::String, child::String)::String + return "$(rtrim(base, '/'))/$(ltrim(child, '/'))" +end + +function dirnameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) || slash_index <= 1 + return "/" + end + return normalized[1:slash_index-1] +end + +function basenameEnvPath(path::String)::String + normalized = rtrim(path, '/') + slash_index = findlast('/', normalized) + if isnothing(slash_index) + return normalized + end + return normalized[slash_index+1:end] +end + +function relativeEnvPath(root::String, path::String)::String + normalized_root = rtrim(root, '/') + normalized_path = rtrim(path, '/') + + if normalized_path == normalized_root + return "" + end + + if startswith(normalized_path, "$(normalized_root)/") + return normalized_path[length(normalized_root)+2:end] + end + + return lstrip(normalized_path, '/') +end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function lstrip(s::String, chars::String)::String + idx = 1 + while idx <= length(s) && s[idx] in chars + idx += 1 + end + return s[idx:end] +end + +function rtrim(s::String, chars::String)::String + idx = length(s) + while idx >= 1 && s[idx] in chars + idx -= 1 + end + return s[1:idx] +end + +function findlast(pattern::Char, s::String)::Union{Int64, Nothing} + for i in length(s):-1:1 + if s[i] == pattern + return i + end + end + return nothing +end + +end diff --git a/packages/agent/julia_implementation/src/stream_fn.jl b/packages/agent/julia_implementation/src/stream_fn.jl new file mode 100644 index 00000000..c78481fe --- /dev/null +++ b/packages/agent/julia_implementation/src/stream_fn.jl @@ -0,0 +1,45 @@ +""" + stream_fn.jl - Stream function utilities + +This module provides the default stream function configuration for AgentCore. +""" + +module StreamFn + +using ..Types: StreamFn + +let default_stream_fn::Union{StreamFn, Nothing} = nothing + +""" + setDefaultStreamFn(stream_fn) + +Configure the fallback used by Agent and low-level loops when callers omit stream_fn. + +# Arguments +- `stream_fn`: The stream function to set as default +""" +function setDefaultStreamFn(stream_fn::Union{StreamFn, Nothing}) + global default_stream_fn = stream_fn +end + +""" + getDefaultStreamFn() + +Get the configured default stream function, or throw an error if none is configured. + +# Returns +- The configured stream function + +# Throws +- ErrorException if no default stream function is configured +""" +function getDefaultStreamFn()::StreamFn + if isnothing(default_stream_fn) + throw(ErrorException( + "No default stream function configured. Pass stream_fn explicitly or call setDefaultStreamFn()." + )) + end + return default_stream_fn +end + +end diff --git a/packages/agent/julia_implementation/src/system_prompt.jl b/packages/agent/julia_implementation/src/system_prompt.jl new file mode 100644 index 00000000..ba1a4e85 --- /dev/null +++ b/packages/agent/julia_implementation/src/system_prompt.jl @@ -0,0 +1,56 @@ +""" + system_prompt.jl - System prompt formatting + +This module provides utilities for formatting skills in the system prompt. +""" + +module SystemPrompt + +using ..Types: Skill + +""" + formatSkillsForSystemPrompt(skills) + +Format skills for inclusion in the system prompt using XML-formatted blocks. +""" +function formatSkillsForSystemPrompt(skills::Vector{Skill})::String + visible_skills = filter(s -> !s.disableModelInvocation, skills) + if isempty(visible_skills) + return "" + end + + lines = String[ + "The following skills provide specialized instructions for specific tasks.", + "Read the full skill file when the task matches its description.", + "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", + "", + "", + ] + + for skill in visible_skills + push!(lines, " ") + push!(lines, " $(escapeXml(skill.name))") + push!(lines, " $(escapeXml(skill.description))") + push!(lines, " $(escapeXml(skill.filePath))") + push!(lines, " ") + end + + push!(lines, "") + return join(lines, "\n") +end + +""" + escapeXml(value) + +Escape special characters in a string for XML. +""" +function escapeXml(value::String)::String + result = replace(value, "&" => "&") + result = replace(result, "<" => "<") + result = replace(result, ">" => ">") + result = replace(result, "\"" => """) + result = replace(result, "'" => "'") + return result +end + +end diff --git a/packages/agent/julia_implementation/src/tools/bash.jl b/packages/agent/julia_implementation/src/tools/bash.jl new file mode 100644 index 00000000..ab4e3b64 --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/bash.jl @@ -0,0 +1,49 @@ +""" + tools/bash.jl - Bash execution tool + +This module provides the bash execution tool for AgentCore. +""" + +module Bash + +using ..Types: * + +struct BashExecution + command::String + cwd::String + env::Dict{String, String} + inherit_env::Bool +end + +mutable struct BashPrepare{TContext} + function::Function + context::TContext + signal::Union{Any, Nothing} +end + +mutable struct BashToolOptions{TContext} + command_prefix::Union{String, Nothing} + prepare::Union{BashPrepare{TContext}, Nothing} +end + +mutable struct BashToolDetails + truncation::Union{Any, Nothing} + full_output_path::Union{String, Nothing} +end + +function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) where TContext + return AgentTool( + "bash", + "bash", + "Execute a bash command in the current working directory.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement bash execution + return AgentToolResult([TextContent("Command executed successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/packages/agent/julia_implementation/src/tools/edit.jl b/packages/agent/julia_implementation/src/tools/edit.jl new file mode 100644 index 00000000..8b004052 --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/edit.jl @@ -0,0 +1,32 @@ +""" + tools/edit.jl - File edit tool + +This module provides the file edit tool for AgentCore. +""" + +module Edit + +using ..Types: * + +mutable struct EditToolDetails + diff::String + patch::String + first_changed_line::Union{Int64, Nothing} +end + +function createEditTool{TContext}() where TContext + return AgentTool( + "edit", + "edit", + "Edit a single file using exact text replacement.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement edit execution + return AgentToolResult([TextContent("File edited successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/packages/agent/julia_implementation/src/tools/edit_diff.jl b/packages/agent/julia_implementation/src/tools/edit_diff.jl new file mode 100644 index 00000000..1dd4c7de --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/edit_diff.jl @@ -0,0 +1,67 @@ +""" + tools/edit_diff.jl - Edit diff utilities + +This module provides shared diff computation utilities for the edit tool. +""" + +module EditDiff + +using ..Types: * + +function detectLineEnding(content::String)::String + crlf_idx = findfirst("\r\n", content) + lf_idx = findfirst("\n", content) + if isnothing(lf_idx) + return "\n" + end + if isnothing(crlf_idx) + return "\n" + end + return crlf_idx < lf_idx ? "\r\n" : "\n" +end + +function normalizeToLF(text::String)::String + return replace(text, "\r\n" => "\n", "\r" => "\n") +end + +function restoreLineEndings(text::String, ending::String)::String + if ending == "\r\n" + return replace(text, "\n" => "\r\n") + end + return text +end + +function normalizeForFuzzyMatch(text::String)::String + # TODO: Implement fuzzy matching normalization + return text +end + +function splitLinesWithEndings(content::String)::Vector{String} + # TODO: Implement line splitting with endings + return split(content, "\n") +end + +function applyEditsToNormalizedContent( + normalized_content::String, + edits::Vector{Any}, + path::String, +)::Tuple{String, String} + # TODO: Implement edit application + return normalized_content, normalized_content +end + +function generateUnifiedPatch(path::String, old_content::String, new_content::String, context_lines::Int64=4)::String + # TODO: Implement unified patch generation + return "" +end + +function generateDiffString( + old_content::String, + new_content::String, + context_lines::Int64=4, +)::Tuple{String, Union{Int64, Nothing}} + # TODO: Implement diff string generation + return "", nothing +end + +end diff --git a/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl b/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl new file mode 100644 index 00000000..2f106ce9 --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/file_mutation_queue.jl @@ -0,0 +1,59 @@ +""" + tools/file_mutation_queue.jl - File mutation queue + +This module provides file mutation serialization for safe concurrent file writes. +""" + +module FileMutationQueue + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result + +# ============================================================================ +# Mutation queue state +# ============================================================================ + +mutable struct MutationQueueState + queues::Dict{String, Any} + registration::Any +end + +# Global state +const states = Dict{ExecutionEnv, MutationQueueState}() + +function getState(env::ExecutionEnv)::MutationQueueState + if !haskey(states, env) + states[env] = MutationQueueState(Dict{String, Any}(), nothing) + end + return states[env] +end + +# ============================================================================ +# File mutation queue helpers +# ============================================================================ + +async function getMutationQueueKey(env::ExecutionEnv, path::String)::String + absolute_path = getOrThrow(getOrThrow(absolutePath(env, path), "Failed to get absolute path")) + canonical_path = canonicalPath(env, absolute_path, nothing) + if canonical_path.ok + return canonical_path.value + end + if canonical_path.error.code in ("not_found", "not_supported") + return absolute_path + end + throw(canonical_path.error) +end + +# ============================================================================ +# Main function - serialize file mutations +# ============================================================================ + +function withFileMutationQueue{T}(env::ExecutionEnv, path::String, fn::Function)::T + state = getState(env) + + # TODO: Implement proper async queueing + # This is a simplified version + return fn() +end + +end diff --git a/packages/agent/julia_implementation/src/tools/image.jl b/packages/agent/julia_implementation/src/tools/image.jl new file mode 100644 index 00000000..db99146e --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/image.jl @@ -0,0 +1,66 @@ +""" + tools/image.jl - Image utilities + +This module provides image detection and encoding utilities. +""" + +module Image + +using ..Types: * + +function detectSupportedImageMimeType(buffer::Vector{UInt8})::Union{String, Nothing} + if length(buffer) >= 3 && buffer[1:3] == [0xff, 0xd8, 0xff] + if buffer[4] == 0xf7 + return nothing + end + return "image/jpeg" + end + + if length(buffer) >= 8 && buffer[1:8] == [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] + return "image/png" + end + + if length(buffer) >= 3 && buffer[1:3] == [0x47, 0x49, 0x46] + return "image/gif" + end + + if length(buffer) >= 12 && buffer[1:4] == [0x52, 0x49, 0x46, 0x46] && buffer[9:12] == [0x57, 0x45, 0x42, 0x50] + return "image/webp" + end + + if length(buffer) >= 2 && buffer[1:2] == [0x42, 0x4d] + return "image/bmp" + end + + return nothing +end + +function encodeBase64(bytes::Vector{UInt8})::String + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + output = "" + + for i in 1:3:length(bytes) + first_byte = i <= length(bytes) ? bytes[i] : 0 + second_byte = i+1 <= length(bytes) ? bytes[i+1] : 0 + third_byte = i+2 <= length(bytes) ? bytes[i+2] : 0 + + output *= alphabet[first_byte >> 2 + 1] + output *= alphabet[(((first_byte & 0x03) << 4) | ((second_byte >> 4) & 0x0f)) + 1] + + if i+1 <= length(bytes) + output *= alphabet[(((second_byte & 0x0f) << 2) | ((third_byte >> 6) & 0x03)) + 1] + else + output *= "=" + end + + if i+2 <= length(bytes) + output *= alphabet[third_byte & 0x3f + 1] + else + output *= "=" + end + end + + return output +end + +end diff --git a/packages/agent/julia_implementation/src/tools/index.jl b/packages/agent/julia_implementation/src/tools/index.jl new file mode 100644 index 00000000..d20be3aa --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/index.jl @@ -0,0 +1,35 @@ +""" + tools/index.jl - Tool exports + +This module exports all tools. +""" + +module ToolsIndex + +using ..Tools.Bash: createBashTool +using ..Tools.Read: createReadTool +using ..Tools.Write: createWriteTool +using ..Tools.Edit: createEditTool +using ..Tools.Edit: EditToolDetails, EditToolInput +using ..Tools.Read: ReadToolDetails, ReadToolInput, ReadToolOptions, ReadImageProcessor, ReadImageProcessorResult + +export + createBashTool, + createReadTool, + createWriteTool, + createEditTool, + BashExecution, + BashPrepare, + BashToolDetails, + BashToolInput, + BashToolOptions, + EditToolDetails, + EditToolInput, + ReadToolDetails, + ReadToolInput, + ReadToolOptions, + ReadImageProcessor, + ReadImageProcessorResult, + WriteToolInput + +end diff --git a/packages/agent/julia_implementation/src/tools/path_utils.jl b/packages/agent/julia_implementation/src/tools/path_utils.jl new file mode 100644 index 00000000..dadacf49 --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/path_utils.jl @@ -0,0 +1,44 @@ +""" + tools/path_utils.jl - Path resolution utilities + +This module provides path resolution utilities for tools. +""" + +module PathUtils + +using ..Types: * +using ..HarnessTypes: ExecutionEnv, getOrThrow, FileError, Result + +function normalizeToolPath(path::String)::String + normalized = replace(path, r"[\u00A0\u2000-\u200A\u202F\u205F\u3000]" => " ") + if startswith(normalized, "@") + return normalized[2:end] + end + return normalized +end + +function resolveToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String + return getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) +end + +function resolveReadToolPath(env::ExecutionEnv, path::String, signal::Union{Any, Nothing}=nothing)::String + resolved = getOrThrow(getOrThrow(absolutePath(env, normalizeToolPath(path), signal), "Failed to resolve path")) + + variants = String[ + resolved, + replace(resolved, r" (AM|PM)\."i => " $1."), + normalized = replace(resolved, NFC => NFD), + replace(resolved, "'" => "\u2019"), + replace(replace(resolved, NFC => NFD), "'" => "\u2019"), + ] + + for variant in variants + if getOrThrow(getOrThrow(exists(env, variant, signal), "Failed to check existence"), "Not found") + return variant + end + end + + return resolved +end + +end diff --git a/packages/agent/julia_implementation/src/tools/read.jl b/packages/agent/julia_implementation/src/tools/read.jl new file mode 100644 index 00000000..b3584377 --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/read.jl @@ -0,0 +1,35 @@ +""" + tools/read.jl - File read tool + +This module provides the file read tool for AgentCore. +""" + +module Read + +using ..Types: * + +mutable struct ReadToolDetails + truncation::Union{Any, Nothing} +end + +mutable struct ReadToolOptions + auto_resize_images::Bool + image_processor::Union{Any, Nothing} +end + +function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext + return AgentTool( + "read", + "read", + "Read the contents of a file.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement read execution + return AgentToolResult([TextContent("File read successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/packages/agent/julia_implementation/src/tools/write.jl b/packages/agent/julia_implementation/src/tools/write.jl new file mode 100644 index 00000000..5d4acc3d --- /dev/null +++ b/packages/agent/julia_implementation/src/tools/write.jl @@ -0,0 +1,26 @@ +""" + tools/write.jl - File write tool + +This module provides the file write tool for AgentCore. +""" + +module Write + +using ..Types: * + +function createWriteTool{TContext}() where TContext + return AgentTool( + "write", + "write", + "Write content to a file.", + Dict{String, Any}(), + (tool_call_id, params, signal, on_update, context) -> begin + # TODO: Implement write execution + return AgentToolResult([TextContent("File written successfully")], nothing, nothing, nothing, nothing) + end, + nothing, + nothing, + ) +end + +end diff --git a/packages/agent/julia_implementation/src/types.jl b/packages/agent/julia_implementation/src/types.jl new file mode 100644 index 00000000..fd39e412 --- /dev/null +++ b/packages/agent/julia_implementation/src/types.jl @@ -0,0 +1,588 @@ +""" + types.jl - Core types for AgentCore + +This module defines the fundamental types used throughout the AgentCore package. +""" + +module Types + +using Dates +using UUIDs +using JSON3 +using Unicode + +# ============================================================================ +# Basic type aliases +# ============================================================================ + +const Timestamp = Int64 + +# ============================================================================ +# Thinking level enum +# ============================================================================ + +@enum ThinkingLevel begin + THINKING_OFF = "off" + THINKING_MINIMAL = "minimal" + THINKING_LOW = "low" + THINKING_MEDIUM = "medium" + THINKING_HIGH = "high" + THINKING_XHIGH = "xhigh" + THINKING_MAX = "max" +end + +# ============================================================================ +# Tool execution modes +# ============================================================================ + +@enum ToolExecutionMode begin + EXECUTION_SEQUENTIAL = "sequential" + EXECUTION_PARALLEL = "parallel" +end + +# ============================================================================ +# Queue drain modes +# ============================================================================ + +@enum QueueMode begin + QUEUE_ALL = "all" + QUEUE_ONE_AT_A_TIME = "one-at-a-time" +end + +# ============================================================================ +# Message content types +# ============================================================================ + +abstract type MessageContent end + +struct TextContent <: MessageContent + text::String +end + +struct ImageContent <: MessageContent + data::String + mime_type::String +end + +# ============================================================================ +# Message types +# ============================================================================ + +abstract type Message end + +struct UserMessage <: Message + role::String + content::Vector{MessageContent} + timestamp::Timestamp +end + +struct AssistantMessage <: Message + role::String + content::Vector{MessageContent} + api::String + provider::String + model::String + usage::Usage + stop_reason::String + error_message::Union{String, Nothing} + timestamp::Timestamp +end + +struct ToolResultMessage <: Message + role::String + tool_call_id::String + tool_name::String + content::Vector{MessageContent} + details::Any + usage::Union{Usage, Nothing} + added_tool_names::Union{Vector{String}, Nothing} + is_error::Bool + timestamp::Timestamp +end + +# ============================================================================ +# Usage statistics +# ============================================================================ + +struct UsageCost + input::Float64 + output::Float64 + cache_read::Float64 + cache_write::Float64 + total::Float64 +end + +struct Usage + input::Int64 + output::Int64 + cache_read::Int64 + cache_write::Int64 + total_tokens::Int64 + cost::UsageCost +end + +# ============================================================================ +# Model types +# ============================================================================ + +struct ModelCost + input::Float64 + output::Float64 + cache_read::Float64 + cache_write::Float64 +end + +struct Model{Api} + id::String + name::String + api::Api + provider::String + base_url::String + reasoning::Bool + input::Vector{String} + cost::ModelCost + context_window::Int64 + max_tokens::Int64 +end + +# ============================================================================ +# Agent message union type +# ============================================================================ + +abstract type AgentMessage end + +# Custom message types can extend this via multiple dispatch +struct CustomMessage <: AgentMessage + message::AgentMessage + custom_type::String +end + +# ============================================================================ +# Tool types +# ============================================================================ + +struct AgentToolResult{T} + content::Vector{MessageContent} + details::T + usage::Union{Usage, Nothing} + added_tool_names::Union{Vector{String}, Nothing} + terminate::Union{Bool, Nothing} +end + +struct AgentTool{TParameters, TDetails} + name::String + label::String + description::String + parameters::TParameters + execute::Function + prepare_arguments::Union{Function, Nothing} + execution_mode::Union{ToolExecutionMode, Nothing} +end + +# ============================================================================ +# Agent context +# ============================================================================ + +struct AgentContext + system_prompt::String + messages::Vector{AgentMessage} + tools::Union{Vector{AgentTool}, Nothing} +end + +# ============================================================================ +# Event types +# ============================================================================ + +abstract type AgentEvent end + +struct AgentStartEvent <: AgentEvent end +struct AgentEndEvent <: AgentEvent + messages::Vector{AgentMessage} +end +struct TurnStartEvent <: AgentEvent end +struct TurnEndEvent <: AgentEvent + message::AgentMessage + tool_results::Vector{ToolResultMessage} +end +struct MessageStartEvent <: AgentEvent + message::AgentMessage +end +struct MessageUpdateEvent <: AgentEvent + message::AgentMessage + assistant_message_event::Any +end +struct MessageEndEvent <: AgentEvent + message::AgentMessage +end +struct ToolExecutionStartEvent <: AgentEvent + tool_call_id::String + tool_name::String + args::Any +end +struct ToolExecutionUpdateEvent <: AgentEvent + tool_call_id::String + tool_name::String + args::Any + partial_result::Any +end +struct ToolExecutionEndEvent <: AgentEvent + tool_call_id::String + tool_name::String + result::Any + is_error::Bool +end + +# ============================================================================ +# Assistant message event types +# ============================================================================ + +abstract type AssistantMessageEvent end + +struct StartEvent <: AssistantMessageEvent + partial::AssistantMessage +end +struct TextStartEvent <: AssistantMessageEvent + content_index::Int64 + partial::AssistantMessage +end +struct TextDeltaEvent <: AssistantMessageEvent + content_index::Int64 + delta::String + partial::AssistantMessage +end +struct TextEndEvent <: AssistantMessageEvent + content_index::Int64 + content::String + partial::AssistantMessage +end +struct DoneEvent <: AssistantMessageEvent + reason::String + usage::Usage + message::AssistantMessage +end +struct ErrorEvent <: AssistantMessageEvent + reason::String + error_message::Union{String, Nothing} + usage::Usage + error::AssistantMessage +end + +# ============================================================================ +# Agent state +# ============================================================================ + +mutable struct AgentState + system_prompt::String + model::Model + thinking_level::ThinkingLevel + tools::Vector{AgentTool} + messages::Vector{AgentMessage} + is_streaming::Bool + streaming_message::Union{AgentMessage, Nothing} + pending_tool_calls::Set{String} + error_message::Union{String, Nothing} + + function AgentState( + system_prompt::String="", + model::Model=Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0), + thinking_level::ThinkingLevel=THINKING_OFF, + tools::Vector{AgentTool}=AgentTool[], + messages::Vector{AgentMessage}=AgentMessage[], + ) + new( + system_prompt, + model, + thinking_level, + copy(tools), + copy(messages), + false, + nothing, + Set{String}(), + nothing, + ) + end +end + +# ============================================================================ +# Tool call types +# ============================================================================ + +struct ToolCall + type::String + id::String + name::String + arguments::Dict{String, Any} + partial_json::Union{String, Nothing} +end + +# ============================================================================ +# Context transform types +# ============================================================================ + +struct PrepareNextTurnContext + message::AssistantMessage + tool_results::Vector{ToolResultMessage} + context::AgentContext + new_messages::Vector{AgentMessage} +end + +struct AgentLoopTurnUpdate + context::Union{AgentContext, Nothing} + model::Union{Model, Nothing} + thinking_level::Union{ThinkingLevel, Nothing} +end + +# ============================================================================ +# Before/After tool call types +# ============================================================================ + +struct BeforeToolCallContext + assistant_message::AssistantMessage + tool_call::ToolCall + args::Any + context::AgentContext +end + +struct BeforeToolCallResult + block::Union{Bool, Nothing} + reason::Union{String, Nothing} +end + +struct AfterToolCallContext + assistant_message::AssistantMessage + tool_call::ToolCall + args::Any + result::AgentToolResult + is_error::Bool + context::AgentContext +end + +struct AfterToolCallResult + content::Union{Vector{MessageContent}, Nothing} + details::Union{Any, Nothing} + is_error::Union{Bool, Nothing} + usage::Union{Usage, Nothing} + terminate::Union{Bool, Nothing} +end + +# ============================================================================ +# Stream function signature +# ============================================================================ + +const StreamFn = Function + +# ============================================================================ +# File types +# ============================================================================ + +struct FileKind + value::String +end +const FILE_KIND_FILE = FileKind("file") +const FILE_KIND_DIRECTORY = FileKind("directory") +const FILE_KIND_SYMLINK = FileKind("symlink") + +struct FileInfo + name::String + path::String + kind::FileKind + size::Int64 + mtime_ms::Int64 +end + +struct FileError <: Exception + code::String + message::String + path::Union{String, Nothing} + cause::Union{Exception, Nothing} +end + +struct ExecutionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct CompactionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct BranchSummaryError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct SessionError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +struct AgentHarnessError <: Exception + code::String + message::String + cause::Union{Exception, Nothing} +end + +# ============================================================================ +# Session tree entry types +# ============================================================================ + +abstract type SessionTreeEntry end + +struct SessionTreeEntryBase + type::String + id::String + parent_id::Union{String, Nothing} + timestamp::String +end + +struct MessageEntry <: SessionTreeEntry + base::SessionTreeEntryBase + message::AgentMessage +end + +struct ThinkingLevelChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + thinking_level::String +end + +struct ModelChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + provider::String + model_id::String +end + +struct ActiveToolsChangeEntry <: SessionTreeEntry + base::SessionTreeEntryBase + active_tool_names::Vector{String} +end + +struct CompactionEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + summary::String + first_kept_entry_id::Union{String, Nothing} + tokens_before::Int64 + retained_tail::Union{Vector{AgentMessage}, Nothing} + details::Union{T, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct BranchSummaryEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + from_id::String + summary::String + details::Union{T, Nothing} + usage::Union{Usage, Nothing} + from_hook::Bool +end + +struct CustomEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + custom_type::String + data::Union{T, Nothing} +end + +struct CustomMessageEntry{T} <: SessionTreeEntry + base::SessionTreeEntryBase + custom_type::String + content::String + details::Union{T, Nothing} + display::Bool +end + +struct LabelEntry <: SessionTreeEntry + base::SessionTreeEntryBase + target_id::String + label::Union{String, Nothing} +end + +struct SessionInfoEntry <: SessionTreeEntry + base::SessionTreeEntryBase + name::Union{String, Nothing} +end + +struct LeafEntry <: SessionTreeEntry + base::SessionTreeEntryBase + target_id::Union{String, Nothing} +end + +# ============================================================================ +# Session context +# ============================================================================ + +struct SessionContext + messages::Vector{AgentMessage} + thinking_level::String + model::Union{Dict{String, String}, Nothing} + active_tool_names::Union{Vector{String}, Nothing} +end + +# ============================================================================ +# Session stats +# ============================================================================ + +struct SessionStats + message_count::Int64 + cached_tokens::Int64 + uncached_tokens::Int64 + total_tokens::Int64 + cost_total::Float64 +end + +# ============================================================================ +# Session metadata +# ============================================================================ + +abstract type SessionMetadata end + +struct JsonlSessionMetadata <: SessionMetadata + id::String + created_at::String + cwd::String + path::String + parent_session_path::Union{String, Nothing} + metadata::Union{Dict{String, Any}, Nothing} +end + +# ============================================================================ +# Session storage interface +# ============================================================================ + +abstract type SessionStorage{T<:SessionMetadata} end + +# ============================================================================ +# Session repo interface +# ============================================================================ + +abstract type SessionRepo< + TMetadata<:SessionMetadata, + TCreateOptions, + TListOptions +> end + +# ============================================================================ +# Helper functions +# ============================================================================ + +function create_timestamp()::String + return string(Dates.now(Dates.UTC)) +end + +function uuidv7()::String + return string(UUIDs.uuid7()) +end + +function uuidstring()::String + return string(UUIDs.uuid4()) +end + +function tempname()::String + return tempname() +end + +end