Module: Insika::SubagentGraph
- Defined in:
- lib/insika/subagent_graph.rb
Overview
Definition-time integrity of the subagent delegation graph (RFC-0010 ยง4.4,
item 21). A pure function over {id => [child_ids]}: detects CYCLES and
computes the max delegation DEPTH, raising a typed SubagentError so the
authoring Command (CreateAgent/UpdateAgent) fails cleanly and boot refuses a
bad static set. Cycle + bounded depth are exactly Flue's definition-time
guarantee (DelegationDepthExceededError + anti-circular) โ with the graph
acyclic and depth <= cap, the runtime is provably bounded (the guard in
Executor#run_subagent is only belt-and-suspenders for a graph that changed
mid-run).
UNKNOWN child refs are treated as LEAVES (no outgoing edges), NOT an error: dynamic authoring must not break by creation order โ a not-yet-created child surfaces as a clean "not found" at runtime (run_subagent), not a boot failure.
Constant Summary collapse
- DEFAULT_DEPTH_CAP =
Longest delegation chain allowed (root counts as depth 0; each spawn +1). Override with INSIKA_SUBAGENT_DEPTH_CAP.
5- DEFAULT_FANOUT_CAP =
Max children a single
spawn_subagentsfan-out may run โ also the concurrency bound (N concurrent LLM calls hit the provider rate limit + the per-agent token ceiling of item 33). Override with INSIKA_SUBAGENT_FANOUT_CAP. 8
Class Method Summary collapse
-
.check_from(root, map, cap) ⇒ Object
DFS from
roottracking the recursion stack (cycle) and the longest path (depth). - .depth_cap ⇒ Object
-
.each_profile(profiles) ⇒ Object
Duck-typed enumeration: an Array of profiles, or a ProfileSource exposing #all.
- .fan_out_cap ⇒ Object
- .int_env(name, default) ⇒ Object
- .normalize(hash) ⇒ Object
-
.to_map(profiles) ⇒ Object
Builds the => [child_ids] adjacency map.
-
.validate!(profiles, cap: depth_cap) ⇒ Object
Validates the whole set.
-
.walk(node, map, cap, stack, memo) ⇒ Object
Returns the max depth of the subtree rooted at
node.
Class Method Details
.check_from(root, map, cap) ⇒ Object
DFS from root tracking the recursion stack (cycle) and the longest path
(depth). A ref to an id absent from map is a leaf.
62 63 64 |
# File 'lib/insika/subagent_graph.rb', line 62 def check_from(root, map, cap) walk(root, map, cap, [], {}) end |
.depth_cap ⇒ Object
29 30 31 |
# File 'lib/insika/subagent_graph.rb', line 29 def depth_cap int_env("INSIKA_SUBAGENT_DEPTH_CAP", DEFAULT_DEPTH_CAP) end |
.each_profile(profiles) ⇒ Object
Duck-typed enumeration: an Array of profiles, or a ProfileSource exposing #all. Anything else Enumerable is iterated as-is.
89 90 91 92 93 94 |
# File 'lib/insika/subagent_graph.rb', line 89 def each_profile(profiles) return profiles if profiles.is_a?(Array) return profiles.all if profiles.respond_to?(:all) Array(profiles) end |
.fan_out_cap ⇒ Object
33 34 35 |
# File 'lib/insika/subagent_graph.rb', line 33 def fan_out_cap int_env("INSIKA_SUBAGENT_FANOUT_CAP", DEFAULT_FANOUT_CAP) end |
.int_env(name, default) ⇒ Object
37 38 39 40 |
# File 'lib/insika/subagent_graph.rb', line 37 def int_env(name, default) raw = Insika::EnvSchema.read(name) raw && !raw.strip.empty? ? Integer(raw) : default end |
.normalize(hash) ⇒ Object
83 84 85 |
# File 'lib/insika/subagent_graph.rb', line 83 def normalize(hash) hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = Array(v).map(&:to_s) } end |
.to_map(profiles) ⇒ Object
Builds the => [child_ids] adjacency map. Absent/nil subagents => [].
52 53 54 55 56 57 58 |
# File 'lib/insika/subagent_graph.rb', line 52 def to_map(profiles) return normalize(profiles) if profiles.is_a?(Hash) each_profile(profiles).each_with_object({}) do |p, acc| acc[p.id.to_s] = Array(p.subagents).map(&:to_s) end end |
.validate!(profiles, cap: depth_cap) ⇒ Object
Validates the whole set. profiles is anything enumerable of profiles
responding to #id and #subagents (an Array or a ProfileSource#all result),
OR a plain {id => [child_ids]} Hash. Raises on the FIRST violation.
45 46 47 48 49 |
# File 'lib/insika/subagent_graph.rb', line 45 def validate!(profiles, cap: depth_cap) map = to_map(profiles) map.each_key { |root| check_from(root, map, cap) } map end |
.walk(node, map, cap, stack, memo) ⇒ Object
Returns the max depth of the subtree rooted at node. stack is the
current path (cycle detection); memo caches finished subtrees.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/insika/subagent_graph.rb', line 68 def walk(node, map, cap, stack, memo) raise SubagentCycleError.new(cycle: stack + [node]) if stack.include?(node) return memo[node] if memo.key?(node) children = map[node] || [] # unknown ref => leaf depth = if children.empty? 0 else 1 + children.map { |c| walk(c, map, cap, stack + [node], memo) }.max end raise SubagentDepthExceeded.new(depth: depth, cap: cap) if depth > cap memo[node] = depth end |