Class: Silas::Tools::Handoff
- Inherits:
-
Silas::Tool
- Object
- Silas::Tool
- Silas::Tools::Handoff
- Defined in:
- lib/silas/tools/handoff.rb
Overview
Staff-to-staff composition WITHOUT free-form agent chatter: file a brief that starts (or awaits) another named agent's session, linked to this one. at_most_once! — the handoff is one external effect; a crash parks it in-doubt instead of double-starting the colleague.
Constant Summary collapse
- MAX_CHAIN =
3
Instance Attribute Summary
Attributes inherited from Silas::Tool
Class Method Summary collapse
-
.description ⇒ Object
Roster from DIRECTORY NAMES only — reading scopes here would recurse (description -> digest -> scope build -> description).
Instance Method Summary collapse
Methods inherited from Silas::Tool
approval, #approval_policy, at_most_once!, effect_mode, #effect_mode, idempotent!, param, schema, tool_name, transactional!, validate_signature!
Class Method Details
.description ⇒ Object
Roster from DIRECTORY NAMES only — reading scopes here would recurse (description -> digest -> scope build -> description). Names alone keep the digest roster-sensitive without the cycle.
12 13 14 15 16 17 18 |
# File 'lib/silas/tools/handoff.rb', line 12 def description names = Dir[Rails.root.join("app/agents/*")].select { |p| File.directory?(p) } .map { |p| File.basename(p) }.sort "Hand a task to another staff agent as a self-contained brief (they share none of " \ "your context). Async by default; await: true waits for their answer. " \ "Staff: #{names.join(', ')}." end |
Instance Method Details
#call(agent:, brief:, await: nil) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/silas/tools/handoff.rb', line 29 def call(agent:, brief:, await: nil) target = agent.to_s return { "error" => "unknown agent #{target.inspect}" } unless Silas.named_agent?(target) return { "error" => "an agent cannot hand off to itself" } if target == session.agent_name chain = ancestry(session) if chain.include?(target) return { "error" => "handoff cycle: #{[ *chain.reverse, session.agent_name, target ].join(' -> ')}" } end return { "error" => "handoff chain too deep (max #{MAX_CHAIN})" } if chain.size >= MAX_CHAIN nested = Session.create!(agent_name: target, parent_session_id: session.id, metadata: { "handoff_from" => session.agent_name }) if await # Inline drive via NestedRunner (NOT AgentLoopJob.perform_now: a # Continuable job's isolated steps re-enqueue and return early under # production isolate_steps — the await would see a half-run turn). turn = NestedRunner.run(nested, input: brief, scope: Silas.named_agent_scope!(target)) { "session_id" => nested.id, "status" => turn.status, "answer" => turn.answer_text.to_s } else turn = nested.continue(input: brief, enqueue: false) AgentLoopJob.perform_later(turn.id) { "session_id" => nested.id, "status" => "queued" } end end |