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 withToolConcurrency(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
-
#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 (§11 R1): the chat's message count RIGHT AFTER
assemble(seeded history) and BEFOREask. -
#profile ⇒ Object
readonly
turn identity (1-based).
-
#queue_policy ⇒ Object
Internal (RFC-0015): the turn's resolved QueuePolicy.
-
#requires_approval ⇒ Object
Approval gate.
-
#resumed ⇒ Object
Internal (item 33): 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 (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).
-
#turn ⇒ Object
readonly
turn identity (1-based).
-
#turn_context ⇒ Object
Internal (Phase 6/D2/G4): 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 (Phase 6, 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
Item 30: 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.
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 = @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)).
112 113 114 |
# File 'lib/insika/turn_state.rb', line 112 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)).
112 113 114 |
# File 'lib/insika/turn_state.rb', line 112 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).
71 72 73 |
# File 'lib/insika/turn_state.rb', line 71 def capability_names @capability_names end |
#chat_baseline ⇒ Object
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 |
#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 (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_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)).
112 113 114 |
# File 'lib/insika/turn_state.rb', line 112 def requires_approval @requires_approval end |
#resumed ⇒ Object
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_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) => []).
106 107 108 |
# File 'lib/insika/turn_state.rb', line 106 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.
87 88 89 |
# File 'lib/insika/turn_state.rb', line 87 def tenant @tenant end |
#tool_gate ⇒ Object
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 |
#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 (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 |
#usage ⇒ Object
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_call ⇒ Object
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_name ⇒ Object
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_concurrency ⇒ Object
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_concurrency ⇒ Object
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 |