Class: Insika::TurnState
- Inherits:
-
Object
- Object
- Insika::TurnState
- Defined in:
- lib/insika/turn_state.rb
Overview
MUTABLE on purpose (the only exception to the Data types): the Middleware MODIFIES the execution — the links write into these fields.
Constant Summary collapse
- CALL_KEY =
Internal (not part of the contract): per-CALL correlation between RubyLLM's tool callbacks and the tool decorators —
current_tool_callkeys the side-effect checkpoint / resume skip / trace,current_tool_namelabels the :tool_result event.They live in FIBER STORAGE, not in ivars, and that is the whole point:
before_tool_call→tool.call→after_tool_resultall run in the SAME fiber, and withToolConcurrencythere is one fiber PER CALL. A single slot on this shared object would let one in-flight call overwrite another's — a side-effect recorded under the wrong id (so a resume skips the wrong tool, or re-runs a non-idempotent one) and a mislabelled event. Both silent. One writer per fiber needs no lock; serial execution is unchanged, since a lone fiber writes and reads its own storage.Read/written ONLY through here so the rule has one home.
:insika_tool_call- NAME_KEY =
:insika_tool_name
Instance Attribute Summary collapse
-
#actor ⇒ Object
Approval gate.
-
#approval_coordinator ⇒ Object
Approval gate.
-
#capability_names ⇒ Object
Internal: impl_name(String) -> STABLE name of the capability that resolved it, computed by resolve_capabilities BEFORE the policy_request and consulted AFTER @policy_engine.decide, at the post-Policy junction, to decide which impls enter as Capability::ResolvedTool.
-
#chat_baseline ⇒ Object
Internal (R1): the chat's message count RIGHT AFTER
assemble(seeded history) and BEFOREask. -
#profile ⇒ Object
readonly
turn identity (1-based).
-
#queue_policy ⇒ Object
Internal: the turn's resolved QueuePolicy.
-
#requires_approval ⇒ Object
Approval gate.
-
#resumed ⇒ Object
Internal: true when this turn re-enters the pipeline via resume_task/recovery.
-
#skip_side_effects ⇒ Object
Internal (Tool Search): ids of side-effects already completed in the interrupted turn, propagated to the tools PROMOTED by tool_search (the same
skipthat the eager tools' wrap_tools receives). -
#steer_injector ⇒ Object
Internal: the run's SteerInjector, or nil when steering is off (or the chat cannot host the boundary).
-
#task ⇒ Object
readonly
turn identity (1-based).
-
#tenant ⇒ Object
Internal (memory): the turn's tenant (from the Command), scope of the write path (
remembertool). -
#tool_gate ⇒ Object
Internal: the turn's shared in-flight cap for tool calls — ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools and acquired by every ToolEnvelope, INCLUDING the ones tool_search promotes mid-turn (they read it off the state, so the cap survives promotion).
-
#turn ⇒ Object
readonly
turn identity (1-based).
-
#turn_context ⇒ Object
Internal: turn context deposited into the data-tools to resolve {ctx{ctx.*} (chat_id/agent_id/tenant/store_id) and emit X-Chat-Id/X-Store-Id/X-Agent-Id.
-
#usage ⇒ Object
Internal (observability): the turn's token usage (input/output/ total/cached + model), captured from the provider's response at stage 6.
Instance Method Summary collapse
- #current_tool_call ⇒ Object
- #current_tool_call=(call) ⇒ Object
- #current_tool_name ⇒ Object
- #current_tool_name=(name) ⇒ Object
-
#initialize(task:, profile:, turn:, message:) ⇒ TurnState
constructor
A new instance of TurnState.
-
#requested_tool_concurrency ⇒ Object
parallel tool calls, resolved PER TURN and read by ChatBuilder (whether to hand the gem
concurrency:) and ToolAssembly (the gate's size). - #tool_concurrency ⇒ Object
Constructor Details
#initialize(task:, profile:, turn:, message:) ⇒ TurnState
Returns a new instance of TurnState.
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/insika/turn_state.rb', line 193 def initialize(task:, profile:, turn:, message:) @task = task @profile = profile @turn = turn @message = @capability_names = {} @output_parts = [] @channel_capabilities = [] @evidence_attachments = [] # Fiber storage is INHERITED by fibers created later, so a turn spawned from # inside a tool call (a subagent child) would start out carrying its # parent's correlation. Clearing at turn start keeps a child from keying its # own side-effects under the parent's tool_call id. self.current_tool_call = nil self.current_tool_name = nil end |
Instance Attribute Details
#actor ⇒ Object
Approval gate. requires_approval = names of tools that require
approval (Resolution); approval_coordinator = object (the Executor) that
creates the PendingAction/suspends/waits; actor = the turn's mailbox (used
by the coordinator for await(:approval)).
161 162 163 |
# File 'lib/insika/turn_state.rb', line 161 def actor @actor end |
#approval_coordinator ⇒ Object
Approval gate. requires_approval = names of tools that require
approval (Resolution); approval_coordinator = object (the Executor) that
creates the PendingAction/suspends/waits; actor = the turn's mailbox (used
by the coordinator for await(:approval)).
161 162 163 |
# File 'lib/insika/turn_state.rb', line 161 def approval_coordinator @approval_coordinator end |
#capability_names ⇒ Object
Internal: impl_name(String) -> STABLE name of the capability that resolved it, computed by resolve_capabilities BEFORE the policy_request and consulted AFTER @policy_engine.decide, at the post-Policy junction, to decide which impls enter as Capability::ResolvedTool. {} = no capability_registry or empty profile.capabilities (parity).
114 115 116 |
# File 'lib/insika/turn_state.rb', line 114 def capability_names @capability_names end |
#chat_baseline ⇒ Object
Internal (R1): the chat's message count RIGHT AFTER assemble (seeded
history) and BEFORE ask. persist_turn slices chat.messages.drop(baseline)
to serialize the turn's real exchange — user + assistant(tool_calls) + tool
results + final assistant — into the transcript. nil = no chat recorded
(workflow/halt) → persist_turn falls back to the assistant pair.
107 108 109 |
# File 'lib/insika/turn_state.rb', line 107 def chat_baseline @chat_baseline end |
#profile ⇒ Object (readonly)
turn identity (1-based)
7 8 9 |
# File 'lib/insika/turn_state.rb', line 7 def profile @profile end |
#queue_policy ⇒ Object
Internal: the turn's resolved QueuePolicy. Read at stage 6 to decide whether this run accepts steered messages, and how they are worded. Resolved once per turn, in build_turn_state — an edit to the agent mid-run does not change the rules the run started under.
120 121 122 |
# File 'lib/insika/turn_state.rb', line 120 def queue_policy @queue_policy end |
#requires_approval ⇒ Object
Approval gate. requires_approval = names of tools that require
approval (Resolution); approval_coordinator = object (the Executor) that
creates the PendingAction/suspends/waits; actor = the turn's mailbox (used
by the coordinator for await(:approval)).
161 162 163 |
# File 'lib/insika/turn_state.rb', line 161 def requires_approval @requires_approval end |
#resumed ⇒ Object
Internal: true when this turn re-enters the pipeline via resume_task/recovery. The EdgeLimiter reads it to NEVER re-count or block a turn that was already admitted — a crash/pause under a saturated window must not swallow a legitimate message with the rate-limit reply.
132 133 134 |
# File 'lib/insika/turn_state.rb', line 132 def resumed @resumed end |
#skip_side_effects ⇒ Object
Internal (Tool Search): ids of side-effects already completed in the
interrupted turn, propagated to the tools PROMOTED by tool_search (the same skip
that the eager tools' wrap_tools receives). Set in run_pipeline;
nil = new turn (Array(nil) => []).
155 156 157 |
# File 'lib/insika/turn_state.rb', line 155 def skip_side_effects @skip_side_effects end |
#steer_injector ⇒ Object
Internal: the run's SteerInjector, or nil when steering is off (or the chat cannot host the boundary). Rebuilt per ATTEMPT — a WS3 fallback builds a new chat, and the injector belongs to the chat it appends to. The Executor reaches for it at the end of stage 6 to absorb whatever no tool boundary took.
126 127 128 |
# File 'lib/insika/turn_state.rb', line 126 def steer_injector @steer_injector end |
#task ⇒ Object (readonly)
turn identity (1-based)
7 8 9 |
# File 'lib/insika/turn_state.rb', line 7 def task @task end |
#tenant ⇒ Object
Internal (memory): the turn's tenant (from the Command), scope of the write path
(remember tool). Set in run_pipeline; nil = DEFAULT_TENANT in the MemoryStore.
136 137 138 |
# File 'lib/insika/turn_state.rb', line 136 def tenant @tenant end |
#tool_gate ⇒ Object
Internal: the turn's shared in-flight cap for tool calls — ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools and acquired by every ToolEnvelope, INCLUDING the ones tool_search promotes mid-turn (they read it off the state, so the cap survives promotion). nil = concurrency off: no gate, no overhead, serial execution unchanged.
168 169 170 |
# File 'lib/insika/turn_state.rb', line 168 def tool_gate @tool_gate end |
#turn ⇒ Object (readonly)
turn identity (1-based)
7 8 9 |
# File 'lib/insika/turn_state.rb', line 7 def turn @turn end |
#turn_context ⇒ Object
Internal: turn context deposited into the data-tools to
resolve Insika::TurnState.{ctx{ctx.*} (chat_id/agent_id/tenant/store_id) and emit
X-Chat-Id/X-Store-Id/X-Agent-Id. A Hash of symbols, set in run_pipeline.
Comes from the TURN, never from the model's args (R2). Distinct from tenant (memory).
142 143 144 |
# File 'lib/insika/turn_state.rb', line 142 def turn_context @turn_context end |
#usage ⇒ Object
Internal (observability): the turn's token usage (input/output/ total/cached + model), captured from the provider's response at stage 6. Goes to the terminal event (:task_completed) — feeds the usage of /v1/responses and the Telemetry (OTEL). nil = turn with no model response (workflow) or provider without counts.
149 150 151 |
# File 'lib/insika/turn_state.rb', line 149 def usage @usage end |
Instance Method Details
#current_tool_call ⇒ Object
91 |
# File 'lib/insika/turn_state.rb', line 91 def current_tool_call = Fiber[CALL_KEY] |
#current_tool_call=(call) ⇒ Object
94 95 96 |
# File 'lib/insika/turn_state.rb', line 94 def current_tool_call=(call) Fiber[CALL_KEY] = call end |
#current_tool_name ⇒ Object
92 |
# File 'lib/insika/turn_state.rb', line 92 def current_tool_name = Fiber[NAME_KEY] |
#current_tool_name=(name) ⇒ Object
98 99 100 |
# File 'lib/insika/turn_state.rb', line 98 def current_tool_name=(name) Fiber[NAME_KEY] = name end |
#requested_tool_concurrency ⇒ Object
parallel tool calls, resolved PER TURN and read by ChatBuilder
(whether to hand the gem concurrency:) and ToolAssembly (the gate's size).
requested_tool_concurrency is what the operator configured;
tool_concurrency is what this turn actually gets. They differ for exactly
one reason —: Executor#request_approval blocks on actor.await(approval),
and the mailbox is one queue per TASK. Two fibers waiting there share it,
dequeue wakes exactly one, the message is consumed, and the other fiber
hangs until approval_timeout (~1h). So a turn that can suspend for a human
runs its tools serially. Per-TURN and not per-profile because
requires_approval comes from the Resolution: it can be empty on a turn
whose profile does list approvals.
182 183 184 185 |
# File 'lib/insika/turn_state.rb', line 182 def requested_tool_concurrency n = ((profile.respond_to?(:limits) && profile.limits) || {})[:tool_concurrency].to_i n > 1 ? n : nil end |
#tool_concurrency ⇒ Object
187 188 189 190 191 |
# File 'lib/insika/turn_state.rb', line 187 def tool_concurrency return nil unless Array(requires_approval).empty? requested_tool_concurrency end |