Files
YiemAgent/learning/07-AGENTHARNESS.md
T
2026-07-31 11:41:53 +07:00

652 lines
22 KiB
Markdown

# AgentCore.jl - AgentHarness Design Reference
## Status
> **Note**: The AgentHarness module (`src/agent_harness.jl`) is **not yet implemented**. This document
> describes the intended design based on types defined in `src/harness_types.jl`. The types, events,
> and interfaces below are defined but the harness that connects them is a planned feature.
>
> Several modules referenced in `src/AgentCore.jl` are also not yet implemented:
> `compaction/compaction.jl`, `compaction/utils.jl`, `compaction/branch_summarization.jl`,
> `utils/truncate.jl`, `utils/shell_output.jl`, `proxy.jl`.
>
> Type placeholders not yet defined: `AgentLoopConfig`, `Promise`, `AbortSignal`, `EventStream`,
> `Context`. The `SessionRepo` methods in `harness_types.jl` return `Promise()` stubs.
## AgentHarness Architecture (Planned)
```
AgentHarness = Agent + Session + Resources + Hooks
AgentHarness (to be implemented in src/agent_harness.jl)
├── Manages Agent instances
├── Provides Session persistence via SessionRepo
├── Manages resources (skills, prompt templates)
├── Handles extension hooks (BeforeAgentStart, BeforeProviderPayload, etc.)
└── Coordinates tool execution with AgentHarnessToolContextSource
AgentHarnessOptions (src/harness_types.jl:1067)
├── session::Session
├── models::Any
├── tools::Union{Vector{TTool}, Nothing}
├── resources::Union{AgentHarnessResources, Nothing}
├── system_prompt::Union{AgentHarnessSystemPrompt, Nothing}
├── stream_options::Union{AgentHarnessStreamOptions, Nothing}
├── retry::Union{Any, Nothing}
├── model::Model
├── thinking_level::Union{ThinkingLevel, Nothing}
├── active_tool_names::Union{Vector{String}, Nothing}
├── steering_mode::Union{QueueMode, Nothing}
├── follow_up_mode::Union{QueueMode, Nothing}
└── tool_context::Union{AgentHarnessToolContextSource, Nothing}
```
## Event Type Hierarchy (Actual)
The harness event types are defined as `mutable struct` in `harness_types.jl`.
They are NOT subtypes of `AgentHarnessEvent` or `AgentHarnessOwnEvent` - those
abstract types exist but nothing inherits from them.
```
AgentEvent (abstract, types.jl:196)
├── AgentStartEvent (types.jl:198)
├── AgentEndEvent (types.jl:199)
├── TurnStartEvent (types.jl:202)
├── TurnEndEvent (types.jl:203)
├── MessageStartEvent (types.jl:207)
├── MessageUpdateEvent (types.jl:210)
├── MessageEndEvent (types.jl:214)
├── ToolExecutionStartEvent (types.jl:217)
├── ToolExecutionUpdateEvent (types.jl:222)
└── ToolExecutionEndEvent (types.jl:228)
AgentHarnessOwnEvent (abstract, harness_types.jl:850)
└── (nothing inherits from this)
AgentHarnessEvent (abstract, harness_types.jl:856)
└── (nothing inherits from this)
Harness event structs (harness_types.jl) - mutable structs, not subtypes:
├── BeforeAgentStartEvent (line 653)
├── ContextEvent (line 665)
├── BeforeProviderRequestEvent (line 674)
├── BeforeProviderPayloadEvent (line 685)
├── AfterProviderResponseEvent (line 695)
├── ToolCallEvent (line 705)
├── ToolResultEvent (line 716)
├── SessionBeforeCompactEvent (line 731)
├── SessionCompactEvent (line 743)
├── SessionBeforeTreeEvent (line 753)
├── SessionTreeEvent (line 763)
├── RetryScheduledEvent (line 775)
├── RetryAttemptStartEvent (line 788)
├── RetryFinishedEvent (line 797)
├── ModelUpdateEvent (line 806)
├── ThinkingLevelUpdateEvent (line 817)
├── ToolsUpdateEvent (line 827)
└── ResourcesUpdateEvent (line 840)
```
## Types (from harness_types.jl)
### AgentHarnessOptions (line 1067)
```julia
mutable struct AgentHarnessOptions{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool}
session::Session
models::Any
tools::Union{Vector{TTool}, Nothing}
resources::Union{AgentHarnessResources{TSkill, TPromptTemplate}, Nothing}
system_prompt::Union{AgentHarnessSystemPrompt{TC, TSkill, TPromptTemplate, TTool}, Nothing}
stream_options::Union{AgentHarnessStreamOptions, Nothing}
retry::Union{Any, Nothing}
model::Model
thinking_level::Union{ThinkingLevel, Nothing}
active_tool_names::Union{Vector{String}, Nothing}
steering_mode::Union{QueueMode, Nothing}
follow_up_mode::Union{QueueMode, Nothing}
tool_context::Union{AgentHarnessToolContextSource{TC}, Nothing}
end
```
**Purpose**: Configure AgentHarness with all necessary options (defined but harness not implemented).
### AgentHarnessResources (line 82)
```julia
mutable struct AgentHarnessResources{TSkill<:Skill, TPromptTemplate<:PromptTemplate}
promptTemplates::Union{Vector{TPromptTemplate}, Nothing}
skills::Union{Vector{TSkill}, Nothing}
end
```
### Skill (line 68)
```julia
mutable struct Skill
name::String
description::String
content::String
filePath::String
disableModelInvocation::Bool
end
```
**Loading**: `loadSkills(env, dir)` is defined in `skills.jl` but **parsing is stubbed** - currently returns `nothing, diagnostics`. The frontmatter parsing code (lines 266-300 of skills.jl) is commented out as TODO.
**Skill format**:
```markdown
<!-- SKILL.md -->
{
"name": "File Operations",
"description": "Handle file system operations",
"disable-model-invocation": false
}
---
# File Operations Skill
This skill provides instructions for working with files...
```
### PromptTemplate (line 76)
```julia
mutable struct PromptTemplate
name::String
description::Union{String, Nothing}
content::String
end
```
**Loading**: `loadPromptTemplates(env, paths)` is defined in `prompt_templates.jl` but **parsing is stubbed** - currently returns `nothing, diagnostics`. Frontmatter parsing is commented out as TODO (lines 188-215).
**Format**:
```markdown
<!-- template.md -->
{
"description": "Generate commit message"
}
---
Generate a git commit message for:
$1
$ARGUMENTS
```
### AgentHarnessStreamOptions (line 109)
```julia
mutable struct AgentHarnessStreamOptions
transport::Union{String, Nothing}
timeout_ms::Union{Int64, Nothing}
max_retries::Union{Int64, Nothing}
max_retry_delay_ms::Union{Int64, Nothing}
headers::Union{Dict{String, String}, Nothing}
metadata::Union{Dict{String, Any}, Nothing}
cache_retention::Union{String, Nothing}
end
```
### AgentHarnessStreamOptionsPatch (line 119)
```julia
mutable struct AgentHarnessStreamOptionsPatch
transport::Union{String, Nothing}
timeout_ms::Union{Int64, Nothing}
max_retries::Union{Int64, Nothing}
max_retry_delay_ms::Union{Int64, Nothing}
cache_retention::Union{String, Nothing}
headers::Union{Dict{String, String}, Nothing}
metadata::Union{Dict{String, Any}, Nothing}
end
```
### AgentHarnessTool (line 91)
```julia
mutable struct AgentHarnessTool{TContext, TParameters, TDetails}
name::String
label::String
description::String
parameters::TParameters
execute::Function
prepareArguments::Union{Function, Nothing}
executionMode::Union{ToolExecutionMode, Nothing}
end
```
### AgentHarnessToolContextSource (line 101)
```julia
mutable struct AgentHarnessToolContextSource{TContext}
context::Union{TContext, Function}
end
```
### AgentHarnessSystemPrompt (line 1059)
```julia
mutable struct AgentHarnessSystemPrompt{TC<:Any, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool}
value::Union{String, Function}
end
```
## SessionRepo Interface (stubs in harness_types.jl:564-588)
```julia
abstract type SessionRepo<
TMetadata<:SessionMetadata,
TCreateOptions,
TListOptions
> end
function create(repo::SessionRepo, options::TCreateOptions)::Promise{Session}
return Promise() # STUB - Promise type not defined
end
function open(repo::SessionRepo, metadata::TMetadata)::Promise{Session}
return Promise() # STUB
end
function list(repo::SessionRepo, options::TListOptions)::Promise{Vector{TMetadata}}
return Promise() # STUB
end
function delete(repo::SessionRepo, metadata::TMetadata)::Promise{Nothing}
return Promise() # STUB
end
function fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Promise{Session}
return Promise() # STUB
end
```
> **Note**: These methods are stubs in `harness_types.jl`. `Promise` is not defined anywhere.
### JsonlSessionRepo (src/session/jsonl_repo.jl)
```julia
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
```
> **Note**: Constructor uses **keyword arguments** (`sessions_root=`, `fs=`), NOT positional.
### JsonlSessionStorage (src/session/jsonl_storage.jl)
- `file_path::String`
- `metadata::T` (SessionMetadata)
- `entries::Vector{SessionTreeEntry}`
- `by_id::Dict{String, SessionTreeEntry}`
- `labels_by_id::Dict{String, String}`
- `current_leaf_id::Union{String, Nothing}`
Methods: `getMetadata`, `getLeafId`, `setLeafId`, `createEntryId`, `appendEntry`, `getEntry`, `findEntries`, `getLabel`, `getSessionName`, `getSessionStats`, `getPathToRootOrCompaction`, `getEntries`.
## Agent (src/agent.jl)
```julia
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
end
```
Key methods:
- `subscribe(agent, listener)` - subscribe to events, returns unsubscribe function
- `get_state(agent)` - get current AgentState
- `steer(agent, message)` - queue message for injection after current turn
- `followUp(agent, message)` - queue message to run after agent would stop
- `prompt(agent, input, images)` - start a new prompt (input can be String, AgentMessage, or Vector{AgentMessage})
- `continue!(agent)` - continue from current transcript
- `waitForIdle(agent)` - resolve when current run finishes
- `abort(agent)` - abort current run (partially implemented)
- `reset!(agent)` - clear all state
- `clearSteeringQueue(agent)` / `clearFollowUpQueue(agent)` / `clearAllQueues(agent)`
- `hasQueuedMessages(agent)` - check for pending messages
- `createContextSnapshot(agent)` - create AgentContext snapshot
- `createLoopConfig(agent, options)` - create AgentLoopConfig
> **Note**: `AgentLoopConfig` type is **not defined** in any visible file. It is referenced in `agent.jl:368` and `agent_loop.jl`.
## AgentLoop (src/agent_loop.jl)
Key functions:
- `agentLoop(prompts, context, config, signal, stream_fn)` - main loop, returns EventStream
- `agentLoopContinue(context, config, signal, stream_fn)` - continue from existing context
- `runAgentLoop(...)` - internal run, emits events via `emit::AgentEventSink`
- `runAgentLoopContinue(...)` - internal continue run
- `runLoop(...)` - shared main loop logic
- `streamAssistantResponse(...)` - stream LLM response with event emission
- `executeToolCalls(...)` - execute tool calls (sequential or parallel)
- `executeToolCallsSequential(...)` - sequential execution
- `executeToolCallsParallel(...)` - parallel execution via Threads.@spawn
The loop flow:
1. `AgentStartEvent` emitted
2. `TurnStartEvent` emitted (first turn only from agentLoop, not from runLoop)
3. Steering messages drained and emitted as `MessageStartEvent`/`MessageEndEvent`
4. `streamAssistantResponse` called - transforms context, converts to LLM messages, calls stream_fn
5. For each tool call in response: execute sequentially or in parallel
6. `TurnEndEvent` emitted with message and tool results
7. `prepare_next_turn` hook (if configured) called
8. If `should_stop_after_turn` returns true or no pending messages, `AgentEndEvent` emitted
9. Follow-up messages drained and loop repeats
### AgentLoopConfig fields (referenced, not defined)
Created in `agent.jl:368-401`:
```
model, reasoning (thinking_level), session_id, on_payload, on_response,
transport, thinking_budgets, max_retry_delay_ms, tool_execution,
before_tool_call, after_tool_call, prepare_next_turn, convert_to_llm,
transform_context, get_api_key, get_steering_messages, get_follow_up_messages
```
## Hook System (Planned - Harness Not Implemented)
The following hook types are defined as event/result structs in `harness_types.jl`
but **no harness implementation exists to trigger or handle them**. These are
intended to be used by the future AgentHarness module.
### BeforeAgentStartEvent (line 653)
```julia
mutable struct BeforeAgentStartEvent{TSkill, TPromptTemplate}
type::String
prompt::String
images::Union{Vector{ImageContent}, Nothing}
system_prompt::String
resources::AgentHarnessResources{TSkill, TPromptTemplate}
end
```
**Result**: `BeforeAgentStartResult` (line 862) - `messages::Union{Vector{AgentMessage}, Nothing}`, `system_prompt::Union{String, Nothing}`
### ContextEvent (line 665)
```julia
mutable struct ContextEvent
type::String
messages::Vector{AgentMessage}
end
```
**Result**: `ContextResult` (line 871) - `messages::Vector{AgentMessage}`
### BeforeProviderRequestEvent (line 674)
```julia
mutable struct BeforeProviderRequestEvent
type::String
model::Model
session_id::String
stream_options::AgentHarnessStreamOptions
end
```
**Result**: `BeforeProviderRequestResult` (line 879) - `stream_options::Union{AgentHarnessStreamOptionsPatch, Nothing}`
### BeforeProviderPayloadEvent (line 685)
```julia
mutable struct BeforeProviderPayloadEvent
type::String
model::Model
payload::Any
end
```
**Result**: `BeforeProviderPayloadResult` (line 887) - `payload::Any`
### AfterProviderResponseEvent (line 695)
```julia
mutable struct AfterProviderResponseEvent
type::String
status::Int64
headers::Dict{String, String}
end
```
### ToolCallEvent (line 705)
```julia
mutable struct ToolCallEvent
type::String
tool_call_id::String
tool_name::String
input::Dict{String, Any}
end
```
**Result**: `ToolCallResult` (line 895) - `block::Union{Bool, Nothing}`, `reason::Union{String, Nothing}`
### ToolResultEvent (line 716)
```julia
mutable struct ToolResultEvent
type::String
tool_call_id::String
tool_name::String
input::Dict{String, Any}
content::Vector{MessageContent}
details::Any
is_error::Bool
usage::Union{Usage, Nothing}
end
```
**Result**: `ToolResultPatch` (line 904) - `content`, `details`, `is_error`, `usage`, `terminate` (all Union{...})
### SessionBeforeCompactEvent (line 731)
```julia
mutable struct SessionBeforeCompactEvent
type::String
preparation::Any
branch_entries::Vector{SessionTreeEntry}
custom_instructions::Union{String, Nothing}
signal::Any
end
```
**Result**: `SessionBeforeCompactResult` (line 916) - `cancel::Union{Bool, Nothing}`, `compaction::Union{CompactResult, Nothing}`
### SessionBeforeTreeEvent (line 753)
```julia
mutable struct SessionBeforeTreeEvent
type::String
preparation::Any
signal::Any
end
```
**Result**: `SessionBeforeTreeResult` (line 925) - `cancel`, `summary`, `custom_instructions`, `replace_instructions`, `label`
### SessionCompactEvent (line 743)
```julia
mutable struct SessionCompactEvent
type::String
compaction_entry::CompactionEntry
from_hook::Bool
end
```
### SessionTreeEvent (line 763)
```julia
mutable struct SessionTreeEvent
type::String
new_leaf_id::Union{String, Nothing}
old_leaf_id::Union{String, Nothing}
summary_entry::Union{BranchSummaryEntry, Nothing}
from_hook::Union{Bool, Nothing}
end
```
## Session (src/session/session.jl)
```julia
mutable struct Session{T<:SessionMetadata}
storage::SessionStorage{T}
context_build_options::SessionContextBuildOptions
end
```
Key methods:
- `getMetadata(session)` / `getStorage(session)` / `getLeafId(session)` / `getEntry(session, id)`
- `getEntries(session, options)` / `getBranch(session, from_id)`
- `buildContextEntries(session, options)` / `buildContext(session, options)`
- `getLabel(session, id)` / `getSessionStats(session)` / `getSessionName(session)`
- `appendMessage(session, message)` → entry_id
- `appendThinkingLevelChange(session, level)` → entry_id
- `appendModelChange(session, provider, model_id)` → entry_id
- `appendActiveToolsChange(session, active_tool_names)` → entry_id
- `appendCompaction(session, summary, first_kept_entry_id, tokens_before, ...)` → entry_id
- `appendCustomEntry(session, custom_type, data)` → entry_id
- `appendCustomMessageEntry(session, custom_type, content, display, details)` → entry_id
- `appendLabel(session, target_id, label)` → entry_id
- `appendSessionName(session, name)` → entry_id
- `moveTo(session, entry_id, summary)` → new_leaf_id or nothing (line 392)
## Session Tree Entries (types.jl and harness_types.jl)
```julia
abstract type SessionTreeEntry end
struct MessageEntry <: SessionTreeEntry
base::SessionTreeEntryBase # or direct fields in harness_types.jl
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
```
## Resource Loading (stubs)
### loadSkills (skills.jl:61)
```julia
skills, diagnostics = loadSkills(env, "/path/to/skills")
```
> **Note**: Parsing is **stubbed** (line 302 returns `nothing, diagnostics`). The frontmatter parsing code is commented out (lines 266-300). `formatSkillInvocation(skill, additional_instructions)` is implemented.
### loadPromptTemplates (prompt_templates.jl:43)
```julia
templates, diagnostics = loadPromptTemplates(env, "/path/to/templates")
```
> **Note**: Parsing is **stubbed** (line 217 returns `nothing, diagnostics`). The frontmatter parsing code is commented out (lines 188-215). `formatPromptTemplateInvocation(template, args)` and `parseCommandArgs(args_string)` and `substituteArgs(content, args)` are implemented.
## Missing Types / Modules
The following types are referenced in the code but **not defined**:
- `AgentLoopConfig` - referenced in `agent.jl:368`, `agent_loop.jl`
- `Promise` - referenced in `harness_types.jl`
- `AbortSignal` - referenced in `agent_loop.jl`
- `EventStream` - referenced in `agent_loop.jl:158`
- `Context` - referenced in `agent_loop.jl:376`
- `AgentToolResultMutable` - referenced in `agent_loop.jl`
- `FinalizedToolCallOutcome`, `PreparedToolCall`, `ImmediateToolCallOutcome`, `ExecutedToolCallOutcome` - defined in `agent_loop.jl:639-661` (these exist)
The following modules are referenced in `AgentCore.jl` but **files don't exist**:
- `compaction/compaction.jl`
- `compaction/utils.jl`
- `compaction/branch_summarization.jl`
- `utils/truncate.jl`
- `utils/shell_output.jl`
- `proxy.jl`
## AgentCore Exports (from AgentCore.jl:61-147)
The module exports: AgentMessage, AgentTool, AgentContext, AgentEvent, ThinkingLevel, ToolExecutionMode, QueueMode, AgentState, Agent, AgentOptions, AgentLoopConfig, agentLoop, agentLoopContinue, runAgentLoop, runAgentLoopContinue, AgentHarness, AgentHarnessOptions, AgentHarnessEvent, AgentHarnessResources, AgentHarnessSystemPrompt, Session, SessionStorage, SessionRepo, JsonlSessionStorage, JsonlSessionRepo, InMemorySessionStorage, InMemorySessionRepo, createBashTool, createReadTool, createWriteTool, createEditTool, ExecutionEnv, compact, prepareCompaction, DEFAULT_COMPACTION_SETTINGS, generateSummary, generateBranchSummary, truncateHead, truncateTail, formatSize, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES, convertToLlm, bashExecutionToText, formatSkillsForSystemPrompt, loadSkills, formatSkillInvocation, loadPromptTemplates, formatPromptTemplateInvocation, parseCommandArgs, substituteArgs, streamProxy, ProxyStreamOptions, setDefaultStreamFn, getDefaultStreamFn, uuidv7, create_timestamp.
## Best Practices
1. **Agent hooks** (planned): Use `beforeAgentStart` for initialization, `beforeProviderPayload` for custom metadata, `toolCall` for blocking dangerous operations
2. **Skills**: Organize by domain (file operations, database queries, HTTP requests, git operations)
3. **Templates**: Use for common patterns (commit messages, code review, testing prompts)
4. **Sessions**: Compact periodically, use branches for exploration, clean up old sessions
5. **Monitoring**: Track token counts, watch API costs, optimize tool execution
6. **Tool execution**: Choose between `EXECUTION_SEQUENTIAL` and `EXECUTION_PARALLEL` based on tool dependencies