Class: Esp::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/esp/agent.rb

Overview

A hand-rolled tool-use loop. The agent authors mods by calling the same curated tool surface MCP exposes (Esp::McpServer::TOOLS), each tool dispatched to Esp::Operations.public_send(op, …) — so the in-house agent and external MCP clients share one tool definition.

The LLM is reached through an injected provider (see Esp::Providers), so the loop is provider-agnostic and fully testable with a stub and no live key. When none is given, the default provider is built from the registry (Esp::Providers.default_id). See .claude/roadmap/19-agent-workspace.md.

Transcript shape (neutral, owned by Agent; providers translate it to their native wire format):

{ role: :user,      text: String }
{ role: :assistant, text: String, tool_calls: [{id:, name:, input:}],
                    raw: <provider-native message, opaque to Agent> }
{ role: :tool,      results: [{id:, content: String, is_error: bool}] }

Defined Under Namespace

Classes: Result

Constant Summary collapse

MAX_TURNS =
20
SYSTEM =
<<~PROMPT.freeze
  You are the authoring agent inside ESPresso, the GUI for the `esp`
  Morrowind modding toolchain. You build and edit mods by calling the
  provided tools, which operate on the user's mod project as diffable
  source. Read current state (records_read, refs_find) before writing.
  Make the smallest change that satisfies the request, then build and
  lint to confirm. Keep explanations brief — the human reviews the diff.
PROMPT

Instance Method Summary collapse

Constructor Details

#initialize(provider: nil, system: SYSTEM, max_turns: MAX_TURNS) ⇒ Agent

Returns a new instance of Agent.



34
35
36
37
38
# File 'lib/esp/agent.rb', line 34

def initialize(provider: nil, system: SYSTEM, max_turns: MAX_TURNS)
  @provider = provider || Esp::Providers.build(Esp::Providers.default_id)
  @system = system
  @max_turns = max_turns
end

Instance Method Details

#run(prompt, &on_event) ⇒ Object

Run the loop to completion for one user prompt. Optionally yields events as they happen ([:text, str] / [:tool_use, name, input] /

:tool_result, name, payload

/ [:tool_error, name, error]) for a

streaming UI to render.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/esp/agent.rb', line 44

def run(prompt, &on_event)
  messages = [{ role: :user, text: prompt }]
  @max_turns.times do
    completion = @provider.complete(system: @system, tools: tool_definitions, messages: messages)
    tool_calls = Array(completion.tool_calls)
    messages << { role: :assistant, text: completion.text, tool_calls: tool_calls, raw: completion.raw }
    on_event&.call([:text, completion.text]) unless completion.text.to_s.empty?

    break if tool_calls.empty?

    messages << { role: :tool, results: tool_calls.map { |tc| run_tool(tc, &on_event) } }
  end
  Result.new(text: final_text(messages), messages: messages)
end