interlock-agentgov 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agentgov/__init__.py ADDED
@@ -0,0 +1,213 @@
1
+ """AgentGov: a runtime spend-governance kernel for AI agents.
2
+
3
+ Provides hierarchical, capability-scoped spend budgets over an immutable,
4
+ hash-chained ledger, with a latching kernel-level circuit breaker — so a
5
+ runaway agent (or a tree of sub-agents spawning sub-agents) cannot exceed
6
+ its delegated envelope. A denial-of-wallet backstop for agentic systems.
7
+
8
+ Quick start::
9
+
10
+ from agentgov import BudgetManager, Interceptor, money
11
+
12
+ gov = BudgetManager()
13
+ gov.open_root("orchestrator", money("1.00"))
14
+ gov.delegate("orchestrator", "researcher", money("0.25"))
15
+
16
+ metered = Interceptor(gov, "researcher", model="claude-opus-5")
17
+ result = metered.invoke(client.messages.create, model=..., messages=[...])
18
+
19
+ print(result.cost, gov.available("researcher"))
20
+
21
+ For a governor whose ledger and topology survive a process restart, open a
22
+ SQLite-backed one instead::
23
+
24
+ with BudgetManager.open_sqlite("governor.db") as gov:
25
+ ... # same API; state above is durable across restarts
26
+
27
+ See :mod:`agentgov.core` for the ledger and budget DAG,
28
+ :mod:`agentgov.exceptions` for the error hierarchy,
29
+ :mod:`agentgov.interceptor` for the call-site enforcement wrappers,
30
+ :mod:`agentgov.storage` for the durable-store interface,
31
+ :mod:`agentgov.adapters` for the LangChain and CrewAI drop-ins, and
32
+ :mod:`agentgov.reconciliation` for matching a provider invoice against the
33
+ ledger.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from agentgov.cognitive import (
39
+ CallCycleDetector,
40
+ CognitiveBreaker,
41
+ CognitivePolicy,
42
+ ExactRepeatDetector,
43
+ LoopDetector,
44
+ NearDuplicateDetector,
45
+ Redactor,
46
+ SemanticObserver,
47
+ ToolCall,
48
+ TrajectoryEntropyObserver,
49
+ Verdict,
50
+ canonical_arguments,
51
+ extract_result_text,
52
+ )
53
+ from agentgov.core import (
54
+ Authorization,
55
+ BudgetManager,
56
+ BudgetNode,
57
+ ControlEvent,
58
+ Direction,
59
+ EntryType,
60
+ GovernancePolicy,
61
+ JoinedTransaction,
62
+ Ledger,
63
+ LedgerEntry,
64
+ LedgerLine,
65
+ SettlementClaim,
66
+ WriteBatch,
67
+ format_audit_line,
68
+ money,
69
+ )
70
+ from agentgov.exceptions import (
71
+ AgentGovError,
72
+ AgentThrashingError,
73
+ AgentThrashingException,
74
+ BudgetError,
75
+ BudgetExceededError,
76
+ CircuitBreakerError,
77
+ CircuitOpenError,
78
+ ConcurrentGovernorError,
79
+ DenialOfWalletError,
80
+ DenialOfWalletException,
81
+ DoubleSpendError,
82
+ DuplicateScopeError,
83
+ LedgerConflictError,
84
+ LedgerError,
85
+ LedgerIntegrityError,
86
+ ReadOnlyLedgerError,
87
+ RunawayLoopDetectedError,
88
+ ScopeError,
89
+ StorageError,
90
+ SubBudgetAllocationError,
91
+ UnknownScopeError,
92
+ )
93
+ from agentgov.interceptor import (
94
+ CHARS_PER_TOKEN,
95
+ PRICING,
96
+ Interceptor,
97
+ MeteredCall,
98
+ ModelPricing,
99
+ SpendGuard,
100
+ TokenUsage,
101
+ default_usage_extractor,
102
+ estimate_tokens,
103
+ extract_prompt_text,
104
+ normalize_model_id,
105
+ pricing_for,
106
+ )
107
+ from agentgov.proxy import GovernedClient, GovernorHandle, govern
108
+ from agentgov.reconciliation import (
109
+ MeteringJournal,
110
+ ProviderUsageRecord,
111
+ ReconciliationPolicy,
112
+ ReconciliationReport,
113
+ load_provider_export,
114
+ reconcile,
115
+ )
116
+ from agentgov.storage import (
117
+ JoinableStore,
118
+ PersistedAuthorization,
119
+ PersistedNode,
120
+ PersistenceStore,
121
+ SharedStore,
122
+ SqliteStore,
123
+ StoreDelta,
124
+ StoreImage,
125
+ WriterSession,
126
+ )
127
+ from agentgov.streaming import AsyncMeteredStream, MeteredStream
128
+
129
+ __all__ = [
130
+ "CHARS_PER_TOKEN",
131
+ "PRICING",
132
+ "AgentGovError",
133
+ "AgentThrashingError",
134
+ "AgentThrashingException",
135
+ "AsyncMeteredStream",
136
+ "Authorization",
137
+ "BudgetError",
138
+ "BudgetExceededError",
139
+ "BudgetManager",
140
+ "BudgetNode",
141
+ "CallCycleDetector",
142
+ "CircuitBreakerError",
143
+ "CircuitOpenError",
144
+ "CognitiveBreaker",
145
+ "CognitivePolicy",
146
+ "ConcurrentGovernorError",
147
+ "ControlEvent",
148
+ "DenialOfWalletError",
149
+ "DenialOfWalletException",
150
+ "Direction",
151
+ "DoubleSpendError",
152
+ "DuplicateScopeError",
153
+ "EntryType",
154
+ "ExactRepeatDetector",
155
+ "GovernancePolicy",
156
+ "GovernedClient",
157
+ "GovernorHandle",
158
+ "Interceptor",
159
+ "JoinableStore",
160
+ "JoinedTransaction",
161
+ "Ledger",
162
+ "LedgerConflictError",
163
+ "LedgerEntry",
164
+ "LedgerError",
165
+ "LedgerIntegrityError",
166
+ "LedgerLine",
167
+ "LoopDetector",
168
+ "MeteredCall",
169
+ "MeteredStream",
170
+ "MeteringJournal",
171
+ "ModelPricing",
172
+ "NearDuplicateDetector",
173
+ "PersistedAuthorization",
174
+ "PersistedNode",
175
+ "PersistenceStore",
176
+ "ProviderUsageRecord",
177
+ "ReadOnlyLedgerError",
178
+ "ReconciliationPolicy",
179
+ "ReconciliationReport",
180
+ "Redactor",
181
+ "RunawayLoopDetectedError",
182
+ "ScopeError",
183
+ "SemanticObserver",
184
+ "SettlementClaim",
185
+ "SharedStore",
186
+ "SpendGuard",
187
+ "SqliteStore",
188
+ "StorageError",
189
+ "StoreDelta",
190
+ "StoreImage",
191
+ "SubBudgetAllocationError",
192
+ "TokenUsage",
193
+ "ToolCall",
194
+ "TrajectoryEntropyObserver",
195
+ "UnknownScopeError",
196
+ "Verdict",
197
+ "WriteBatch",
198
+ "WriterSession",
199
+ "canonical_arguments",
200
+ "default_usage_extractor",
201
+ "estimate_tokens",
202
+ "extract_prompt_text",
203
+ "extract_result_text",
204
+ "format_audit_line",
205
+ "govern",
206
+ "load_provider_export",
207
+ "money",
208
+ "normalize_model_id",
209
+ "pricing_for",
210
+ "reconcile",
211
+ ]
212
+
213
+ __version__ = "0.3.0"
@@ -0,0 +1,23 @@
1
+ """Drop-in adapters for the major agent frameworks.
2
+
3
+ Each adapter is a thin translation layer between a framework's execution model
4
+ and AgentGov's ``authorize -> execute -> capture`` lifecycle. None of them adds
5
+ a dependency: every framework object is **duck-typed**, so
6
+ ``import agentgov.adapters.langchain`` works on a machine with no LangChain
7
+ installed, and the adapters are testable against fakes rather than against a
8
+ pinned framework version.
9
+
10
+ That choice is deliberate. A governor that only works with LangChain 0.3.x is
11
+ a liability the first time LangChain ships 0.4; matching on the shapes these
12
+ frameworks expose — and degrading clearly when a shape is unrecognised — keeps
13
+ AgentGov useful across versions it has never seen.
14
+
15
+ - :mod:`agentgov.adapters.langchain` — ``GovernedChatModel`` and
16
+ ``GovernedCallbackHandler`` for LangChain and LangGraph.
17
+ - :mod:`agentgov.adapters.crewai` — ``GovernedCrew`` and ``govern_agent`` for
18
+ CrewAI and any framework reporting cumulative ``usage_metrics``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ __all__ = ["crewai", "langchain"]
@@ -0,0 +1,417 @@
1
+ """CrewAI adapter, and a generic wrapper for any framework reporting usage totals.
2
+
3
+ CrewAI does not surface individual model calls. It runs a crew to completion
4
+ and hands back a ``UsageMetrics`` object with the totals for the whole run.
5
+ That shapes the integration in two ways worth stating plainly:
6
+
7
+ **Settlement is per-run, not per-call.** A hold is placed before ``kickoff()``
8
+ covering the whole crew, and settled once against the reported totals. Budget
9
+ enforcement is therefore at run granularity: AgentGov stops the *next* run, not
10
+ the middle of this one. For mid-run enforcement, govern the underlying model
11
+ too — the LangChain adapter or :func:`~agentgov.proxy.govern` on the client
12
+ gives per-call control, and the two compose.
13
+
14
+ **Usage metrics are cumulative.** CrewAI accumulates ``token_usage`` across
15
+ every ``kickoff()`` on the same crew object. Settling the reported total each
16
+ time would bill the first run again on the second, and again on the third.
17
+ :class:`GovernedCrew` tracks what it has already settled and charges only the
18
+ delta — a detail that is easy to miss and expensive to get wrong.
19
+
20
+ Nothing here imports CrewAI; every object is duck-typed and tested with fakes.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from collections.abc import Callable, Mapping, Sequence
27
+ from dataclasses import dataclass
28
+ from decimal import Decimal
29
+ from typing import Any, TypeVar
30
+
31
+ from agentgov.core import BudgetManager
32
+ from agentgov.exceptions import CircuitBreakerError
33
+ from agentgov.interceptor import Interceptor, TokenUsage
34
+ from agentgov.reconciliation import MeteringJournal
35
+
36
+ __all__ = [
37
+ "CrewHalted",
38
+ "GovernedCrew",
39
+ "extract_crew_usage",
40
+ "govern_agent",
41
+ "govern_crew",
42
+ ]
43
+
44
+ logger = logging.getLogger("agentgov.adapters.crewai")
45
+
46
+ R = TypeVar("R")
47
+
48
+ _USAGE_CONTAINERS = ("token_usage", "usage_metrics", "usage")
49
+ _INPUT_ALIASES = ("prompt_tokens", "input_tokens")
50
+ _OUTPUT_ALIASES = ("completion_tokens", "output_tokens")
51
+ _CACHED_ALIASES = ("cached_prompt_tokens", "cache_read_input_tokens")
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class CrewHalted:
56
+ """Returned in place of a result when a halted crew exits without raising.
57
+
58
+ When :class:`GovernedCrew` is built with ``raise_on_halt=False``, a
59
+ breaker trip mid-crew returns this instead of propagating, so an
60
+ orchestrator can record a failed task and continue without unwinding the
61
+ rest of the crew's state.
62
+
63
+ :ivar scope_id: The scope that was halted.
64
+ :ivar reason: Why the breaker tripped.
65
+ :ivar error: The original exception, for logging or re-raising.
66
+ :ivar settled_cost: What the run had already spent when it was stopped.
67
+ """
68
+
69
+ scope_id: str
70
+ reason: str
71
+ error: CircuitBreakerError
72
+ settled_cost: Decimal = Decimal(0)
73
+
74
+ def __bool__(self) -> bool:
75
+ """Falsey, so ``if not result:`` reads correctly at a call site."""
76
+ return False
77
+
78
+
79
+ def _read_int(source: object, aliases: Sequence[str]) -> int:
80
+ """Pull the first present integer among ``aliases`` off a dict or object."""
81
+ for alias in aliases:
82
+ value = source.get(alias) if isinstance(source, Mapping) else getattr(source, alias, None)
83
+ if isinstance(value, bool):
84
+ continue
85
+ if isinstance(value, int):
86
+ return value
87
+ if isinstance(value, float):
88
+ return int(value)
89
+ return 0
90
+
91
+
92
+ def extract_crew_usage(result: object) -> TokenUsage:
93
+ """Read cumulative token counts off a CrewAI result or usage object.
94
+
95
+ Accepts a ``CrewOutput`` (looking at ``token_usage``), a bare
96
+ ``UsageMetrics``, a crew object carrying ``usage_metrics``, or a plain
97
+ mapping of the same fields.
98
+
99
+ :param result: The object to read.
100
+ :returns: The token counts found; all-zero when the object reports none.
101
+ """
102
+ for container in _USAGE_CONTAINERS:
103
+ holder = (
104
+ result.get(container)
105
+ if isinstance(result, Mapping)
106
+ else getattr(result, container, None)
107
+ )
108
+ if holder is None:
109
+ continue
110
+ usage = TokenUsage(
111
+ input_tokens=_read_int(holder, _INPUT_ALIASES),
112
+ output_tokens=_read_int(holder, _OUTPUT_ALIASES),
113
+ cache_read_input_tokens=_read_int(holder, _CACHED_ALIASES),
114
+ )
115
+ if usage.total_tokens:
116
+ return usage
117
+
118
+ direct = TokenUsage(
119
+ input_tokens=_read_int(result, _INPUT_ALIASES),
120
+ output_tokens=_read_int(result, _OUTPUT_ALIASES),
121
+ cache_read_input_tokens=_read_int(result, _CACHED_ALIASES),
122
+ )
123
+ return direct
124
+
125
+
126
+ class GovernedCrew:
127
+ """A CrewAI crew whose runs are budgeted, metered, and settled.
128
+
129
+ The two-line drop-in::
130
+
131
+ crew = GovernedCrew(Crew(agents=[...], tasks=[...]), gov, "research-crew")
132
+ result = crew.kickoff() # unchanged call site, now governed
133
+
134
+ Attribute access falls through to the wrapped crew, so everything this
135
+ adapter does not govern keeps working unchanged.
136
+
137
+ **Graceful halting.** With the default ``raise_on_halt=True`` a breaker
138
+ trip propagates as a :class:`~agentgov.exceptions.CircuitBreakerError`.
139
+ Set it to ``False`` and :meth:`kickoff` returns a falsey
140
+ :class:`CrewHalted` instead, so a supervising orchestrator can record the
141
+ failure and continue. Either way the hold is always resolved first, so a
142
+ halted crew never strands budget.
143
+
144
+ :param crew: The CrewAI crew to wrap. Never mutated.
145
+ :param manager: The governor to enforce against.
146
+ :param scope_id: The scope this crew's runs are charged to.
147
+ :param model: Model id used to look up pricing.
148
+ :param raise_on_halt: Whether a breaker trip propagates or returns
149
+ :class:`CrewHalted`.
150
+ :param journal: Optional metering journal, for later reconciliation.
151
+ :param interceptor: An existing interceptor to use instead of building one.
152
+ :param interceptor_options: Forwarded to :class:`~agentgov.interceptor.Interceptor`.
153
+ """
154
+
155
+ __slots__ = ("_crew", "_interceptor", "_journal", "_raise_on_halt", "_settled_usage")
156
+
157
+ def __init__(
158
+ self,
159
+ crew: object,
160
+ manager: BudgetManager | None = None,
161
+ scope_id: str = "",
162
+ *,
163
+ model: str = "claude-opus-5",
164
+ raise_on_halt: bool = True,
165
+ journal: MeteringJournal | None = None,
166
+ interceptor: Interceptor | None = None,
167
+ **interceptor_options: Any,
168
+ ) -> None:
169
+ if interceptor is None:
170
+ if manager is None or not scope_id:
171
+ raise ValueError(
172
+ "provide either an interceptor, or a manager and scope_id to build one"
173
+ )
174
+ interceptor = Interceptor(manager, scope_id, model=model, **interceptor_options)
175
+ self._crew = crew
176
+ self._interceptor = interceptor
177
+ self._raise_on_halt = raise_on_halt
178
+ self._journal = journal
179
+ # CrewAI accumulates usage across kickoffs on one crew object; this is
180
+ # what has already been billed, so each run settles only its delta.
181
+ self._settled_usage = TokenUsage()
182
+
183
+ @property
184
+ def interceptor(self) -> Interceptor:
185
+ """The underlying primitive, unchanged and reachable."""
186
+ return self._interceptor
187
+
188
+ @property
189
+ def raw(self) -> object:
190
+ """The unwrapped crew."""
191
+ return self._crew
192
+
193
+ @property
194
+ def settled_usage(self) -> TokenUsage:
195
+ """Cumulative usage already billed across every run of this crew."""
196
+ return self._settled_usage
197
+
198
+ def __repr__(self) -> str:
199
+ return f"<governed {self._crew!r} scope={self._interceptor.scope_id!r}>"
200
+
201
+ def __getattr__(self, name: str) -> Any:
202
+ if name.startswith("_"):
203
+ # Without this, an attribute miss during construction or copying
204
+ # recurses forever through this same __getattr__.
205
+ raise AttributeError(name)
206
+ return getattr(self._crew, name)
207
+
208
+ def kickoff(self, *args: object, **kwargs: object) -> Any:
209
+ """Run the crew under budget enforcement and settle its true cost.
210
+
211
+ :param args: Forwarded to the crew's own ``kickoff``.
212
+ :param kwargs: Forwarded to the crew's own ``kickoff``.
213
+ :returns: The crew's own result, or :class:`CrewHalted` when the
214
+ breaker tripped and ``raise_on_halt`` is ``False``.
215
+ :raises ~agentgov.exceptions.DenialOfWalletError: If the scope cannot
216
+ afford the run.
217
+ :raises ~agentgov.exceptions.CircuitBreakerError: If the scope is
218
+ halted and ``raise_on_halt`` is ``True``.
219
+ """
220
+ return self._run(self._crew.kickoff, args, kwargs) # type: ignore[attr-defined]
221
+
222
+ async def kickoff_async(self, *args: object, **kwargs: object) -> Any:
223
+ """Async counterpart of :meth:`kickoff`."""
224
+ try:
225
+ authorization = self._authorize(args, kwargs)
226
+ except CircuitBreakerError as error:
227
+ return self._halted(error)
228
+
229
+ try:
230
+ result = await self._crew.kickoff_async(*args, **kwargs) # type: ignore[attr-defined]
231
+ except BaseException:
232
+ self._interceptor.manager.void(authorization, memo="crew run failed")
233
+ raise
234
+ return self._settle(authorization, result)
235
+
236
+ # -- internals --------------------------------------------------------
237
+
238
+ def _run(
239
+ self, call: Callable[..., Any], args: Sequence[object], kwargs: Mapping[str, object]
240
+ ) -> Any:
241
+ try:
242
+ authorization = self._authorize(args, kwargs)
243
+ except CircuitBreakerError as error:
244
+ # Halted before anything ran: nothing to unwind, nothing to bill.
245
+ return self._halted(error)
246
+
247
+ try:
248
+ result = call(*args, **kwargs)
249
+ except BaseException:
250
+ # The crew failed on its own terms. Release the encumbrance rather
251
+ # than bill for a run that produced nothing we can account for.
252
+ self._interceptor.manager.void(authorization, memo="crew run failed")
253
+ raise
254
+ return self._settle(authorization, result)
255
+
256
+ def _authorize(self, args: Sequence[object], kwargs: Mapping[str, object]) -> Any:
257
+ """Check the breakers and place a hold for one whole run."""
258
+ payload = dict(kwargs)
259
+ breaker = self._interceptor.cognitive
260
+ if breaker is not None:
261
+ breaker.observe_call(
262
+ self._interceptor.scope_id,
263
+ "crewai.kickoff",
264
+ args,
265
+ payload,
266
+ trajectory=self._interceptor.trajectory,
267
+ )
268
+ hold = self._interceptor.size_hold(args, payload)
269
+ return self._interceptor.manager.authorize(
270
+ self._interceptor.scope_id, hold, memo="crew run authorization"
271
+ )
272
+
273
+ def _settle(self, authorization: Any, result: object) -> Any:
274
+ """Bill only what this run added to the crew's cumulative totals."""
275
+ cumulative = extract_crew_usage(result)
276
+ if cumulative.total_tokens == 0:
277
+ cumulative = extract_crew_usage(self._crew)
278
+
279
+ delta = TokenUsage(
280
+ input_tokens=max(0, cumulative.input_tokens - self._settled_usage.input_tokens),
281
+ output_tokens=max(0, cumulative.output_tokens - self._settled_usage.output_tokens),
282
+ cache_read_input_tokens=max(
283
+ 0,
284
+ cumulative.cache_read_input_tokens - self._settled_usage.cache_read_input_tokens,
285
+ ),
286
+ )
287
+ cost = self._interceptor.pricing.cost_of(delta)
288
+ if cost <= 0:
289
+ logger.warning(
290
+ "crew run for scope %s reported no new usage; releasing the hold",
291
+ self._interceptor.scope_id,
292
+ )
293
+ self._interceptor.manager.void(authorization, memo="crew run reported no usage")
294
+ return result
295
+
296
+ entry = self._interceptor.manager.capture(authorization, cost, memo="crew run")
297
+ # Only advance the watermark once the settlement is committed.
298
+ self._settled_usage = cumulative
299
+ if self._journal is not None:
300
+ self._journal.record_usage(
301
+ entry.transaction_id,
302
+ self._interceptor.pricing.model_id,
303
+ delta,
304
+ self._interceptor.scope_id,
305
+ entry.timestamp,
306
+ )
307
+ return result
308
+
309
+ def _halted(self, error: CircuitBreakerError) -> Any:
310
+ """Raise or return, per policy — but never leave state half-built."""
311
+ if self._raise_on_halt:
312
+ raise error
313
+ logger.warning(
314
+ "crew for scope %s halted by the governor: %s", self._interceptor.scope_id, error
315
+ )
316
+ return CrewHalted(
317
+ scope_id=self._interceptor.scope_id,
318
+ reason=str(error),
319
+ error=error,
320
+ settled_cost=self._interceptor.pricing.cost_of(self._settled_usage),
321
+ )
322
+
323
+
324
+ def govern_crew(
325
+ crew: object, manager: BudgetManager, scope_id: str, **options: Any
326
+ ) -> GovernedCrew:
327
+ """Wrap a CrewAI crew in one call.
328
+
329
+ :param crew: The crew to govern.
330
+ :param manager: The governor to enforce against.
331
+ :param scope_id: The scope this crew's runs are charged to.
332
+ :param options: Forwarded to :class:`GovernedCrew`.
333
+ :returns: The governed crew.
334
+ """
335
+ return GovernedCrew(crew, manager, scope_id, **options)
336
+
337
+
338
+ def govern_agent(
339
+ manager: BudgetManager,
340
+ scope_id: str,
341
+ *,
342
+ model: str = "claude-opus-5",
343
+ usage_of: Callable[[object], TokenUsage] = extract_crew_usage,
344
+ journal: MeteringJournal | None = None,
345
+ interceptor: Interceptor | None = None,
346
+ **interceptor_options: Any,
347
+ ) -> Callable[[Callable[..., R]], Callable[..., R]]:
348
+ """Decorate any agent-execution function so its run is governed.
349
+
350
+ The generic escape hatch: for a framework this package has no adapter for,
351
+ wrap the function that runs one unit of agent work and tell AgentGov how to
352
+ read usage off whatever it returns::
353
+
354
+ @govern_agent(gov, "researcher", usage_of=my_framework_usage)
355
+ def run_research(topic: str) -> Report:
356
+ ...
357
+
358
+ A hold is placed before the call, the result is priced by ``usage_of``, and
359
+ the hold is settled — or voided if the function raises.
360
+
361
+ :param manager: The governor to enforce against.
362
+ :param scope_id: The scope charged for these runs.
363
+ :param model: Model id used to look up pricing.
364
+ :param usage_of: Reads token counts off the wrapped function's return value.
365
+ :param journal: Optional metering journal, for later reconciliation.
366
+ :param interceptor: An existing interceptor to use instead of building one.
367
+ :param interceptor_options: Forwarded to :class:`~agentgov.interceptor.Interceptor`.
368
+ :returns: A decorator.
369
+ """
370
+ resolved = (
371
+ interceptor
372
+ if interceptor is not None
373
+ else Interceptor(manager, scope_id, model=model, **interceptor_options)
374
+ )
375
+
376
+ def decorate(function: Callable[..., R]) -> Callable[..., R]:
377
+ def governed(*args: object, **kwargs: object) -> R:
378
+ breaker = resolved.cognitive
379
+ if breaker is not None:
380
+ breaker.observe_call(
381
+ resolved.scope_id,
382
+ getattr(function, "__name__", "agent"),
383
+ args,
384
+ kwargs,
385
+ trajectory=resolved.trajectory,
386
+ )
387
+ authorization = resolved.manager.authorize(
388
+ resolved.scope_id, resolved.size_hold(args, kwargs), memo="agent run authorization"
389
+ )
390
+ try:
391
+ result = function(*args, **kwargs)
392
+ except BaseException:
393
+ resolved.manager.void(authorization, memo="agent run failed")
394
+ raise
395
+
396
+ usage = usage_of(result)
397
+ cost = resolved.pricing.cost_of(usage)
398
+ if cost <= 0:
399
+ resolved.manager.void(authorization, memo="agent run reported no usage")
400
+ return result
401
+ entry = resolved.manager.capture(authorization, cost, memo="agent run")
402
+ if journal is not None:
403
+ journal.record_usage(
404
+ entry.transaction_id,
405
+ resolved.pricing.model_id,
406
+ usage,
407
+ resolved.scope_id,
408
+ entry.timestamp,
409
+ )
410
+ return result
411
+
412
+ governed.__name__ = getattr(function, "__name__", "governed")
413
+ governed.__doc__ = function.__doc__
414
+ governed.__wrapped__ = function # type: ignore[attr-defined]
415
+ return governed
416
+
417
+ return decorate