Class: Insika::TurnState

Inherits:
Object
  • Object
show all
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_call keys the side-effect checkpoint / resume skip / trace, current_tool_name labels the :tool_result event.

They live in FIBER STORAGE, not in ivars, and that is the whole point: before_tool_calltool.callafter_tool_result all run in the SAME fiber, and with ToolConcurrency there 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

Instance Method Summary collapse

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 = 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

#actorObject

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_coordinatorObject

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_namesObject

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_baselineObject

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

#profileObject (readonly)

turn identity (1-based)



7
8
9
# File 'lib/insika/turn_state.rb', line 7

def profile
  @profile
end

#queue_policyObject

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_approvalObject

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

#resumedObject

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_effectsObject

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

#taskObject (readonly)

turn identity (1-based)



7
8
9
# File 'lib/insika/turn_state.rb', line 7

def task
  @task
end

#tenantObject

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_gateObject

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

#turnObject (readonly)

turn identity (1-based)



7
8
9
# File 'lib/insika/turn_state.rb', line 7

def turn
  @turn
end

#turn_contextObject

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

#usageObject

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_callObject



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_nameObject



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_concurrencyObject

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_concurrencyObject



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