Class: Insika::EdgeLimiter
- Inherits:
-
Middleware
- Object
- Middleware
- Insika::EdgeLimiter
- Defined in:
- lib/insika/edge_limiter.rb
Overview
The production edge: 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).
· calendar budget — WS2: `AgentProfile#budget` caps the spend per
(tenant, agent) over CALENDAR windows (daily/monthly), on the
BudgetLedger. Hard (default): crossing the cap raises the typed
Insika::BudgetExceeded (the envelope quotes budget_exceeded +
retry_after); soft: crossing warns instead — ONE budget_warning event
per window plus a note injected into the context. Crossing `alert_at`
(default 0.8 of the cap) warns the same way, before the wall. The turn's
billed spend (input+output+cached+cache_creation — the A4 rule) lands on
the windows after the terminal.
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: 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.
The BUDGET breach is the ONE deliberate exception: it is a typed failure
(BudgetExceeded), not a customer-facing reply — the operator wants the
envelope to say "budget" and quote when the window rolls, not to hand the
customer a cost message.
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
- #call(state, &nxt) ⇒ Object
-
#initialize(ledger:, settings_store: nil, budget_ledger: nil, event_stream: nil) ⇒ EdgeLimiter
constructor
A new instance of EdgeLimiter.
Constructor Details
#initialize(ledger:, settings_store: nil, budget_ledger: nil, event_stream: nil) ⇒ EdgeLimiter
Returns a new instance of EdgeLimiter.
51 52 53 54 55 56 57 58 |
# File 'lib/insika/edge_limiter.rb', line 51 def initialize(ledger:, settings_store: nil, budget_ledger: nil, event_stream: nil) @ledger = ledger @settings = settings_store # WS2: the calendar-window ledger. nil = budget off (parity — the bare # wiring is byte-identical to before). @budget_ledger = budget_ledger @event_stream = event_stream end |
Instance Method Details
#call(state, &nxt) ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/insika/edge_limiter.rb', line 60 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 # WS2: calendar budgets. Entry — a HARD budget at/over the cap raises the # typed error (never a customer-facing reply); the alert_at warning and # the SOFT over-cap both warn once per window + inject a context note. # A resumed turn (crash/pause replay) was already admitted: it is never # refused twice — its spend still lands on the ledger below. budget_on = budget_configured?(state) budget_enforce(state) unless resumed result = begin nxt.call(state) ensure # A turn that FAILED after burning tokens still SPENT them: record the # usage the state captured before the error propagates. The ask's usage # lands on state.usage before any later stage (guardrail block, tool # error, workflow schema) can fail the turn — a failed turn must count # against the budget like a completed one (WS2). record_usage(state, record_after) if record_after record_budget_usage(state) if budget_on end result end |