update
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name = "AgentCore"
|
||||||
|
uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f"
|
||||||
|
authors = ["Mario Zechner <post@badlogicgames.com>"]
|
||||||
|
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"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name = "AgentCore"
|
||||||
|
uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f"
|
||||||
|
authors = ["Mario Zechner <post@badlogicgames.com>"]
|
||||||
|
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"]
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||||
|
|
||||||
|
<summary>
|
||||||
|
"""
|
||||||
|
|
||||||
|
const COMPACTION_SUMMARY_SUFFIX = """
|
||||||
|
</summary>"""
|
||||||
|
|
||||||
|
const BRANCH_SUMMARY_PREFIX = """The following is a summary of a branch that this conversation came back from:
|
||||||
|
|
||||||
|
<summary>
|
||||||
|
"""
|
||||||
|
|
||||||
|
const BRANCH_SUMMARY_SUFFIX = """</summary>"""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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
|
||||||
@@ -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<PromptTemplateFrontmatter>(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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 = "<skill name=\"$(skill.name)\" location=\"$(skill.filePath)\">\nReferences are relative to $(dirnameEnvPath(skill.filePath)).\n\n$(skill.content)\n</skill>"
|
||||||
|
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<SkillFrontmatter>(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
|
||||||
@@ -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
|
||||||
@@ -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.",
|
||||||
|
"",
|
||||||
|
"<available_skills>",
|
||||||
|
]
|
||||||
|
|
||||||
|
for skill in visible_skills
|
||||||
|
push!(lines, " <skill>")
|
||||||
|
push!(lines, " <name>$(escapeXml(skill.name))</name>")
|
||||||
|
push!(lines, " <description>$(escapeXml(skill.description))</description>")
|
||||||
|
push!(lines, " <location>$(escapeXml(skill.filePath))</location>")
|
||||||
|
push!(lines, " </skill>")
|
||||||
|
end
|
||||||
|
|
||||||
|
push!(lines, "</available_skills>")
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user