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 (item 30) 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.



144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/insika/turn_state.rb', line 144

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)).



112
113
114
# File 'lib/insika/turn_state.rb', line 112

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)).



112
113
114
# File 'lib/insika/turn_state.rb', line 112

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).



71
72
73
# File 'lib/insika/turn_state.rb', line 71

def capability_names
  @capability_names
end

#chat_baselineObject

Internal (§11 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.



64
65
66
# File 'lib/insika/turn_state.rb', line 64

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 (RFC-0015): 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.



77
78
79
# File 'lib/insika/turn_state.rb', line 77

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)).



112
113
114
# File 'lib/insika/turn_state.rb', line 112

def requires_approval
  @requires_approval
end

#resumedObject

Internal (item 33): 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.



83
84
85
# File 'lib/insika/turn_state.rb', line 83

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) => []).



106
107
108
# File 'lib/insika/turn_state.rb', line 106

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.



87
88
89
# File 'lib/insika/turn_state.rb', line 87

def tenant
  @tenant
end

#tool_gateObject

Internal (item 30 / D4): 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.



119
120
121
# File 'lib/insika/turn_state.rb', line 119

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 (Phase 6/D2/G4): 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).



93
94
95
# File 'lib/insika/turn_state.rb', line 93

def turn_context
  @turn_context
end

#usageObject

Internal (Phase 6, 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.



100
101
102
# File 'lib/insika/turn_state.rb', line 100

def usage
  @usage
end

Instance Method Details

#current_tool_callObject



48
# File 'lib/insika/turn_state.rb', line 48

def current_tool_call = Fiber[CALL_KEY]

#current_tool_call=(call) ⇒ Object



51
52
53
# File 'lib/insika/turn_state.rb', line 51

def current_tool_call=(call)
  Fiber[CALL_KEY] = call
end

#current_tool_nameObject



49
# File 'lib/insika/turn_state.rb', line 49

def current_tool_name = Fiber[NAME_KEY]

#current_tool_name=(name) ⇒ Object



55
56
57
# File 'lib/insika/turn_state.rb', line 55

def current_tool_name=(name)
  Fiber[NAME_KEY] = name
end

#requested_tool_concurrencyObject

Item 30: 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 — D3: 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.



133
134
135
136
# File 'lib/insika/turn_state.rb', line 133

def requested_tool_concurrency
  n = ((profile.respond_to?(:limits) && profile.limits) || {})[:tool_concurrency].to_i
  n > 1 ? n : nil
end

#tool_concurrencyObject



138
139
140
141
142
# File 'lib/insika/turn_state.rb', line 138

def tool_concurrency
  return nil unless Array(requires_approval).empty?

  requested_tool_concurrency
end