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). -
#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.
148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/insika/turn_state.rb', line 148 def initialize(task:, profile:, turn:, message:) @task = task @profile = profile @turn = turn @message = @capability_names = {} # 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)).
116 117 118 |
# File 'lib/insika/turn_state.rb', line 116 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)).
116 117 118 |
# File 'lib/insika/turn_state.rb', line 116 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).
75 76 77 |
# File 'lib/insika/turn_state.rb', line 75 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.
68 69 70 |
# File 'lib/insika/turn_state.rb', line 68 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.
81 82 83 |
# File 'lib/insika/turn_state.rb', line 81 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)).
116 117 118 |
# File 'lib/insika/turn_state.rb', line 116 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.
87 88 89 |
# File 'lib/insika/turn_state.rb', line 87 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) => []).
110 111 112 |
# File 'lib/insika/turn_state.rb', line 110 def skip_side_effects @skip_side_effects 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.
91 92 93 |
# File 'lib/insika/turn_state.rb', line 91 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.
123 124 125 |
# File 'lib/insika/turn_state.rb', line 123 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).
97 98 99 |
# File 'lib/insika/turn_state.rb', line 97 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.
104 105 106 |
# File 'lib/insika/turn_state.rb', line 104 def usage @usage end |
Instance Method Details
#current_tool_call ⇒ Object
52 |
# File 'lib/insika/turn_state.rb', line 52 def current_tool_call = Fiber[CALL_KEY] |
#current_tool_call=(call) ⇒ Object
55 56 57 |
# File 'lib/insika/turn_state.rb', line 55 def current_tool_call=(call) Fiber[CALL_KEY] = call end |
#current_tool_name ⇒ Object
53 |
# File 'lib/insika/turn_state.rb', line 53 def current_tool_name = Fiber[NAME_KEY] |
#current_tool_name=(name) ⇒ Object
59 60 61 |
# File 'lib/insika/turn_state.rb', line 59 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.
137 138 139 140 |
# File 'lib/insika/turn_state.rb', line 137 def requested_tool_concurrency n = ((profile.respond_to?(:limits) && profile.limits) || {})[:tool_concurrency].to_i n > 1 ? n : nil end |
#tool_concurrency ⇒ Object
142 143 144 145 146 |
# File 'lib/insika/turn_state.rb', line 142 def tool_concurrency return nil unless Array(requires_approval).empty? requested_tool_concurrency end |