Class: Insika::EdgeLimiter

Inherits:
Middleware show all
Defined in:
lib/insika/edge_limiter.rb

Overview

The production edge (item 33 / §12 G7): THE named place where volume/cost abuse is cut. A Middleware with two independent limits, both OPT-IN (nil/0 = off — a bare wiring behaves exactly as before):

· chat rate limit    — turn ATTEMPTS per chat per window. Counted on entry
(a blocked attempt still counts), so a flood keeps hitting the wall.
· agent token ceiling — total tokens per agent per window. Checked on entry
against the accumulated ledger; the turn's own usage is recorded AFTER the
terminal returns (the Middleware wraps stages 5-9, so state.usage is set).

Config resolution, per turn (configuration over convention):

profile.limits[:chat_rate_limit / :agent_token_ceiling]  — per-agent override
settings["edge"]                                          — platform default

A per-agent 0 explicitly disables a platform default for that agent.

On breach it uses the graceful-halt contract (RFC-0009 §3.1): halt_response (the safe reply) + guardrail_block (audit -> :guardrail_blocked) and does NOT call nxt — the turn completes with ZERO LLM calls. It sits BEFORE the InputGuardrail in the stack so a flood can't spend the LLM moderator either.

Constant Summary collapse

CHAT_KIND =
"chat"
TOKENS_KIND =
"tokens"
DEFAULT_CHAT_WINDOW =

seconds

60
DEFAULT_TOKEN_WINDOW =

seconds (daily ceiling)

86_400
DEFAULT_RESPONSE =

Neutral fallback, same contract as Safety::SafeResponses (pt-BR — the pilot's language; override via settings edge.limit_response).

"Estou recebendo muitas mensagens agora. Aguarde um " \
"momento e tente novamente, por favor."

Instance Method Summary collapse

Constructor Details

#initialize(ledger:, settings_store: nil) ⇒ EdgeLimiter

Returns a new instance of EdgeLimiter.



38
39
40
41
# File 'lib/insika/edge_limiter.rb', line 38

def initialize(ledger:, settings_store: nil)
  @ledger = ledger
  @settings = settings_store
end

Instance Method Details

#call(state, &nxt) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/insika/edge_limiter.rb', line 43

def call(state, &nxt)
  edge = platform_edge
  limits = state.profile.limits || {}
  # A resume (crash/pause recovery) re-enters the pipeline for a turn that was
  # ALREADY admitted: re-counting it would swallow a legitimate message with
  # the rate-limit reply exactly when the window is saturated. Entry checks
  # are skipped; the turn's usage still lands on the ledger below.
  resumed = state.resumed

  if !resumed && (limit = positive(limits.key?(:chat_rate_limit) ? limits[:chat_rate_limit] : edge["chat_rate_limit"]))
    breach = check_chat_rate(state, limit, edge)
    return block(state, edge, **breach) if breach
  end

  # NB: a per-agent key PRESENT with nil (e.g. an imported pack carrying
  # `"chat_rate_limit": null`) reads as OFF for that agent, not "inherit".
  if (ceiling = positive(limits.key?(:agent_token_ceiling) ? limits[:agent_token_ceiling] : edge["agent_token_ceiling"]))
    token_window = positive(edge["agent_token_window"]) || DEFAULT_TOKEN_WINDOW
    unless resumed
      spent = @ledger.count(TOKENS_KIND, state.profile.id.to_s, window: token_window)
      if spent >= ceiling
        return block(state, edge, category: :token_ceiling,
                                  detail: "agent #{state.profile.id}: #{spent}/#{ceiling} tokens per #{token_window}s")
      end
    end

    record_after = token_window
  end

  result = nxt.call(state)
  record_usage(state, record_after) if record_after
  result
end