fifty-agent-sdk 1.1.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.
- fifty_agent_sdk/__init__.py +191 -0
- fifty_agent_sdk/audit/__init__.py +45 -0
- fifty_agent_sdk/audit/console.py +69 -0
- fifty_agent_sdk/audit/protocol.py +112 -0
- fifty_agent_sdk/audit/sql.py +328 -0
- fifty_agent_sdk/errors.py +119 -0
- fifty_agent_sdk/llm/__init__.py +30 -0
- fifty_agent_sdk/llm/openai_compat.py +345 -0
- fifty_agent_sdk/llm/protocol.py +87 -0
- fifty_agent_sdk/llm/types.py +149 -0
- fifty_agent_sdk/loop.py +836 -0
- fifty_agent_sdk/mcp/__init__.py +39 -0
- fifty_agent_sdk/mcp/client.py +622 -0
- fifty_agent_sdk/mcp/transport.py +177 -0
- fifty_agent_sdk/observability/__init__.py +18 -0
- fifty_agent_sdk/observability/hooks.py +232 -0
- fifty_agent_sdk/parser/__init__.py +32 -0
- fifty_agent_sdk/parser/base.py +118 -0
- fifty_agent_sdk/parser/json_mode.py +219 -0
- fifty_agent_sdk/parser/native_tools.py +93 -0
- fifty_agent_sdk/parser/prose_mode.py +162 -0
- fifty_agent_sdk/prompts.py +186 -0
- fifty_agent_sdk/py.typed +0 -0
- fifty_agent_sdk/runner.py +797 -0
- fifty_agent_sdk/safety.py +134 -0
- fifty_agent_sdk/state/__init__.py +53 -0
- fifty_agent_sdk/state/memory.py +156 -0
- fifty_agent_sdk/state/protocol.py +112 -0
- fifty_agent_sdk/state/redis.py +371 -0
- fifty_agent_sdk/state/sql.py +688 -0
- fifty_agent_sdk/streaming.py +273 -0
- fifty_agent_sdk/tools/__init__.py +32 -0
- fifty_agent_sdk/tools/inproc_provider.py +232 -0
- fifty_agent_sdk/tools/mcp_provider.py +345 -0
- fifty_agent_sdk/tools/protocol.py +124 -0
- fifty_agent_sdk/tools/registry.py +165 -0
- fifty_agent_sdk-1.1.0.dist-info/METADATA +225 -0
- fifty_agent_sdk-1.1.0.dist-info/RECORD +41 -0
- fifty_agent_sdk-1.1.0.dist-info/WHEEL +5 -0
- fifty_agent_sdk-1.1.0.dist-info/licenses/LICENSE +21 -0
- fifty_agent_sdk-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""fifty-agent-sdk — Production-grade reusable agent loop SDK.
|
|
2
|
+
|
|
3
|
+
Foundation layer: typed errors, Pydantic v2 message types, the
|
|
4
|
+
:class:`LLMClient` protocol, an OpenAI-compatible adapter, and a system
|
|
5
|
+
prompt template builder. Higher layers (parser, tool registry, ReACT loop,
|
|
6
|
+
state stores) build on these contracts in subsequent briefs.
|
|
7
|
+
|
|
8
|
+
Optional extra surface
|
|
9
|
+
:class:`SqlStateStore` and :data:`sql_metadata`, plus
|
|
10
|
+
:class:`SqlAuditSink` and :data:`audit_metadata` (all the ``sql``
|
|
11
|
+
extra, ``pip install 'fifty-agent-sdk[sql]'``), and :class:`RedisStateStore`
|
|
12
|
+
(the ``redis`` extra, ``pip install 'fifty-agent-sdk[redis]'``) are
|
|
13
|
+
re-exported lazily. Importing :mod:`fifty_agent_sdk` itself pulls neither
|
|
14
|
+
SQLAlchemy nor redis-py. First access to one of these symbols triggers
|
|
15
|
+
its import and raises a clear :class:`ImportError` if the relevant
|
|
16
|
+
extra is missing.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
from fifty_agent_sdk.audit import AuditEvent, AuditSink, ConsoleAuditSink
|
|
24
|
+
from fifty_agent_sdk.errors import (
|
|
25
|
+
AgentSdkError,
|
|
26
|
+
LLMError,
|
|
27
|
+
MaxIterationsExceeded,
|
|
28
|
+
MCPError,
|
|
29
|
+
ParserError,
|
|
30
|
+
StateStoreError,
|
|
31
|
+
ToolNotFound,
|
|
32
|
+
ToolTimeout,
|
|
33
|
+
)
|
|
34
|
+
from fifty_agent_sdk.llm import (
|
|
35
|
+
ChatMessage,
|
|
36
|
+
ChatRequest,
|
|
37
|
+
ChatResponse,
|
|
38
|
+
FinishReason,
|
|
39
|
+
LLMClient,
|
|
40
|
+
OpenAICompatibleClient,
|
|
41
|
+
Role,
|
|
42
|
+
ToolCall,
|
|
43
|
+
Usage,
|
|
44
|
+
)
|
|
45
|
+
from fifty_agent_sdk.loop import AgentLoop
|
|
46
|
+
from fifty_agent_sdk.mcp import MCPClient, MCPClientConfig, MCPToolDef
|
|
47
|
+
from fifty_agent_sdk.observability import Hooks
|
|
48
|
+
from fifty_agent_sdk.parser import (
|
|
49
|
+
FinalAnswer,
|
|
50
|
+
JsonModeParser,
|
|
51
|
+
NativeToolsParser,
|
|
52
|
+
NativeToolsParserStub,
|
|
53
|
+
Parser,
|
|
54
|
+
ParseResult,
|
|
55
|
+
ProseModeParser,
|
|
56
|
+
ThoughtAction,
|
|
57
|
+
)
|
|
58
|
+
from fifty_agent_sdk.prompts import (
|
|
59
|
+
JSON_MODE_OUTPUT_FORMAT,
|
|
60
|
+
PROSE_MODE_OUTPUT_FORMAT,
|
|
61
|
+
PromptSections,
|
|
62
|
+
json_mode_template,
|
|
63
|
+
prose_mode_template,
|
|
64
|
+
render_system_prompt,
|
|
65
|
+
)
|
|
66
|
+
from fifty_agent_sdk.runner import AgentRunner
|
|
67
|
+
from fifty_agent_sdk.safety import SafetyConfig
|
|
68
|
+
from fifty_agent_sdk.state import MemoryStateStore, StateStore
|
|
69
|
+
from fifty_agent_sdk.streaming import (
|
|
70
|
+
ActionEvent,
|
|
71
|
+
AgentEvent,
|
|
72
|
+
ErrorEvent,
|
|
73
|
+
FinalEvent,
|
|
74
|
+
ObservationEvent,
|
|
75
|
+
ThoughtEvent,
|
|
76
|
+
TokenEvent,
|
|
77
|
+
ToolFailedEvent,
|
|
78
|
+
ToolProgressEvent,
|
|
79
|
+
ToolStartedEvent,
|
|
80
|
+
)
|
|
81
|
+
from fifty_agent_sdk.tools import (
|
|
82
|
+
InProcProvider,
|
|
83
|
+
MCPProvider,
|
|
84
|
+
RefreshSummary,
|
|
85
|
+
Registry,
|
|
86
|
+
Tool,
|
|
87
|
+
ToolResult,
|
|
88
|
+
ToolSchema,
|
|
89
|
+
tool,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if TYPE_CHECKING:
|
|
93
|
+
from fifty_agent_sdk.audit.sql import SqlAuditSink, audit_metadata
|
|
94
|
+
from fifty_agent_sdk.state.redis import RedisStateStore
|
|
95
|
+
from fifty_agent_sdk.state.sql import SqlStateStore, sql_metadata
|
|
96
|
+
|
|
97
|
+
__version__ = "1.0.0"
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"JSON_MODE_OUTPUT_FORMAT",
|
|
101
|
+
"PROSE_MODE_OUTPUT_FORMAT",
|
|
102
|
+
"ActionEvent",
|
|
103
|
+
"AgentEvent",
|
|
104
|
+
"AgentLoop",
|
|
105
|
+
"AgentRunner",
|
|
106
|
+
"AgentSdkError",
|
|
107
|
+
"AuditEvent",
|
|
108
|
+
"AuditSink",
|
|
109
|
+
"ChatMessage",
|
|
110
|
+
"ChatRequest",
|
|
111
|
+
"ChatResponse",
|
|
112
|
+
"ConsoleAuditSink",
|
|
113
|
+
"ErrorEvent",
|
|
114
|
+
"FinalAnswer",
|
|
115
|
+
"FinalEvent",
|
|
116
|
+
"FinishReason",
|
|
117
|
+
"Hooks",
|
|
118
|
+
"InProcProvider",
|
|
119
|
+
"JsonModeParser",
|
|
120
|
+
"LLMClient",
|
|
121
|
+
"LLMError",
|
|
122
|
+
"MCPClient",
|
|
123
|
+
"MCPClientConfig",
|
|
124
|
+
"MCPError",
|
|
125
|
+
"MCPProvider",
|
|
126
|
+
"MCPToolDef",
|
|
127
|
+
"MaxIterationsExceeded",
|
|
128
|
+
"MemoryStateStore",
|
|
129
|
+
"NativeToolsParser",
|
|
130
|
+
"NativeToolsParserStub",
|
|
131
|
+
"ObservationEvent",
|
|
132
|
+
"OpenAICompatibleClient",
|
|
133
|
+
"ParseResult",
|
|
134
|
+
"Parser",
|
|
135
|
+
"ParserError",
|
|
136
|
+
"PromptSections",
|
|
137
|
+
"ProseModeParser",
|
|
138
|
+
"RedisStateStore",
|
|
139
|
+
"RefreshSummary",
|
|
140
|
+
"Registry",
|
|
141
|
+
"Role",
|
|
142
|
+
"SafetyConfig",
|
|
143
|
+
"SqlAuditSink",
|
|
144
|
+
"SqlStateStore",
|
|
145
|
+
"StateStore",
|
|
146
|
+
"StateStoreError",
|
|
147
|
+
"ThoughtAction",
|
|
148
|
+
"ThoughtEvent",
|
|
149
|
+
"TokenEvent",
|
|
150
|
+
"Tool",
|
|
151
|
+
"ToolCall",
|
|
152
|
+
"ToolFailedEvent",
|
|
153
|
+
"ToolNotFound",
|
|
154
|
+
"ToolProgressEvent",
|
|
155
|
+
"ToolResult",
|
|
156
|
+
"ToolSchema",
|
|
157
|
+
"ToolStartedEvent",
|
|
158
|
+
"ToolTimeout",
|
|
159
|
+
"Usage",
|
|
160
|
+
"audit_metadata",
|
|
161
|
+
"json_mode_template",
|
|
162
|
+
"prose_mode_template",
|
|
163
|
+
"render_system_prompt",
|
|
164
|
+
"sql_metadata",
|
|
165
|
+
"tool",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def __getattr__(name: str) -> Any:
|
|
170
|
+
"""Lazily import optional-extra surface symbols on first access.
|
|
171
|
+
|
|
172
|
+
``SqlStateStore`` / ``sql_metadata`` and ``SqlAuditSink`` /
|
|
173
|
+
``audit_metadata`` require the optional ``sql`` extra;
|
|
174
|
+
``RedisStateStore`` requires the ``redis`` extra. Routing access
|
|
175
|
+
through this hook keeps ``import fifty_agent_sdk`` free of SQLAlchemy and
|
|
176
|
+
redis-py, and surfaces a clear :class:`ImportError` only when the
|
|
177
|
+
symbols are actually used without the extra installed.
|
|
178
|
+
"""
|
|
179
|
+
if name in {"SqlStateStore", "sql_metadata"}:
|
|
180
|
+
from fifty_agent_sdk.state import sql
|
|
181
|
+
|
|
182
|
+
return getattr(sql, name)
|
|
183
|
+
if name in {"SqlAuditSink", "audit_metadata"}:
|
|
184
|
+
from fifty_agent_sdk.audit import sql as audit_sql
|
|
185
|
+
|
|
186
|
+
return getattr(audit_sql, name)
|
|
187
|
+
if name == "RedisStateStore":
|
|
188
|
+
from fifty_agent_sdk.state import redis
|
|
189
|
+
|
|
190
|
+
return redis.RedisStateStore
|
|
191
|
+
raise AttributeError(f"module 'fifty_agent_sdk' has no attribute {name!r}")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Audit subpackage: pluggable, tamper-evident action provenance.
|
|
2
|
+
|
|
3
|
+
Re-exports the :class:`AuditEvent` model, the :class:`AuditSink` protocol,
|
|
4
|
+
and the dependency-free :class:`ConsoleAuditSink`. The durable SQL backend
|
|
5
|
+
(:class:`SqlAuditSink`) with its Alembic :data:`audit_metadata` symbol is
|
|
6
|
+
re-exported lazily via a module-level ``__getattr__`` — it requires the
|
|
7
|
+
optional ``sql`` extra, so ``import fifty_agent_sdk.audit`` itself does NOT pull
|
|
8
|
+
SQLAlchemy.
|
|
9
|
+
|
|
10
|
+
Accessing :data:`SqlAuditSink` or :data:`audit_metadata` without
|
|
11
|
+
``fifty-agent-sdk[sql]`` installed raises a clear :class:`ImportError` referencing
|
|
12
|
+
the extras line.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from fifty_agent_sdk.audit.console import ConsoleAuditSink
|
|
20
|
+
from fifty_agent_sdk.audit.protocol import AuditEvent, AuditSink
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from fifty_agent_sdk.audit.sql import SqlAuditSink, audit_metadata
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AuditEvent",
|
|
27
|
+
"AuditSink",
|
|
28
|
+
"ConsoleAuditSink",
|
|
29
|
+
"SqlAuditSink",
|
|
30
|
+
"audit_metadata",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __getattr__(name: str) -> Any:
|
|
35
|
+
"""Lazily import optional-extra surface symbols on first access.
|
|
36
|
+
|
|
37
|
+
Keeps the package's eager import surface free of SQLAlchemy. When the
|
|
38
|
+
``sql`` extra is not installed, importing the backing module (triggered
|
|
39
|
+
here) raises a documented :class:`ImportError`.
|
|
40
|
+
"""
|
|
41
|
+
if name in {"SqlAuditSink", "audit_metadata"}:
|
|
42
|
+
from fifty_agent_sdk.audit import sql
|
|
43
|
+
|
|
44
|
+
return getattr(sql, name)
|
|
45
|
+
raise AttributeError(f"module 'fifty_agent_sdk.audit' has no attribute {name!r}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""ConsoleAuditSink — structured-log audit sink for dev / single-tenant use.
|
|
2
|
+
|
|
3
|
+
:class:`ConsoleAuditSink` is the zero-dependency :class:`AuditSink`
|
|
4
|
+
implementation. It emits each :class:`AuditEvent` as one ``INFO`` structlog
|
|
5
|
+
record under the fixed logger name ``fifty_agent_sdk.audit``.
|
|
6
|
+
|
|
7
|
+
It is the right choice when audit logs are already centralized (a single
|
|
8
|
+
deployment shipping structlog output to a log aggregator) and a separate
|
|
9
|
+
audit database is not warranted. It is NOT tamper-evident on its own — the
|
|
10
|
+
durability and integrity guarantees are whatever the surrounding log
|
|
11
|
+
pipeline provides. For a durable, queryable trail use
|
|
12
|
+
:class:`fifty_agent_sdk.audit.sql.SqlAuditSink`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Final
|
|
18
|
+
|
|
19
|
+
import structlog
|
|
20
|
+
|
|
21
|
+
from fifty_agent_sdk.audit.protocol import AuditEvent
|
|
22
|
+
|
|
23
|
+
_log: Final = structlog.get_logger("fifty_agent_sdk.audit")
|
|
24
|
+
"""Module-level structured logger.
|
|
25
|
+
|
|
26
|
+
Bound to the fixed name ``fifty_agent_sdk.audit`` (NOT ``__name__``) so every
|
|
27
|
+
audit record — from this sink and from :mod:`fifty_agent_sdk.audit.sql` — shares
|
|
28
|
+
one logger name a consumer can route or filter on.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConsoleAuditSink:
|
|
33
|
+
"""An :class:`AuditSink` that writes each event to structlog at ``INFO``.
|
|
34
|
+
|
|
35
|
+
Satisfies :class:`fifty_agent_sdk.audit.protocol.AuditSink` structurally (no
|
|
36
|
+
explicit inheritance needed thanks to ``@runtime_checkable``).
|
|
37
|
+
|
|
38
|
+
No constructor arguments — the sink is stateless and safe to share
|
|
39
|
+
across concurrent runs (structlog loggers are themselves thread- and
|
|
40
|
+
task-safe).
|
|
41
|
+
|
|
42
|
+
Caution:
|
|
43
|
+
Not tamper-evident on its own. The integrity of the trail is only
|
|
44
|
+
as strong as the log pipeline the records are shipped through. Use
|
|
45
|
+
:class:`fifty_agent_sdk.audit.sql.SqlAuditSink` when a durable, queryable,
|
|
46
|
+
append-only record is required.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
async def record(self, event: AuditEvent) -> None:
|
|
50
|
+
"""Emit ``event`` as a single ``INFO`` structlog record.
|
|
51
|
+
|
|
52
|
+
The record's event name is ``audit.event``; the
|
|
53
|
+
:class:`AuditEvent` fields are spread as structured key/values
|
|
54
|
+
(``timestamp`` rendered as an ISO-8601 string).
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
event: The :class:`AuditEvent` to log.
|
|
58
|
+
"""
|
|
59
|
+
_log.info(
|
|
60
|
+
"audit.event",
|
|
61
|
+
session_id=event.session_id,
|
|
62
|
+
user_id=event.user_id,
|
|
63
|
+
event_type=event.event_type,
|
|
64
|
+
timestamp=event.timestamp.isoformat(),
|
|
65
|
+
payload=event.payload,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["ConsoleAuditSink"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""AuditSink protocol — pluggable, tamper-evident action provenance.
|
|
2
|
+
|
|
3
|
+
The :class:`AuditSink` protocol is the SDK's abstraction for capturing a
|
|
4
|
+
durable record of every agent action. :class:`fifty_agent_sdk.runner.AgentRunner`
|
|
5
|
+
emits an :class:`AuditEvent` at four points of every ``run()``: session
|
|
6
|
+
start, each tool invocation, the final answer, and any error.
|
|
7
|
+
|
|
8
|
+
Implementations may write to structured logs (see
|
|
9
|
+
:class:`fifty_agent_sdk.audit.console.ConsoleAuditSink` — the dev default), a SQL
|
|
10
|
+
table (:class:`fifty_agent_sdk.audit.sql.SqlAuditSink`), or any other backend.
|
|
11
|
+
|
|
12
|
+
Contract invariants
|
|
13
|
+
* **Ordering.** :meth:`AuditSink.record` is called in the order events
|
|
14
|
+
occur within a run. Implementations MUST NOT reorder events; the
|
|
15
|
+
append-only nature of the record is a load-bearing property for any
|
|
16
|
+
consumer treating it as a compliance trail.
|
|
17
|
+
* **Concurrency.** :meth:`AuditSink.record` MUST be safe to call from
|
|
18
|
+
multiple concurrent runs sharing one sink instance. Implementations
|
|
19
|
+
that serialize do so internally; the caller does not lock.
|
|
20
|
+
* **Failure is the caller's to swallow.** A raising :meth:`record` is
|
|
21
|
+
tolerated by :class:`AgentRunner`, which catches the exception, logs a
|
|
22
|
+
``WARNING``, and continues — an audit-sink outage MUST NOT abort a
|
|
23
|
+
live agent run. Implementations SHOULD still raise on backend failure
|
|
24
|
+
(rather than silently dropping) so the gap is visible in the Runner's
|
|
25
|
+
``audit.emit_failed`` log line.
|
|
26
|
+
* **Tamper-evidence is the sink's job.** "Tamper-evident" is a property
|
|
27
|
+
of the *sink's storage* (append-only table, hash-chaining, WORM
|
|
28
|
+
bucket), not of the emission path. The emission path's contract is
|
|
29
|
+
best-effort delivery plus a loud warning when delivery fails.
|
|
30
|
+
* **No secrets in the payload.** Consumers and tools MUST NOT place
|
|
31
|
+
credentials or raw secrets into :attr:`AuditEvent.payload`; the
|
|
32
|
+
payload is persisted verbatim and may appear in logs.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from typing import Any, Protocol, runtime_checkable
|
|
39
|
+
|
|
40
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuditEvent(BaseModel):
|
|
44
|
+
"""A single audited agent action.
|
|
45
|
+
|
|
46
|
+
The provider-agnostic shape passed to every :class:`AuditSink`. Like the
|
|
47
|
+
other SDK models it sets ``extra="forbid"`` so a typo'd field is caught
|
|
48
|
+
at construction rather than silently dropped.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
session_id: Opaque session identifier the action belongs to.
|
|
52
|
+
user_id: Optional end-user identifier. The Runner does not have a
|
|
53
|
+
user-id channel today and always emits ``None``; consumers
|
|
54
|
+
wanting per-user attribution set it via a wrapping sink.
|
|
55
|
+
timestamp: When the action occurred (timezone-aware UTC). The Runner
|
|
56
|
+
stamps this at emission time — it is the *action* time, not the
|
|
57
|
+
time the row was persisted.
|
|
58
|
+
event_type: Short tag for the kind of action. The four values the
|
|
59
|
+
Runner emits are ``"session_start"``, ``"tool_invocation"``,
|
|
60
|
+
``"final_answer"``, and ``"error"``; the field is a free-form
|
|
61
|
+
string (not a ``Literal``) so consumers can emit their own
|
|
62
|
+
event types through the same sink.
|
|
63
|
+
payload: Structured, event-specific detail. Always a real ``dict``
|
|
64
|
+
(never ``None``); defaults to ``{}``. MUST NOT contain
|
|
65
|
+
credentials or raw secrets — it is persisted and logged
|
|
66
|
+
verbatim.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(extra="forbid")
|
|
70
|
+
|
|
71
|
+
session_id: str
|
|
72
|
+
user_id: str | None = None
|
|
73
|
+
timestamp: datetime
|
|
74
|
+
event_type: str
|
|
75
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@runtime_checkable
|
|
79
|
+
class AuditSink(Protocol):
|
|
80
|
+
"""Pluggable audit-trail backend.
|
|
81
|
+
|
|
82
|
+
See the module docstring for the full contract. Implementations are
|
|
83
|
+
duck-typed: any class providing the single async method below with a
|
|
84
|
+
matching signature satisfies :func:`isinstance` against
|
|
85
|
+
:class:`AuditSink` thanks to ``@runtime_checkable``.
|
|
86
|
+
|
|
87
|
+
Note:
|
|
88
|
+
``@runtime_checkable`` :class:`Protocol` instances only check for
|
|
89
|
+
method *presence*, not signature compatibility. Mypy ``--strict``
|
|
90
|
+
catches signature mismatches at type-check time.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
async def record(self, event: AuditEvent) -> None:
|
|
94
|
+
"""Persist a single :class:`AuditEvent`.
|
|
95
|
+
|
|
96
|
+
Called once per audited action, in occurrence order. Implementations
|
|
97
|
+
MUST NOT reorder events and MUST be safe to call concurrently from
|
|
98
|
+
multiple runs sharing the sink.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
event: The :class:`AuditEvent` to record.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
Exception: Implementations MAY raise on backend failure. The
|
|
105
|
+
:class:`fifty_agent_sdk.runner.AgentRunner` catches the exception,
|
|
106
|
+
logs a ``WARNING``, and continues — a sink failure never
|
|
107
|
+
aborts an agent run.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = ["AuditEvent", "AuditSink"]
|