Class: Ask::CodingHarness::AgentRunner
- Inherits:
-
Object
- Object
- Ask::CodingHarness::AgentRunner
- Defined in:
- lib/ask/coding_harness/agent_runner.rb
Overview
Runs coding-agent turns for conversations: maps each conversation to an adapter session, streams translated events, and persists assistant messages back into the store.
The default runtime is ask-agent (via the ask_agent adapter). Other coding agents (Codex, Claude Code, ACP-based agents) plug in through ask-coding-providers by setting ACH_ADAPTER/CODING_PROVIDER — the runner only needs the adapter's duck-typed surface: create_session, send_and_stream, and (when supported) approve_action, reject_action, approve_all, approve_plan, reject_plan, abort.
Constant Summary collapse
- TOOL_CLASSES =
Tool name → ask-tools-shell class, for the ask_agent runtime.
{ "bash" => Ask::Tools::Bash, "read" => Ask::Tools::Read, "write" => Ask::Tools::Write, "edit" => Ask::Tools::Edit, "glob" => Ask::Tools::Glob, "grep" => Ask::Tools::Grep, "code" => Ask::Tools::Code, "apply_patch" => Ask::Tools::ApplyPatch, "repl" => Ask::Tools::Repl }.freeze
Instance Method Summary collapse
- #abort(conversation_id) ⇒ Object
-
#adapter ⇒ Object
The adapter (created lazily on first use).
-
#agent_definitions(workspace) ⇒ Object
Declarative agents in a workspace (ask-agent convention): [instructions:] discovered without loading the definitions.
-
#agent_instructions(workspace, agent) ⇒ Object
The instructions.md content of a named agent, loaded through the ask-agent registry (which also validates the definition).
-
#approve(conversation_id, action_id) ⇒ Object
── Approval / plan controls (no-ops when the adapter lacks them) ──.
- #approve_all(conversation_id) ⇒ Object
- #approve_plan(conversation_id) ⇒ Object
-
#initialize(config:, store:, translator: EventTranslator.new, tool_classes: TOOL_CLASSES) ⇒ AgentRunner
constructor
A new instance of AgentRunner.
- #pending_approvals(conversation_id) ⇒ Object
- #reject(conversation_id, action_id) ⇒ Object
- #reject_plan(conversation_id) ⇒ Object
- #running?(conversation_id) ⇒ Boolean
-
#start_turn(conversation, prompt, model: nil) {|Hash| ... } ⇒ Thread
Run a turn for a conversation, streaming harness events to the block.
- #stop ⇒ Object
-
#system_prompt_for(workspace, agent = nil) ⇒ Object
The system prompt for a workspace.
Constructor Details
#initialize(config:, store:, translator: EventTranslator.new, tool_classes: TOOL_CLASSES) ⇒ AgentRunner
Returns a new instance of AgentRunner.
36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 36 def initialize(config:, store:, translator: EventTranslator.new, tool_classes: TOOL_CLASSES) @config = config @store = store @translator = translator @tool_classes = tool_classes @adapter = nil @mutex = Mutex.new @adapter_mutex = Mutex.new @turn_mutex = Mutex.new @sessions = {} @turns = {} end |
Instance Method Details
#abort(conversation_id) ⇒ Object
104 105 106 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 104 def abort(conversation_id) send_adapter(conversation_id, :abort) end |
#adapter ⇒ Object
The adapter (created lazily on first use). Uses its own mutex so callers holding @mutex (session_for, stop) can still build it.
51 52 53 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 51 def adapter @adapter_mutex.synchronize { @adapter ||= build_adapter } end |
#agent_definitions(workspace) ⇒ Object
Declarative agents in a workspace (ask-agent convention): [instructions:] discovered without loading the definitions. Safe for listing in the UI.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 230 def agent_definitions(workspace) Dir.chdir(workspace) do Ask::Agent.default_agent_paths.filter_map do |base| next unless File.directory?(base) Dir["#{base}/*/agent.rb"].sort.filter_map do |file| dir = File.dirname(file) name = File.basename(dir) next if name == "shared" instructions = File.file?(File.join(dir, "instructions.md")) ? File.read(File.join(dir, "instructions.md")) : nil { "name" => name, "instructions" => instructions } end end.flatten end end |
#agent_instructions(workspace, agent) ⇒ Object
The instructions.md content of a named agent, loaded through the ask-agent registry (which also validates the definition).
247 248 249 250 251 252 253 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 247 def agent_instructions(workspace, agent) Dir.chdir(workspace) do Ask::Agent.rediscover! klass = Ask::Agent.definitions[agent.to_s]&.first klass&.instructions_content end end |
#approve(conversation_id, action_id) ⇒ Object
── Approval / plan controls (no-ops when the adapter lacks them) ──
84 85 86 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 84 def approve(conversation_id, action_id) send_adapter(conversation_id, :approve_action, action_id) end |
#approve_all(conversation_id) ⇒ Object
92 93 94 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 92 def approve_all(conversation_id) send_adapter(conversation_id, :approve_all) end |
#approve_plan(conversation_id) ⇒ Object
96 97 98 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 96 def approve_plan(conversation_id) send_adapter(conversation_id, :approve_plan) end |
#pending_approvals(conversation_id) ⇒ Object
108 109 110 111 112 113 114 115 116 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 108 def pending_approvals(conversation_id) sid = @mutex.synchronize { @sessions[conversation_id] } return [] unless sid if adapter.respond_to?(:pending_approvals) adapter.pending_approvals(sid) else [] end end |
#reject(conversation_id, action_id) ⇒ Object
88 89 90 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 88 def reject(conversation_id, action_id) send_adapter(conversation_id, :reject_action, action_id) end |
#reject_plan(conversation_id) ⇒ Object
100 101 102 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 100 def reject_plan(conversation_id) send_adapter(conversation_id, :reject_plan) end |
#running?(conversation_id) ⇒ Boolean
77 78 79 80 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 77 def running?(conversation_id) thread = @mutex.synchronize { @turns[conversation_id] } thread&.alive? ? true : false end |
#start_turn(conversation, prompt, model: nil) {|Hash| ... } ⇒ Thread
Run a turn for a conversation, streaming harness events to the block. Runs in a background thread; returns the thread.
63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 63 def start_turn(conversation, prompt, model: nil, &on_event) raise ArgumentError, "prompt must not be empty" if prompt.to_s.strip.empty? thread = Thread.new do begin run_turn(conversation, prompt, model: model, &on_event) rescue => e emit(on_event, { type: "error", data: { error: e. } }) end end @mutex.synchronize { @turns[conversation["id"]] = thread } thread end |
#stop ⇒ Object
118 119 120 121 122 123 124 125 126 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 118 def stop @mutex.synchronize do @turns.each_value { |t| t.kill if t.alive? } @turns.clear @sessions.clear @adapter&.stop if @adapter.respond_to?(:stop) @adapter = nil end end |
#system_prompt_for(workspace, agent = nil) ⇒ Object
The system prompt for a workspace. When a declarative agent is selected, its instructions.md become the custom base prompt (project context, guidelines, append, and footer still apply). Adapters that don't accept a per-session prompt (external ACP agents) ignore the extra keywords.
133 134 135 136 137 138 139 140 |
# File 'lib/ask/coding_harness/agent_runner.rb', line 133 def system_prompt_for(workspace, agent = nil) Ask::CodingHarness::SystemPrompt.build( workspace: workspace, custom: @config.system_prompt || (agent_instructions(workspace, agent) if agent), append: @config.system_prompt_append, guidelines: @config.system_prompt_guidelines ) end |