pi harness reimplement

This commit is contained in:
2026-07-28 07:37:57 +07:00
parent b7658af76b
commit 984a678d92
37 changed files with 6124 additions and 5696 deletions
+119
View File
@@ -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
+20 -1122
View File
File diff suppressed because it is too large Load Diff
+17 -32
View File
@@ -1,37 +1,22 @@
name = "YiemAgent"
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
name = "AgentCore"
uuid = "6e2f7b3a-9a0b-4e8e-8f8f-8f8f8f8f8f8f"
authors = ["Mario Zechner <post@badlogicgames.com>"]
version = "0.8.0"
authors = ["narawat lamaiin <narawat@outlook.com>"]
[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
GeneralUtils = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
LLMMCTS = "d76c5a4d-449e-4835-8cc4-dd86ec44f241"
LibPQ = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
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"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
Sockets = "6462fe0b-2de3-572b-8e7f-4c2f5e2c2e2b"
Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf2a872"
UUIDs = "cf7118a7-4649-5bc2-89ac-36d7b14660ca"
[compat]
Base64 = "1.11.0"
CSV = "0.10.15"
DataFrames = "1.7.0"
GeneralUtils = "0.5.10"
HTTP = "2.4.0"
JSON = "1.6.1"
LLMMCTS = "0.1.5"
NATS = "0.1.0"
SQLLLM = "0.2.8"
Serde = "3.7.2"
[extras]
Test = "8dfed614-e22c-5e4d-98d3-97fe1b80e45d"
[targets]
test = ["Test"]
+179 -7
View File
@@ -1,7 +1,179 @@
version 0.1.0
TODO:
[WORKING] build MCTS() for planning
[] executeplan() to execute the plan
Change from version: 0.0.9
-
# 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.
-72
View File
@@ -1,72 +0,0 @@
To make **LLM-driven inference** fast while maintaining its dynamic capabilities, there are a few practices or approaches to avoid, as they could lead to performance bottlenecks or inefficiencies. Here's what *not* to do:
---
### **1. Avoid Using Overly Large Models for Every Query**
While larger LLMs like GPT-4 provide high accuracy and nuanced responses, they may slow down real-time processing due to their computational complexity. Instead:
- Use distilled or smaller models (e.g., GPT-3.5 Turbo or fine-tuned versions) for faster inference without compromising much on quality.
---
### **2. Avoid Excessive Entity Preprocessing**
Dont rely on overly complicated preprocessing steps (like advanced NER models or regex-heavy pipelines) to extract entities from the query before invoking the LLM. This could add latency. Instead:
- Design efficient prompts that allow the LLM to extract entities and generate responses simultaneously.
---
### **3. Avoid Asking the LLM Multiple Separate Questions**
Running the LLM for multiple subtasks—for example, entity extraction first and response generation second—can significantly slow down the pipeline. Instead:
- Create prompts that combine tasks into one pass, e.g., *"Identify the city name and generate a weather response for this query: 'What's the weather in London?'"*.
---
### **4. Dont Overload the LLM with Context History**
Excessively lengthy conversation history or irrelevant context in your prompts can slow down inference times. Instead:
- Provide only the relevant context for each query, trimming unnecessary parts of the conversation.
---
### **5. Avoid Real-Time Dependence on External APIs**
Using external APIs to fetch supplementary data (e.g., weather details or location info) during every query can introduce latency. Instead:
- Pre-fetch API data asynchronously and use the LLM to integrate it dynamically into responses.
---
### **6. Avoid Running LLM on Underpowered Hardware**
Running inference on CPUs or low-spec GPUs will result in slower response times. Instead:
- Deploy the LLM on optimized infrastructure (e.g., high-performance GPUs like NVIDIA A100 or cloud platforms like Azure AI) to reduce latency.
---
### **7. Skip Lengthy Generative Prompts**
Avoid prompts that encourage the LLM to produce overly detailed or verbose responses, as these take longer to process. Instead:
- Use concise prompts that focus on generating actionable or succinct answers.
---
### **8. Dont Ignore Optimization Techniques**
Failing to optimize your LLM setup can drastically impact performance. For example:
- Avoid skipping techniques like model quantization (reducing numerical precision to speed up inference) or distillation (training smaller models).
---
### **9. Dont Neglect Response Caching**
While you may not want a full caching system to avoid sunk costs, dismissing lightweight caching entirely can impact speed. Instead:
- Use temporary session-based caching for very frequent queries, without committing to a full-fledged cache infrastructure.
---
### **10. Avoid One-Size-Fits-All Solutions**
Applying the same LLM inference method to all queries—whether simple or complex—will waste processing resources. Instead:
- Route basic queries to faster, specialized models and use the LLM for nuanced or multi-step queries only.
---
### Summary: Focus on Efficient Design
By avoiding these pitfalls, you can ensure that LLM-driven inference remains fast and responsive:
- Optimize prompts.
- Use smaller models for simpler queries.
- Run the LLM on high-performance hardware.
- Trim unnecessary preprocessing or contextual steps.
Would you like me to help refine a prompt or suggest specific tools to complement your implementation? Let me know!
+149
View File
@@ -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
-537
View File
@@ -1,537 +0,0 @@
https://github.com/andyz245/LanguageAgentTreeSearch/blob/main/hotpot/hotpot.py
standard_prompt = '''
Write a coherent passage of 4 short paragraphs. The end sentence of each paragraph must be: {input}
'''
reflection_prompt = '''You are an advanced reasoning agent that can improve based on self refection. You will be given a previous reasoning trial in which you were given access to an Docstore API environment and a question to answer. You were unsuccessful in answering the question either because you guessed the wrong answer with Finish[<answer>], or you used up your set number of reasoning steps. In a few sentences, Diagnose a possible reason for failure and devise a new, concise, high level plan that aims to mitigate the same failure. Use complete sentences.
Here are some examples:
Previous Trial:
Question: The Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of what?
Thought 1: I need to search Rome Protocols, find the three Prime Ministers, then find what they were assassinated as part of.
Action 1: Search[Rome Protocols]
Observation 1: The Rome Protocols were a series of three international agreements concluded in Rome on 17 March 1934 between the governments of Austria, Hungary and Italy. They were signed by Italian Prime Minister Benito Mussolini, Austrian Chancellor Engelbert Dollfuss and Hungarian Prime Minister Gyula Gömbös. All of the protocols went into effect on 12 July 1934 and were registered in League of Nations Treaty Series on 12 December 1934.
Thought 2: The three Prime Ministers were Benito Mussolini, Engelbert Dollfuss, and Gyula Gömbös. I need to search Benito Mussolini and find what he was assassinated as part of.
Action 2: Search[Benito Mussolini]
Observation 2: Benito Amilcare Andrea Mussolini (UK: MU(U)SS-ə-LEE-nee, US: MOOSS-, Italian: [beˈniːto aˈmilkare anˈdrɛːa mussoˈliːni]; 29 July 1883 28 April 1945) was an Italian politician and journalist who founded and led the National Fascist Party (PNF). He was Prime Minister of Italy from the March on Rome in 1922 until his deposition in 1943, as well as "Duce" of Italian fascism from the establishment of the Italian Fasces of Combat in 1919 until his summary execution in 1945 by Italian partisans. As dictator of Italy and principal founder of fascism, Mussolini inspired and supported the international spread of fascist movements during the inter-war period.Mussolini was originally a socialist politician and a journalist at the Avanti! newspaper. In 1912, he became a member of the National Directorate of the Italian Socialist Party (PSI), but he was expelled from the PSI for advocating military intervention in World War I, in opposition to the party's stance on neutrality. In 1914, Mussolini founded a new journal, Il Popolo d'Italia, and served in the Royal Italian Army during the war until he was wounded and discharged in 1917. Mussolini denounced the PSI, his views now centering on Italian nationalism instead of socialism, and later founded the fascist movement which came to oppose egalitarianism and class conflict, instead advocating "revolutionary nationalism" transcending class lines. On 31 October 1922, following the March on Rome (2830 October), Mussolini was appointed prime minister by King Victor Emmanuel III, becoming the youngest individual to hold the office up to that time. After removing all political opposition through his secret police and outlawing labor strikes, Mussolini and his followers consolidated power through a series of laws that transformed the nation into a one-party dictatorship. Within five years, Mussolini had established dictatorial authority by both legal and illegal means and aspired to create a totalitarian state. In 1929, Mussolini signed the Lateran Treaty with the Holy See to establish Vatican City.
Mussolini's foreign policy aimed to restore the ancient grandeur of the Roman Empire by expanding Italian colonial possessions and the fascist sphere of influence. In the 1920s, he ordered the Pacification of Libya, instructed the bombing of Corfu over an incident with Greece, established a protectorate over Albania, and incorporated the city of Fiume into the Italian state via agreements with Yugoslavia. In 1936, Ethiopia was conquered following the Second Italo-Ethiopian War and merged into Italian East Africa (AOI) with Eritrea and Somalia. In 1939, Italian forces annexed Albania. Between 1936 and 1939, Mussolini ordered the successful Italian military intervention in Spain in favor of Francisco Franco during the Spanish Civil War. Mussolini's Italy initially tried to avoid the outbreak of a second global war, sending troops at the Brenner Pass to delay Anschluss and taking part in the Stresa Front, the Lytton Report, the Treaty of Lausanne, the Four-Power Pact and the Munich Agreement. However, Italy then alienated itself from Britain and France by aligning with Germany and Japan. Germany invaded Poland on 1 September 1939, resulting in declarations of war by France and the UK and the start of World War II.
On 10 June 1940, Mussolini decided to enter the war on the Axis side. Despite initial success, the subsequent Axis collapse on multiple fronts and eventual Allied invasion of Sicily made Mussolini lose the support of the population and members of the Fascist Party. As a consequence, early on 25 July 1943, the Grand Council of Fascism passed a motion of no confidence in Mussolini; later that day King Victor Emmanuel III dismissed him as head of government and had him placed in custody, appointing Pietro Badoglio to succeed him as Prime Minister. After the king agreed to an armistice with the Allies, on 12 September 1943 Mussolini was rescued from captivity in the Gran Sasso raid by German paratroopers and Waffen-SS commandos led by Major Otto-Harald Mors. Adolf Hitler, after meeting with the rescued former dictator, then put Mussolini in charge of a puppet regime in northern Italy, the Italian Social Republic (Italian: Repubblica Sociale Italiana, RSI), informally known as the Salò Republic, causing a civil war. In late April 1945, in the wake of near total defeat, Mussolini and his mistress Clara Petacci attempted to flee to Switzerland, but both were captured by Italian communist partisans and summarily executed by firing squad on 28 April 1945 near Lake Como. The bodies of Mussolini and his mistress were then taken to Milan, where they were hung upside down at a service station to publicly confirm their demise.
Thought 3: Benito Mussolini was assassinated as part of the collapse of the Axis powers during World War II. So the Rome Protocols were signed by three Prime Ministers one of which was assassinated as part of World War II.
Action 3: Finish[World War II]
Reflection: I searched one of the prime ministers involved in the signing, then attemted to answer right away. I should have searched each of the prime ministers, then looked up 'death' on each of their pages in order to get more information before answering.
Previous Trial:
Question: Kam Heskin plays Paige Morgan in a 2004 film directed by who?
Thought 1: I need to search Kam Heskin and Paige Morgan, find the 2004 film they are in, then find the director of the film.
Action 1: Search[Kam Heskin]
Observation 1: Kam Erika Heskin (born May 8, 1973) is an American actress. She began her career playing Caitlin Deschanel on the NBC daytime soap opera Sunset Beach (19981999), before appearing in films Planet of the Apes (2001) and Catch Me If You Can (2002). Heskin went on in 2003 to play Elizabeth Bennet in an independent film Pride & Prejudice: A Latter-Day Comedy, Robin in Charmed (2004), and Paige Morgan in The Prince and Me film franchise (20062010).
Thought 2: Kam Heskin plays Paige Morgan in The Prince and Me film franchise. I need to search The Prince and Me film franchise and find the director of the 2004 film.
Action 2: Search[The Prince and Me film franchise]
Observation 2: Could not find [The Prince and Me film franchise]. Similar: ['Kam Heskin', 'Prince of Persia', 'Harry Potter and the Half-Blood Prince (film)', 'Prince of Persia: The Sands of Time (film)', 'The Chronicles of Narnia (film series)', 'Majestic Prince (franchise)', 'Kingsman (franchise)', 'The Little Prince (2015 film)', 'Shrek (franchise)', 'Harry Potter (film series)']
Thought 3: I need to search The Prince and Me (2004 film) instead.
Action 3: Search[The Prince and Me (2004 film)]
Observation 3: Could not find [The Prince and Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Kam Heskin', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince of Persia', 'Brooklynn Prince', 'Prince (musician)']
Thought 4: I need to search The Prince & Me (2004 film).
Action 4: Search[The Prince & Me (2004 film)]
Observation 4: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Thought 5: I need to search The Prince & Me (2004 film) instead.
Action 5: Search[The Prince & Me (2004 film)]
Observation 5: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Thought 6: I need to search The Prince & Me (2004 film).
Action 6: Search[The Prince & Me (2004 film)]
Observation 6: Could not find [The Prince & Me (2004 film)]. Similar: ['The Prince & Me', 'The Prince & Me 2: The Royal Wedding', 'Prince of Darkness (film)', 'Prince of Persia: The Sands of Time (film)', 'Rob Knox', 'Alexander (2004 film)', 'Prince (musician)', 'Prince of Persia', 'Kam Heskin', 'Brooklynn Prince']
Reflection: I got stuck in a loop where I kept trying to search 'The Prince & Me (2004 film)' but the page could not be found. Instead I should have tried to search the similar results that had a similar name to see and they were made in 2004.
Previous Trial:
Question: The Oberoi family is part of a hotel company that has a head office in what city?
Thought 1: I need to search the Oberoi family and find the city where the hotel company's head office is located.
Action 1: Search[Oberoi family]
Observation 1: The Oberoi Group is a luxury hotel group with its head office in New Delhi, India.[2] Founded in 1934, the company owns and operates 32 luxury hotels and two river cruise ships in 7 countries, primarily under its Oberoi Hotels & Resorts and Trident brands.[3] The group also operates The Oberoi Centre for Learning and Development, which is regarded as one of Asia's top institutions for hospitality education.[4]. The foundations of the Oberoi Group date back to 1934 when The Rai Bahadur Mohan Singh Oberoi, the founder of the group, bought two properties: the Maidens in Delhi and the Clarke's in Shimla.[5] In the following years Oberoi, assisted by his two sons, Tilak Raj Singh Oberoi and Prithvi Raj Singh Oberoi (P.R.S. Oberoi
Thought 2: The Oberoi Group has its head office in New Delhi, India. I just need to finish with New Delhi.
Action 2: Finish[New Delhi]
Observation 2: Episode finished, reward = 0
Reflection: I appeared to have retrieved the correct information about The Oberoi Family and the location of it's head office, and provided a corresponding answer. However this answer does not exactly match the ground truth answer so I should try a different wording, such as Delhi.
Previous trial:
{trajectory}Reflection:'''
cot_prompt = '''
Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales.
Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
cot_prompt_short = '''
Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
cot_prompt_feedback_short = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
You have attempted to answer the following question before and failed. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question.
{trajectories}
{input}
'''
cot_prompt_feedback = '''You are also an advanced reasoning agent that can improve based on self refection. Solve a question answering task with interleaving Thought, Action, Observation steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
After each observation, provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Observation 1: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas. This took place from 1780 to 1650 million years ago (Mya), during the Paleoproterozoic (Statherian Period). It is recorded in the Colorado orogen, a >500-km-wide belt of oceanic arc rock that extends southward into New Mexico. The Colorado orogeny was likely part of the larger Yavapai orogeny.
Thought 2: It does not mention the eastern sector of the Colorado orogeny. I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Observation 2: (Result 1 / 1) The Colorado orogen, formerly called the Colorado province, is a >500-km-wide belt of oceanic arc rock (1.78\u20131.65 Ga) that extends southward into New Mexico and composes a major part of the Proterozoic provinces of southwestern United States. This transcontinental collisional event occurred during the Paleoproterozoic (Statherian Period).[1] The Wyoming sector of the Colorado orogeny was formerly called the Medicine Bow orogeny. The eastern sector extends into the High Plains and is called the Central Plains orogeny. The boundary between the Colorado orogeny and the Wyoming craton is the Cheyenne belt, a 5-km-wide mylonitic shear zone that verges northward. The Cheyenne belt transects and cuts off the south edge of the older Trans-Hudson orogeny.[2]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]Observation 3: High Plains refers to one of two distinct land regions:
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Observation 4: The High Plains are a subregion of the Great Plains, mainly in the Western United States, but also partly in the Midwest states of Nebraska, Kansas, and South Dakota, generally encompassing the western part of the Great Plains before the region reaches the Rocky Mountains. The High Plains are located in eastern Montana, southeastern Wyoming, southwestern South Dakota, western Nebraska, eastern Colorado, western Kansas, eastern New Mexico, western Oklahoma, and to just south of the Texas Panhandle.[4] The southern region of the Western High Plains ecology region contains the geological formation known as Llano Estacado which can be seen from a short distance or on satellite maps.[5] From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Observation 1: Guitars for Wounded Warriors is the sixth solo studio album by guitarist Brian Tarquin, released in August 2014 by Cleopatra Records. In 2016, it received a Best Compilation Album nomination by the Independent Music Awards. All tracks were recorded at Tarquin's Jungle Room Studios in New Paltz (village), New York. Being moved by the lack of support for returning veterans through his life Tarquin decided to compose a heavy instrumental rock album as a way to show his appreciation to all veterans. So once again he enlisted top notch players to guest with him on the album, Billy Sheehan on Black Hawk, Ron \"Bumblefoot\" Thal and Reb Beach on Taliban Terror, Steve Morse on Freedom, Chris Poland on Charlie Surfs and Hunting, Chuck Loeb on Escape Kabul, Hal Lindes on Sand & Blood, Gary Hoey on 5 Klicks To Hell and Baghdad, Alex De Rosso Dokken on Broken Arrow, and The Flyin' Ryan Brothers on Surgical Strike. The entire album was engineered, produced and composed by Tarquin especially for each one of the guest guitarists. Partial proceeds are donated to the Fisher House Foundation from sales.
Thought 2: Guitars for Wounded Warriors was recorded in New Paltz (village), New York. I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Observation 2: New Paltz is a village in Ulster County located in the U.S. state of New York. It is approximately 80 miles (130 km) north of New York City and 70 miles (110 km) south of Albany. The population was 7,324 at the 2020 census.[3]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
You have attempted to answer the following question before and failed, either because your reasoning for the answer was incorrect or the phrasing of your response did not exactly match the answer. The following reflection(s) give a plan to avoid failing to answer the question in the same way you did previously. Use them to improve your strategy of correctly answering the given question.
{trajectories}
When providing the thought and action for the current trial, that into account these failed trajectories and make sure not to repeat the same mistakes and incorrect answers.
{input}
'''
vote_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a list of trajectories, decide which trajectory is most promising. Analyze each trajectory in detail and consider possible errors, then conclude in the last line "The best trajectory is {s}", where s the integer id of the trajectory.
'''
compare_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Briefly analyze the correctness of the following two trajectories. Conclude in the last line "The more correct trajectory is 1", "The more correct trajectory is 2", or "The two trajectories are similarly correct".
'''
score_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by pairs of thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, analyze the following trajectory, then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
'''
value_prompt = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
Thus the correctness score is 3
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
Thus the correctness score is 4
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
Thus the correctness score is 10
{input}
'''
value_prompt_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
Thus the correctness score is 10
{trajectories}
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
Thus the correctness score is 10
{input}
'''
value_prompt_reasoning = '''You are an advanced reasoning agent that can improve based on self refection. Analyze the trajectories of your previous solutions to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action.
Thus the correctness score is 10
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager.
Thus the correctness score is 4
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{input}
'''
value_prompt_reasoning_feedback = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners it is reasonable to checkof the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Which magazine was started first Arthur's Magazine or First for Women?
Thought 1: I need to search Arthur's Magazine and First for Women, and find which was started first.
Action 1: Search[Arthur's Magazine]
Observation 1: Arthur's Magazine (1844\u00e2\u0080\u00931846) was an American literary periodical published in Philadelphia in the 19th century. Edited by Timothy Shay Arthur, it featured work by Edgar A. Poe, J.H. Ingraham, Sarah Josepha Hale, Thomas G. Spear, and others.[1][2] In May 1846 it was merged into Godey's Lady's Book.[3]
This trajectory is correct as it is reasonable to search for the first magazine provided in the question. It is also better to have simple searches corresponding to a single entity, making this the best action.
Thus the correctness score is 10
Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?Question: The football manager who recruited David Beckham managed Manchester United during what timeframe?
Thought 1: I need to search for the football manager who recruited David Beckham, and then find out the timeframe in which he managed Manchester United.
Action 1: Search[David Beckham's football manager]
Observation 1: Could not find David Beckham's football manager. Similar: ['David Beckham', 'Inter Miami CF (redirect from Miami Beckham United)', 'Victoria Beckham', 'The Beckham Experiment', 'Miami Freedom Park'].
Thought 2: I couldn't find the information on David Beckham's football manager. I need to search Manchester United and find out the timeframe of the manager.
Action 2: Search[Manchester United]
Observation 2: Manchester United Football Club, commonly referred to as Man United (often stylised as Man Utd), or simply United, is a professional football club based in Old Trafford, Greater Manchester, England. The club competes in the Premier League, the top division in the English football league system. Nicknamed the Red Devils, they were founded as Newton Heath LYR Football Club in 1878, but changed their name to Manchester United in 1902. After a spell playing in Clayton, Manchester, the club moved to their current stadium, Old Trafford, in 1910.. Domestically, Manchester United have won a record 20 league titles, 12 FA Cups, six League Cups and a record 21 FA Community Shields.
Thought 3: The information about David Beckham's football manager is not available. I will search for Manchester United's managers to find the timeframe.
Action 3: Search[Manchester United managers]
Observation 3: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
Thought 4: The information about David Beckham's football manager is not available. The question asks for the timeframe when the manager managed Manchester United, so I need to find a list of Manchester United managers.
Action 4: Search[list of Manchester United managers]
Observation 4: Manchester United Football Club is a professional football club based in Old Trafford, Greater Manchester, England. The club was formed in Newton Heath in 1878 as Newton Heath LYR F.C., and played their first competitive match in October 1886, when they entered the First Round of the 188687 FA Cup. The club was renamed Manchester United F.C. in 1902, and moved to Old Trafford in 1910.. From the beginning of the club's official managerial records in 1892 to the start of the 202223 season, Manchester United have had 23 full-time managers.
This trajectory is mostly incorrect as I was not able to retrieve the relevant information to answer the question. The searches are too broad and not relevant to the specific player recruited, David Beckham. The last two actions are also too similar. It would be better to first search for David Beckham and lookup information related to his manager.
Thus the correctness score is 4
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{trajectories}
{input}
'''
value_prompt_reasoning_feedback_short = '''Analyze the trajectories of a solution to a question answering task. The trajectories are labeled by environmental observations about the situation, thoughts that can reason about the current situation and actions that can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Given a question and a trajectory, evaluate its correctness and provide your reasoning and analysis in detail. Focus on the latest thought, action, and observation. Incomplete trajectories can be correct if the thoughts and actions so far are correct, even if the answer is not found yet. Do not generate additional thoughts or actions. Then at the last line conclude "Thus the correctness score is {s}", where s is an integer from 1 to 10.
Question: Which other Mexican Formula One race car driver has held the podium besides the Force India driver born in 1990?
Thought 1: I need to search for a list of Mexican Formula One race car drivers, then eliminate the one who drove for Force India and was born in 1990.
Action 1: Search[Formula One drivers from Mexico]
Observation 1: There have been six Formula One drivers from Mexico who have taken part in races since the championship began in 1950. Sergio P\u00e9rez is the most successful, having won six Grands Prix. Since 2021, P\u00e9rez drives for the Red Bull team.. P\u00e9rez is the only Mexican F1 driver to achieve a pole position. He took his first pole position at the 2022 Saudi Arabian Grand Prix, in his 219th Grand Prix meeting (a record for the most Grands Prix before a pole).
Thought 2: Sergio P\u00e9rez, born in 1990 and drove for Force India, is not the Mexican driver we are looking for. The passage does not give me enough information about other Mexican drivers who have achieved a podium finish. I need to look up more information.
Action 2: Lookup[Formula One drivers from Mexico]
Observation 2: No more results.
Thought 3: The available information doesn't allow me to determine which other Mexican Formula One race car driver, besides Sergio P\u00e9rez, has held the podium. With the search function failing to provide a full list of Mexican Formula One drivers, I need to return different search results. Let's try searching for Mexican Grand Prix winners, as they would have held the podium.
Action 3: Search[Mexican Grand Prix winners]
Observation 3: Could not find Mexican Grand Prix winners. Similar: ['Mexican Grand Prix', 'List of Formula One Grand Prix winners', '1990 Mexican Grand Prix', '2018 Mexican Grand Prix', '2019 Mexican Grand Prix'].
Thought 4: Given the difficulties in finding a direct list of Mexican Grand Prix winners, I should search for the Mexican Grand Prix to get a broader picture of the race's history. This might include winners.
Action 4: Search[Mexican Grand Prix]
This trajectory is incorrect as my search should be related to Mexican Formula One race car drivers, not winners of the Mexican Grand Prix, a seperate event. A better search would have been for the List of Formula One Grand Prix winners, as suggested.
Thus the correctness score is 3
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Observation 1: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought 2: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action 2: Lookup[named after]
Observation 2: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
This trajectory is correct as all of my thoughts and actions are correct. It makes sense to search for Milhouse first as it is the central subject of the question. It is also correct to directly look up the relevant information in the article, instead of trying another search.
Thus the correctness score is 10
{trajectories}
{input}
'''
rap_prompt = '''
Solve a question answering task with interleaving Thought and Action steps. Thought can reason about the current situation, and Action can be three types:
(1) Search[entity], which searches the exact entity on Wikipedia and returns the first paragraph if it exists. If not, it will return some similar entities to search.
(2) Lookup[keyword], which returns the next sentence containing keyword in the current passage.
(3) Finish[answer], which returns the answer and finishes the task.
Provide the next Thought and next Action. Here are some examples:
Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought 1: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action 1: Search[Colorado orogeny]
Thought 2: Then I need to look up eastern sector.
Action 2: Lookup[eastern sector]
Thought 3: The eastern sector of Colorado orogeny extends into the High Plains, so High Plains is the area. I need to search High Plains and find its elevation range.
Action 3: Search[High Plains]
Thought 4: I need to search High Plains (United States).
Action 4: Search[High Plains (United States)]
Thought 5: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action 5: Finish[1,800 to 7,000 ft]
Question: Guitars for Wounded Warriors is an album that was recorded in the village in which New York county?
Thought 1: I need to search Guitars for Wounded Warriors, then find in which New York county it was recorded.
Action 1: Search[Guitars for Wounded Warriors]
Thought 2: I need to search New Paltz (village), New York and find which county it is in
Action 2: Search[New Paltz (village), New York]
Thought 3: New Paltz (village), New York is in Ulster County, so the answer is Ulster County.
Action 3: Finish[Ulster County]
Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought 1: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action 1: Search[Milhouse]
Thought 2: I can look up "named after" for finding the specific individual Milhouse is named after.
Action 2: Lookup[named after]
Thought 3: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action 3: Finish[President Richard Nixon]
{input}
'''
-47
View File
@@ -1,47 +0,0 @@
module YiemAgent
# export agent
""" Order by dependencies of each file. The 1st included file must not depend on any other
files and each file can only depend on the file included before it.
"""
include("type.jl")
using .type
include("util.jl")
using .util
include("llmfunction.jl")
using .llmfunction
include("interface.jl")
using .interface
# ---------------------------------------------- 100 --------------------------------------------- #
end # module YiemAgent_v1
+416
View File
@@ -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
+861
View File
@@ -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
+1083
View File
File diff suppressed because it is too large Load Diff
-1400
View File
File diff suppressed because it is too large Load Diff
-1647
View File
File diff suppressed because it is too large Load Diff
+183
View File
@@ -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
+335
View File
@@ -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
+148
View File
@@ -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
+290
View File
@@ -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
+133
View File
@@ -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
+227
View File
@@ -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
+65
View File
@@ -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
+422
View File
@@ -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
+375
View File
@@ -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
+45
View File
@@ -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
+56
View File
@@ -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, "&" => "&amp;")
result = replace(result, "<" => "&lt;")
result = replace(result, ">" => "&gt;")
result = replace(result, "\"" => "&quot;")
result = replace(result, "'" => "&apos;")
return result
end
end
+49
View File
@@ -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
+32
View File
@@ -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
+67
View File
@@ -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
+59
View File
@@ -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
+66
View File
@@ -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
+35
View File
@@ -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
+44
View File
@@ -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
+35
View File
@@ -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
+26
View File
@@ -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
-375
View File
@@ -1,375 +0,0 @@
module type
export agent, sommelier, companion, virtualcustomer, agentcontext
using Dates, UUIDs, DataStructures, JSON, NATS
using GeneralUtils
# ---------------------------------------------- 100 --------------------------------------------- #
mutable struct agentcontext
text2textInstructLLM::Function
getTextEmbedding::Function
executeSQL::Function
similarSQLVectorDB::Function
insertSQLVectorDB::Function
similarSommelierDecision::Function
insertSommelierDecision::Function
find_related_tables_for_user_question::Function
pg_conn_str::String
agentconfig::AbstractDict
end
abstract type agent end
mutable struct sommelier <: agent
name::String # agent name
id::String # agent id
retailername::String
retailerid::String
tools::Dict
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
chathistory::Vector{Dict{String, Any}}
memory::Dict{String, Any}
context::agentcontext
llmFormatName::String
end
""" A sommelier agent.
# Arguments
- `context::agentcontext`
Application context containing shared functions for LLM, SQL, and vector database operations.
# Keyword Arguments
- `name::String`
Agent's name. Default: `"Assistant"`
- `id::String`
Agent's ID. Default: generated UUID string.
- `retailername::String`
Retailer name associated with the sommelier. Default: `"retailer_name"`
- `maxHistoryMsg::Integer`
Maximum history messages. Default: `20`
- `chathistory::Vector{Dict{String, String}}`
Chat history. Default: empty vector.
- `llmFormatName::String`
LLM format name. Default: `"granite3"`
# Return
- `sommelier`: An instantiated sommelier agent.
# Example
```julia
julia> using YiemAgent
julia> context = agentcontext(
text2textInstructLLM,
getTextEmbedding,
executeSQL,
similarSQLVectorDB,
insertSQLVectorDB,
similarSommelierDecision,
insertSommelierDecision
)
julia> agent = sommelier(context, name="WineExpert", id="123", retailername="MyWineShop")
```
"""
function sommelier(
context::agentcontext, # agent functions, db connect and other context
;
name::String= "Assistant",
id::String= string(uuid4()),
retailername::String= "not specified",
retailerid::String= "not specified",
maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{String, Any}} = Vector{Dict{String, Any}}(),
llmFormatName::String= "granite3"
)
tools = Dict( # update input format
"chatbox"=> Dict(
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
"output" => "" ,
),
"winestock"=> Dict(
"description" => "<winestock tool description>A handy tool for searching wine in your inventory that match the user preferences.</winestock tool description>",
"input" => """<input>Input is a JSON-formatted string that contains a detailed and precise search query.</input><input example>{\"wine type\": \"rose\", \"price\": \"max 35\", \"sweetness level\": \"sweet\", \"intensity level\": \"light bodied\", \"Tannin level\": \"low\", \"Acidity level\": \"low\"}</input example>""",
"output" => """<output>Output are wines that match the search query in JSON format.""",
),
)
""" Memory
Chat history use openai format as follow:
image1_path = "test/large_image.png" ---
image1_bytes = read(image1_path) | this part must be done
image1_base64_string = base64encode(image1_bytes) | in frontend
mime_type = "image/png" | not in agent code
data1_uri = "data:<mime_type>;base64,<image1_base64_string>" ---
chathistory= [
Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => "You are a helpful assistant"),
]
),
Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "<internal_context_for_assistant>
LLM context here...
</internal_context_for_assistant>
Do you know this wine? Just give me brief intro."
),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data1_uri)
),
]
),
]
shortmem = Dict(
"1"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
"2"=> Dict("plan"=> "...", "action_name"=> "...", "action_input"=> "...", "action_result"=> "..."),
...
)
"""
memory = Dict{String, Any}(
"shortmem"=> OrderedDict{String, Any}(),
"scratchpad"=> "",
"recap"=> OrderedDict{String, Any}(),
)
newAgent = sommelier(
name,
id,
retailername,
retailerid,
tools,
maxHistoryMsg,
chathistory,
memory,
context,
llmFormatName
)
systemmsg =
"""
# store_policy
- Generally speaking, the store inventory has some wines from France, the United States, Australia, Spain, and Italy, but you won't know exactly until you check your inventory.
- If you found wines in the store's database, they are in stock.
- You can only recommend wines that are currently in our inventory
- Before searching the database for wine, ensure you have at least the following information: 1) budget, 2) wine type, and 3) occasion. Additional details are always helpful. If the user is unsure, provide relevant information and gather insights to make reasonable inferences.
- Ask the user one question at a time.
- Once the user has selected their wine, if you haven't already, ask the user whether they need any further assistance. Do not offer any additional services.
- Only end the conversation when the user explicitly intends to do so. When ending, ensure a polite farewell and an invitation to return in the future.
- Spicy foods should be paired only with light red wines.
- We do not sell organic, sustainable, gluten-free, and sulfite-free wine. Inform the user imediately if they are looking for these types of wines. Do not sell our wines as such.
- Gift box, gift card, and custom messages are available. Inform the user to contact our sales team.
# store_guidelines
- Greeting the customer warmly by ask them how could you help. Do not ask any other questions during this greeting.
- Customer may provide images for you to look up.
- Encourage the customer to explore different options and try new things.
- If you are unable to locate the desired item in the database after 2 attempts, it may not be available in your inventory. In such cases, inform the user that the item is unavailable and suggest an alternative instead.
- Your store carries only wine.
- Vintage 0 means non-vintage.
- Start searching the database as broadly as possible within the given information boundary to maximize the chances of finding. Avoid unnecessary parameters unless specified by the user. Refine the search subsequently.
- User usually ask for something similar. This means you should use the search term based on the profile they like.
# situation
You are having conversation with a customer.
# your role
Your name is $(newAgent.name). You are a helpful sommelier for website-based $(newAgent.retailername)'s wine store.
# objective
- Establish a connection with the customer by talking to them politely and showing your enthusiasm for their wine preferences.
- Provide relevant information and guide them to select the best wines only from your store's inventory that align with their preferences.
# your responsibility includes
- According to the store's policy and guidelines, and make an informed decision about what available_actions you need to use to achieve the objective.
- Keep the conversation with the customer going smoothly
# your responsibility does NOT includes
- Requesting the user to place an order, make a purchase, or confirm the order. These are the job of our sales team at the store.
- Processing sales orders or engaging in any other sales-related activities. These are the job of our sales team at the store.
- Answering questions or offering additional services beyond those related to your store's wine recommendations such as discounts, quantity, rewards programs, promotions, delivery options, shipping, boxes, gift wrapping, packaging, personalized messages or something similar. These are the job of our sales team at the store.
# you should then respond to the user with interleaving plan, action_name, action_input in JSON format
1) "plan", Based on the current situation, state a complete action plan to complete the task and rationale. Be specific.
2) "action_name", (Typically corresponds to the execution of the first step in your plan) Can be one of the available_actions name
3) "action_input", The input to the action you are about to perform according to your plan.
After the action is executed you gets "action_result". It is the output from the action you selected.
# available actions
"CHAT_BOX", which you can use to talk with the user. The input is dialogue you want to chat with the user according to your plan.
"SEARCH_WINE_DATABASE", allows you to search information about wines you want in your inventory's database. The input is strictly supported search term including: retailer_name, wine price, winery, name, vintage, region, country, type of wine, grape varietal, tasting notes, occasion, food pairing, intensity, tannin, sweetness, and acidity.
Example query 1: "Dry, full-bodied red wine from Burgundy, France. Grape varietal could be Merlot or Syrah. price 100 to 1000 USD."
Example query 2: "Red or white wine, medium tannin, price under 700 USD"
Example query 3: "white wine from Tuscany, Italy or Bordeaux, France
"WINE_PRESENTATION_GUIDELINE", which you can use to check the store guidelines about how to present wines you have found to the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
"END_CONVER_GUIDELINE", which you can use to check the store guidelines about how to end the conversation with the user. The input is "nothing" keyword. The output is the guidelines that you can follow.
"""
system_msg = Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => systemmsg),
]
)
push!(newAgent.chathistory, system_msg)
return newAgent
end
mutable struct virtualcustomer <: agent
name::String # agent name
id::String # agent id
systemmsg::String # system message
tools::Dict
maxHistoryMsg::Integer # e.g. 21th and earlier messages will get summarized
chathistory::Vector{Dict{String, Any}}
memory::Dict{String, Any}
context # NamedTuple of functions
llmFormatName::String
end
function virtualcustomer(
context, # NamedTuple of functions
;
name::String= "Assistant",
id::String= string(uuid4()),
maxHistoryMsg::Integer= 20,
chathistory::Vector{Dict{String, String}} = Vector{Dict{String, String}}(),
llmFormatName::String= "granite3",
systemmsg::String=
"""
Your name: $name
Your sex: Female
Your role: You are a helpful assistant.
You should follow the following guidelines:
- Focus on the latest conversation.
- Your like to be short and concise.
Let's begin!
""",
)
tools = Dict( # update input format
"chatbox"=> Dict(
"description" => "<askbox tool description>Useful for when you need to ask the user for more context. Do not ask the user their own question.</askbox tool description>",
"input" => """<input>Input is a text in JSON format.</input><input example>{\"Q1\": \"How are you doing?\", \"Q2\": \"How may I help you?\"}</input example>""",
"output" => "" ,
),
)
""" Memory
Ref: Chat prompt format is openai
chathistory = [
Dict(
"role" => "system",
"content" => [
Dict("type" => "text", "text" => system_msg),
]
),
Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Do you know this wine? Just give me brief intro."),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data1_uri)
)
]
)
]
"""
memory = Dict{String, Any}(
"shortmem"=> OrderedDict{String, Any}(
),
"scratchpad"=> "",
"events"=> Vector{Dict{String, Any}}(),
"state"=> Dict{String, Any}(
),
"recap"=> OrderedDict{String, Any}(),
)
newAgent = virtualcustomer(
name,
id,
systemmsg,
tools,
maxHistoryMsg,
chathistory,
memory,
context,
llmFormatName
)
return newAgent
end
end # module type
+588
View File
@@ -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
-457
View File
@@ -1,457 +0,0 @@
module util
export clearhistory, addNewMessage, chatHistoryToText, eventdict, noises, createTimeline,
availableWineToText, createEventsLog, createChatLog, checkAgentResponse_JSON,
checkAgentResponse_text
using UUIDs, Dates, DataStructures, HTTP, JSON
using GeneralUtils
using ..type
# ---------------------------------------------- 100 --------------------------------------------- #
""" Clear agent chat history.
# Arguments
- `a::agent`
an agent
# Return
- nothing
# Example
```jldoctest
julia> using YiemAgent, MQTTClient, GeneralUtils
julia> client, connection = MakeConnection("test.mosquitto.org", 1883)
julia> connect(client, connection)
julia> msgMeta = GeneralUtils.generate_msgMeta("testtopic")
julia> agentConfig = Dict(
"receiveprompt"=>Dict(
"mqtttopic"=> "testtopic/receive",
),
"receiveinternal"=>Dict(
"mqtttopic"=> "testtopic/internal",
),
"text2text"=>Dict(
"mqtttopic"=> "testtopic/text2text",
),
)
julia> a = YiemAgent.sommelier(
client,
msgMeta,
agentConfig,
)
julia> YiemAgent.addNewMessage(a, "user", "hello")
julia> YiemAgent.clearhistory(a)
```
# TODO
- [PENDING] clear memory
# Signature
"""
function clearhistory(a::T) where {T<:agent}
empty!(a.chathistory)
empty!(a.memory["shortmem"])
empty!(a.memory["events"])
a.memory["chatbox"] = ""
end
""" Add new message to agent.
messages => Dict(
"role" => "user",
"content" => [
Dict("type" => "text", "text" => "Describe this image for me"),
Dict(
"type" => "image_url",
"image_url" => Dict("url" => data_uri)
)
]
)
Arguments\n
-----
a::agent
an agent
role::String
message sender role i.e. system, user or assistant
text::String
message text
Return\n
-----
nothing
Example\n
-----
```jldoctest
```
Signature\n
-----
"""
function addNewMessage(a::T1, name::String, userinput::T2;
maximumMsg::Integer=30) where {T1<:agent, T2<:AbstractDict}
# if name ∉ ["system", "user", "assistant"] # guard against typo
# error("name is not in agent.availableRole $(@__LINE__)")
# end
#TODO summarize the oldest 10 message
if length(a.chathistory) > maximumMsg
summarize(a.chathistory)
else
# userinput["timestamp"] = Dates.now()
push!(a.chathistory, userinput)
end
end
""" Converts a vector of dictionaries to a formatted string.
This function takes in a vector of dictionaries and outputs a single string where each dictionary's keys are prefixed by their values.
# Arguments
- `vecd::Vector`
A vector of dictionaries containing chat messages
- `withkey::Bool`
Whether to include the name as a prefix in the output text. Default is true
- `range::Union{Nothing,UnitRange,Int}`
Optional range of messages to include. If nothing, includes all messages
# Returns
A formatted string where each line contains either:
- If withkey=true: "name> message\n"
- If withkey=false: "message\n"
# Example
julia> using Revise
julia> using GeneralUtils
julia> vecd = [Dict("name" => "John", "text" => "Hello"), Dict("name" => "Jane", "text" => "Goodbye")]
julia> GeneralUtils.vectorOfDictToText(vecd, withkey=true)
"John> Hello\nJane> Goodbye\n"
```
"""
function chatHistoryToText(vecd::Vector; withkey=true, range=nothing)::String
# Initialize an empty string to hold the final text
text = ""
# Get the elements within the specified range, or all elements if no range provided
elements = isnothing(range) ? vecd : vecd[range]
# Determine whether to include the key in the output text or not
if withkey
# Loop through each dictionary in the input vector
for d in elements
# Extract the 'name' and 'text' keys from the dictionary
name = titlecase(d[:name])
_text = d[:text]
# Append the formatted string to the text variable
text *= "$name> $_text \n"
end
else
# Loop through each dictionary in the input vector
for d in elements
# Iterate over all key-value pairs in the dictionary
for (k, v) in d
# Append the formatted string to the text variable
text *= "$v \n"
end
end
end
# Return the final text
return text
end
function availableWineToText(vecd::Vector)::String
# Initialize an empty string to hold the final text
rowtext = ""
# Loop through each dictionary in the input vector
for (i, d) in enumerate(vecd)
# Iterate over all key-value pairs in the dictionary
temp = []
for (k, v) in d
# Append the formatted string to the text variable
t = "$k:$v"
push!(temp, t)
end
_rowtext = join(temp, ',')
rowtext *= "$i) $_rowtext "
end
return rowtext
end
""" Create a dictionary representing an event with optional details.
# Arguments
- `event_description::Union{String, Nothing}`
A description of the event
- `timestamp::Union{DateTime, Nothing}`
The time when the event occurred
- `subject::Union{String, Nothing}`
The subject or entity associated with the event
- `thought::Union{AbstractDict, Nothing}`
Any associated thoughts or metadata
- `action_name::Union{String, Nothing}`
The name of the action performed (e.g., "CHAT", "CHECKINVENTORY")
- `action_input::Union{String, Nothing}`
Input or parameters for the action
- `location::Union{String, Nothing}`
Where the event took place
- `equipment_used::Union{String, Nothing}`
Equipment involved in the event
- `material_used::Union{String, Nothing}`
Materials used during the event
- `outcome::Union{String, Nothing}`
The result or consequence of the event after action execution
- `note::Union{String, Nothing}`
Additional notes or comments
# Returns
A dictionary with event details as symbol-keyed key-value pairs
"""
function eventdict(;
event_description::Union{String, Nothing}=nothing,
timestamp::Union{DateTime, Nothing}=nothing,
subject::Union{String, Nothing}=nothing,
thought::Union{AbstractDict, Nothing}=nothing,
action_name::Union{String, Nothing}=nothing, # "CHAT", "CHECKINVENTORY", "PRESENT_WINE_GUIDELINE", etc
action_input::Union{String, Nothing}=nothing,
location::Union{String, Nothing}=nothing,
equipment_used::Union{String, Nothing}=nothing,
material_used::Union{String, Nothing}=nothing,
observation::Union{String, Nothing}=nothing,
note::Union{String, Nothing}=nothing,
)
d = Dict{String, Any}(
"event_description"=> event_description,
"timestamp"=> timestamp,
"subject"=> subject,
"thought"=> thought,
"action_name"=> action_name,
"action_input"=> action_input,
"location"=> location,
"equipment_used"=> equipment_used,
"material_used"=> material_used,
"observation"=> observation,
"note"=> note,
)
return d
end
""" Create a formatted timeline string from a sequence of events.
# Arguments
- `events::T1`
Vector of event dictionaries containing subject, action_input and optional outcome fields
Each event dictionary should have the following keys:
- :subject - The subject or entity performing the action
- :action_input - The action or input performed by the subject
- :observation - (Optional) The result or outcome of the action
# Returns
- `timeline::String`
A formatted string representing the events with their subjects, actions, and optional outcomes
Format: "{index}) {subject}> {action_input} {outcome}\n" for each event
# Example
events = [
Dict("subject" => "User", "action_input" => "Hello", "observation" => nothing),
Dict("subject" => "Assistant", "action_input" => "Hi there!", "observation" => "with a smile")
]
timeline = createTimeline(events)
# 1) User> Hello
# 2) Assistant> Hi there! with a smile
"""
function createTimeline(events::T1; eventindex::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty timeline string
timeline = ""
# Determine which indices to use - either provided range or full length
ind =
if eventindex !== nothing
[eventindex...]
else
1:length(events)
end
# Iterate through events and format each one
for i in ind
event = events[i]
# If no outcome exists, format without outcome
# if event["action_name"] == "CHAT_BOX"
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\n"
# elseif event["action_name"] == "CHECKINVENTORY" && event["observation"] === nothing
# timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: Not done yet.\n"
if event["action_name"] == "SEARCH_WINE_DATABASE"
timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"]), observation: $(event["observation"])\\n"
else
timeline *= "Event_$i $(event["subject"])> action_name: $(event["action_name"]), action_input: $(event["action_input"])\\n"
end
end
# Return formatted timeline string
return timeline
end
function createEventsLog(events::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{String, String}[]
# Determine which indices to use - either provided range or full length
ind =
if index !== nothing
[index...]
else
1:length(events)
end
# Iterate through events and format each one
for i in ind
event = events[i]
# If no outcome exists, format without outcome
if event["observation"] === nothing
subject = event["subject"]
action_name = event["action_name"]
action_input = event["action_input"]
str = "action_name: $action_name, action_input: $action_input"
d = Dict{String, String}("name"=>subject, "text"=>str)
push!(log, d)
else
subject = event["subject"]
action_name = event["action_name"]
action_input = event["action_input"]
observation = event["observation"]
str = "action_name: $action_name, action_input: $action_input, observation: $observation"
d = Dict{String, String}("name"=>subject, "text"=>str)
push!(log, d)
end
end
return log
end
function createChatLog(chatdict::T1; index::Union{UnitRange, Nothing}=nothing
) where {T1<:AbstractVector}
# Initialize empty log array
log = Dict{String, String}[]
# Determine which indices to use - either provided range or full length
ind =
if index !== nothing
[index...]
else
1:length(chatdict)
end
# Iterate through events and format each one
for i in ind
event = chatdict[i]
subject = event["name"]
text = event["text"]
d = Dict{String, String}("name"=>subject, "text"=>text)
push!(log, d)
end
return log
end
function checkAgentResponse_text(response::String, requiredHeader::T
)::Tuple where {T<:Array{String}}
detected_kw = GeneralUtils.detectKeywordVariation(requiredHeader, response)
missingkeys = [k for (k, v) in detected_kw if v === nothing]
ispass = false
errormsg = nothing
if !isempty(missingkeys)
errormsg = "$missingkeys are missing from your previous response"
ispass = false
elseif sum([length(i) for i in values(detected_kw)]) > length(requiredHeader)
errormsg = "Your previous attempt has duplicated points according to the required response format"
ispass = false
else
ispass = true
end
return (ispass, errormsg)
end
end # module util