monoid-agent-kernel 0.16.1__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.
Files changed (191) hide show
  1. monoid_agent_kernel/__init__.py +11 -0
  2. monoid_agent_kernel/_policy_util.py +40 -0
  3. monoid_agent_kernel/_proc.py +86 -0
  4. monoid_agent_kernel/builder.py +351 -0
  5. monoid_agent_kernel/cli.py +1244 -0
  6. monoid_agent_kernel/conformance/__init__.py +32 -0
  7. monoid_agent_kernel/conformance/harness.py +209 -0
  8. monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
  9. monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
  10. monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
  11. monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
  12. monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
  13. monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
  14. monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
  15. monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
  16. monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
  17. monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
  18. monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
  19. monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
  20. monoid_agent_kernel/contracts.py +329 -0
  21. monoid_agent_kernel/core/__init__.py +1 -0
  22. monoid_agent_kernel/core/_util.py +145 -0
  23. monoid_agent_kernel/core/agents.py +883 -0
  24. monoid_agent_kernel/core/cancellation.py +16 -0
  25. monoid_agent_kernel/core/capability.py +376 -0
  26. monoid_agent_kernel/core/capability_revocation.py +92 -0
  27. monoid_agent_kernel/core/checkpoint.py +350 -0
  28. monoid_agent_kernel/core/content.py +101 -0
  29. monoid_agent_kernel/core/context.py +67 -0
  30. monoid_agent_kernel/core/control.py +124 -0
  31. monoid_agent_kernel/core/control_audit.py +99 -0
  32. monoid_agent_kernel/core/durable_metadata.py +116 -0
  33. monoid_agent_kernel/core/event_sequencing.py +164 -0
  34. monoid_agent_kernel/core/events.py +204 -0
  35. monoid_agent_kernel/core/external_agent_envelope.py +338 -0
  36. monoid_agent_kernel/core/frontmatter.py +140 -0
  37. monoid_agent_kernel/core/inbox.py +110 -0
  38. monoid_agent_kernel/core/lease_admission.py +57 -0
  39. monoid_agent_kernel/core/lifecycle.py +440 -0
  40. monoid_agent_kernel/core/manifest.py +88 -0
  41. monoid_agent_kernel/core/media.py +393 -0
  42. monoid_agent_kernel/core/outbox.py +212 -0
  43. monoid_agent_kernel/core/output_validator.py +103 -0
  44. monoid_agent_kernel/core/packages.py +742 -0
  45. monoid_agent_kernel/core/projections.py +213 -0
  46. monoid_agent_kernel/core/prompt.py +37 -0
  47. monoid_agent_kernel/core/proposal_file.py +84 -0
  48. monoid_agent_kernel/core/result.py +131 -0
  49. monoid_agent_kernel/core/schemas.py +1146 -0
  50. monoid_agent_kernel/core/scope.py +185 -0
  51. monoid_agent_kernel/core/side_effect_policy.py +153 -0
  52. monoid_agent_kernel/core/spec.py +325 -0
  53. monoid_agent_kernel/core/streaming.py +208 -0
  54. monoid_agent_kernel/core/subagent_runtime.py +187 -0
  55. monoid_agent_kernel/core/tool_approval.py +175 -0
  56. monoid_agent_kernel/core/tool_surface.py +505 -0
  57. monoid_agent_kernel/core/trace_context.py +76 -0
  58. monoid_agent_kernel/core/wire_validation.py +156 -0
  59. monoid_agent_kernel/core/workspace.py +161 -0
  60. monoid_agent_kernel/core/workspace_index.py +127 -0
  61. monoid_agent_kernel/env.py +32 -0
  62. monoid_agent_kernel/errors.py +105 -0
  63. monoid_agent_kernel/event_loader.py +47 -0
  64. monoid_agent_kernel/identifiers.py +50 -0
  65. monoid_agent_kernel/loop.py +4140 -0
  66. monoid_agent_kernel/loop_phases.py +561 -0
  67. monoid_agent_kernel/mcp/__init__.py +10 -0
  68. monoid_agent_kernel/mcp/client.py +227 -0
  69. monoid_agent_kernel/mcp/provider.py +407 -0
  70. monoid_agent_kernel/narration.py +92 -0
  71. monoid_agent_kernel/observability/__init__.py +9 -0
  72. monoid_agent_kernel/observability/otel.py +264 -0
  73. monoid_agent_kernel/permissions.py +74 -0
  74. monoid_agent_kernel/providers/__init__.py +7 -0
  75. monoid_agent_kernel/providers/_common.py +122 -0
  76. monoid_agent_kernel/providers/base.py +312 -0
  77. monoid_agent_kernel/providers/fake.py +76 -0
  78. monoid_agent_kernel/providers/gateway.py +474 -0
  79. monoid_agent_kernel/providers/openai.py +487 -0
  80. monoid_agent_kernel/public_view.py +173 -0
  81. monoid_agent_kernel/py.typed +0 -0
  82. monoid_agent_kernel/recorder.py +421 -0
  83. monoid_agent_kernel/reference/__init__.py +15 -0
  84. monoid_agent_kernel/reference/_shared/__init__.py +4 -0
  85. monoid_agent_kernel/reference/_shared/http_util.py +111 -0
  86. monoid_agent_kernel/reference/_shared/tokens.py +314 -0
  87. monoid_agent_kernel/reference/backend/__init__.py +21 -0
  88. monoid_agent_kernel/reference/backend/commands.py +375 -0
  89. monoid_agent_kernel/reference/backend/http.py +439 -0
  90. monoid_agent_kernel/reference/backend/jobs.py +68 -0
  91. monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
  92. monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
  93. monoid_agent_kernel/reference/backend/ports.py +188 -0
  94. monoid_agent_kernel/reference/backend/projection.py +286 -0
  95. monoid_agent_kernel/reference/backend/proposal.py +220 -0
  96. monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
  97. monoid_agent_kernel/reference/backend/recovery.py +253 -0
  98. monoid_agent_kernel/reference/backend/run_execution.py +152 -0
  99. monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
  100. monoid_agent_kernel/reference/backend/run_state.py +243 -0
  101. monoid_agent_kernel/reference/backend/run_types.py +128 -0
  102. monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
  103. monoid_agent_kernel/reference/backend/service.py +1664 -0
  104. monoid_agent_kernel/reference/backend/session.py +280 -0
  105. monoid_agent_kernel/reference/backend/session_drive.py +231 -0
  106. monoid_agent_kernel/reference/capability.py +101 -0
  107. monoid_agent_kernel/reference/conformance.py +1777 -0
  108. monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
  109. monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
  110. monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
  111. monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
  112. monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
  113. monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
  114. monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
  115. monoid_agent_kernel/reference/outbox.py +135 -0
  116. monoid_agent_kernel/reference/stores/__init__.py +20 -0
  117. monoid_agent_kernel/reference/stores/lease.py +116 -0
  118. monoid_agent_kernel/reference/stores/sqlite.py +295 -0
  119. monoid_agent_kernel/reference/studio/README.md +97 -0
  120. monoid_agent_kernel/reference/studio/__init__.py +21 -0
  121. monoid_agent_kernel/reference/studio/activity.py +53 -0
  122. monoid_agent_kernel/reference/studio/cli.py +481 -0
  123. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
  124. monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
  125. monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
  126. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
  127. monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
  128. monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
  129. monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
  130. monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
  131. monoid_agent_kernel/reference/studio/server.py +1922 -0
  132. monoid_agent_kernel/reference/studio/web/index.html +1877 -0
  133. monoid_agent_kernel/reference/studio/web/settings.html +108 -0
  134. monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
  135. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  136. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  137. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  138. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  139. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  140. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  141. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  142. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  143. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  144. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  145. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  146. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  147. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  148. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  149. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  150. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  151. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  152. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  153. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  154. monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  155. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
  156. monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
  157. monoid_agent_kernel/reference/studio/window.py +72 -0
  158. monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
  159. monoid_agent_kernel/reference/web_gateway/http.py +155 -0
  160. monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
  161. monoid_agent_kernel/reference/web_gateway/service.py +559 -0
  162. monoid_agent_kernel/shell.py +722 -0
  163. monoid_agent_kernel/skills/__init__.py +20 -0
  164. monoid_agent_kernel/skills/definition.py +82 -0
  165. monoid_agent_kernel/skills/loader.py +55 -0
  166. monoid_agent_kernel/skills/provider.py +378 -0
  167. monoid_agent_kernel/subagent_loader.py +56 -0
  168. monoid_agent_kernel/tasks.py +1326 -0
  169. monoid_agent_kernel/tool_loader.py +51 -0
  170. monoid_agent_kernel/tool_services/__init__.py +8 -0
  171. monoid_agent_kernel/tool_services/base.py +25 -0
  172. monoid_agent_kernel/tool_services/jobs.py +47 -0
  173. monoid_agent_kernel/tool_services/shell.py +255 -0
  174. monoid_agent_kernel/tool_services/web.py +356 -0
  175. monoid_agent_kernel/tools/__init__.py +2 -0
  176. monoid_agent_kernel/tools/base.py +205 -0
  177. monoid_agent_kernel/tools/builtin.py +958 -0
  178. monoid_agent_kernel/tools/decorator.py +132 -0
  179. monoid_agent_kernel/tools/tool_ids.py +62 -0
  180. monoid_agent_kernel/web.py +171 -0
  181. monoid_agent_kernel/workspace/__init__.py +2 -0
  182. monoid_agent_kernel/workspace/local.py +1004 -0
  183. monoid_agent_kernel/workspace/paths.py +30 -0
  184. monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
  185. monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
  186. monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
  187. monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
  188. monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
  189. monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
  190. native_agent_runner/__init__.py +103 -0
  191. native_agent_runner/py.typed +0 -0
@@ -0,0 +1,14 @@
1
+ """Reference implementation of the Monoid Agent Kernel LLM gateway contract.
2
+
3
+ Provided as an example credential-boundary gateway. Real integrators are expected to build
4
+ their own against ``monoid_agent_kernel.contracts`` and ``docs/CONTRACTS.md``. Not part of
5
+ the supported public surface.
6
+ """
7
+
8
+ from monoid_agent_kernel.reference.llm_gateway.service import (
9
+ LlmGatewayBackend,
10
+ LlmGatewayTurnRequest,
11
+ LlmGatewayUsage,
12
+ )
13
+
14
+ __all__ = ["LlmGatewayBackend", "LlmGatewayTurnRequest", "LlmGatewayUsage"]
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Iterable
6
+ from http import HTTPStatus
7
+ from http.server import BaseHTTPRequestHandler
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ from monoid_agent_kernel.errors import ModelAdapterError, NativeAgentError, PermissionDenied
12
+ from monoid_agent_kernel.reference._shared.http_util import (
13
+ HardenedThreadingHTTPServer,
14
+ HttpRequestTooLarge,
15
+ log_http_request,
16
+ read_json_limited,
17
+ redact_internal_error,
18
+ )
19
+ from monoid_agent_kernel.reference.llm_gateway.service import LlmGatewayBackend
20
+ from monoid_agent_kernel.providers.gateway import (
21
+ GATEWAY_AUTH_ERROR,
22
+ GATEWAY_BAD_REQUEST,
23
+ GATEWAY_BAD_RESPONSE,
24
+ GATEWAY_SERVER_ERROR,
25
+ )
26
+
27
+ _LOGGER = logging.getLogger("monoid_agent_kernel.llm_gateway.http")
28
+
29
+
30
+ def make_llm_gateway_handler(
31
+ gateway: LlmGatewayBackend,
32
+ *,
33
+ admin_token: str | None,
34
+ ) -> type[BaseHTTPRequestHandler]:
35
+ class LlmGatewayHttpHandler(BaseHTTPRequestHandler):
36
+ server_version = "MonoidLlmGateway/0.2"
37
+
38
+ def do_GET(self) -> None: # noqa: N802
39
+ parsed = urlparse(self.path)
40
+ try:
41
+ if parsed.path == "/healthz":
42
+ self._write_json({"ok": True})
43
+ return
44
+ parts = [part for part in parsed.path.split("/") if part]
45
+ if len(parts) == 5 and parts[:3] == ["internal", "llm", "tenants"] and parts[4] == "usage":
46
+ self._require_admin()
47
+ self._write_json(gateway.tenant_usage(parts[3]))
48
+ return
49
+ self._write_error(HTTPStatus.NOT_FOUND, "not found")
50
+ except Exception as exc:
51
+ self._write_exception(exc)
52
+
53
+ def do_POST(self) -> None: # noqa: N802
54
+ parsed = urlparse(self.path)
55
+ try:
56
+ if parsed.path == "/internal/llm/turns":
57
+ self._write_json(gateway.handle_turn(self._bearer_token(), self._read_json()))
58
+ return
59
+ if parsed.path == "/internal/llm/turns/stream":
60
+ # Auth/parse/build run eagerly inside handle_turn_stream and may raise
61
+ # here, before any SSE byte — those map to a normal error response below.
62
+ frames = gateway.handle_turn_stream(self._bearer_token(), self._read_json())
63
+ self._write_sse(frames)
64
+ return
65
+ self._write_error(HTTPStatus.NOT_FOUND, "not found")
66
+ except Exception as exc:
67
+ self._write_exception(exc)
68
+
69
+ def log_request(self, code: Any = "-", size: Any = "-") -> None: # noqa: ARG002
70
+ log_http_request(_LOGGER, self, code)
71
+
72
+ def log_message(self, _format: str, *_args: Any) -> None:
73
+ return None
74
+
75
+ def _read_json(self) -> dict[str, Any]:
76
+ return read_json_limited(self)
77
+
78
+ def _bearer_token(self) -> str:
79
+ header = self.headers.get("Authorization") or ""
80
+ prefix = "Bearer "
81
+ if not header.startswith(prefix):
82
+ raise PermissionDenied("missing bearer token")
83
+ return header[len(prefix) :].strip()
84
+
85
+ def _require_admin(self) -> None:
86
+ if admin_token is None:
87
+ raise PermissionDenied("admin token is not configured")
88
+ if self._bearer_token() != admin_token:
89
+ raise PermissionDenied("invalid admin token")
90
+
91
+ def _write_exception(self, exc: Exception) -> None:
92
+ if isinstance(exc, PermissionDenied):
93
+ self._write_error(
94
+ HTTPStatus.UNAUTHORIZED,
95
+ str(exc),
96
+ error_code=GATEWAY_AUTH_ERROR,
97
+ retryable=False,
98
+ )
99
+ elif isinstance(exc, ModelAdapterError):
100
+ status = _model_error_status(exc)
101
+ self._write_error(
102
+ status,
103
+ str(exc),
104
+ error_code=exc.provider_error_code or GATEWAY_BAD_RESPONSE,
105
+ retryable=exc.retryable,
106
+ )
107
+ elif isinstance(exc, HttpRequestTooLarge):
108
+ self._write_error(
109
+ HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
110
+ str(exc),
111
+ error_code=GATEWAY_BAD_REQUEST,
112
+ retryable=False,
113
+ )
114
+ elif isinstance(exc, ValueError):
115
+ self._write_error(
116
+ HTTPStatus.BAD_REQUEST,
117
+ str(exc),
118
+ error_code=GATEWAY_BAD_REQUEST,
119
+ retryable=False,
120
+ )
121
+ elif isinstance(exc, NativeAgentError):
122
+ self._write_error(
123
+ HTTPStatus.BAD_REQUEST,
124
+ str(exc),
125
+ error_code=getattr(exc, "error_code", GATEWAY_BAD_REQUEST),
126
+ retryable=False,
127
+ )
128
+ else:
129
+ self._write_error(
130
+ HTTPStatus.INTERNAL_SERVER_ERROR,
131
+ redact_internal_error(_LOGGER, self, exc),
132
+ error_code=GATEWAY_SERVER_ERROR,
133
+ retryable=True,
134
+ )
135
+
136
+ def _write_error(
137
+ self,
138
+ status: HTTPStatus,
139
+ message: str,
140
+ *,
141
+ error_code: str = GATEWAY_BAD_RESPONSE,
142
+ retryable: bool = False,
143
+ ) -> None:
144
+ self._write_json(
145
+ {
146
+ "error": message,
147
+ "error_code": error_code,
148
+ "retryable": retryable,
149
+ "http_status": int(status),
150
+ },
151
+ status=status,
152
+ )
153
+
154
+ def _write_json(self, payload: dict[str, Any], *, status: HTTPStatus = HTTPStatus.OK) -> None:
155
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
156
+ self.send_response(int(status))
157
+ self.send_header("Content-Type", "application/json; charset=utf-8")
158
+ self.send_header("Content-Length", str(len(body)))
159
+ self.end_headers()
160
+ self.wfile.write(body)
161
+
162
+ def _write_sse(self, frames: Iterable[dict[str, Any]]) -> None:
163
+ # A streaming turn is expected to be long-lived, so clear this route's 30s socket
164
+ # timeout (which would otherwise kill the connection on a >30s gap between tokens).
165
+ # The provider call's own timeout still bounds a wedged upstream. Other routes keep
166
+ # the default. No Content-Length: the body is unbounded and ends on connection close.
167
+ self.connection.settimeout(None)
168
+ self.send_response(int(HTTPStatus.OK))
169
+ self.send_header("Content-Type", "text/event-stream; charset=utf-8")
170
+ self.send_header("Cache-Control", "no-cache")
171
+ self.send_header("Connection", "close")
172
+ self.end_headers()
173
+ try:
174
+ for frame in frames:
175
+ self._write_sse_frame(frame)
176
+ except Exception as exc:
177
+ # We are already committed to a 200 SSE body, so a mid-stream failure surfaces
178
+ # as a terminal error frame (the client raises ModelAdapterError from it).
179
+ self._write_sse_frame(_stream_error_frame(self, exc))
180
+
181
+ def _write_sse_frame(self, frame: dict[str, Any]) -> None:
182
+ # Single-line JSON (no indent), flushed per frame so the stream is live.
183
+ self.wfile.write(b"data: " + json.dumps(frame, ensure_ascii=False).encode("utf-8") + b"\n\n")
184
+ self.wfile.flush()
185
+
186
+ return LlmGatewayHttpHandler
187
+
188
+
189
+ def _stream_error_frame(handler: BaseHTTPRequestHandler, exc: Exception) -> dict[str, Any]:
190
+ """Mid-stream error as an SSE frame, mirroring ``_write_error``'s fields so the client maps
191
+ it back to a ModelAdapterError identically to a non-200 response."""
192
+ if isinstance(exc, ModelAdapterError):
193
+ return {
194
+ "type": "error",
195
+ "error": str(exc),
196
+ "error_code": exc.provider_error_code or GATEWAY_BAD_RESPONSE,
197
+ "retryable": exc.retryable,
198
+ "http_status": int(_model_error_status(exc)),
199
+ }
200
+ return {
201
+ "type": "error",
202
+ "error": redact_internal_error(_LOGGER, handler, exc),
203
+ "error_code": GATEWAY_SERVER_ERROR,
204
+ "retryable": True,
205
+ "http_status": int(HTTPStatus.INTERNAL_SERVER_ERROR),
206
+ }
207
+
208
+
209
+ def _model_error_status(exc: ModelAdapterError) -> HTTPStatus:
210
+ if exc.http_status is not None and 400 <= exc.http_status <= 599:
211
+ try:
212
+ return HTTPStatus(exc.http_status)
213
+ except ValueError:
214
+ pass
215
+ return HTTPStatus.SERVICE_UNAVAILABLE if exc.retryable else HTTPStatus.BAD_GATEWAY
216
+
217
+
218
+ def create_llm_gateway_server(
219
+ gateway: LlmGatewayBackend,
220
+ *,
221
+ host: str,
222
+ port: int,
223
+ admin_token: str,
224
+ ) -> HardenedThreadingHTTPServer:
225
+ return HardenedThreadingHTTPServer((host, port), make_llm_gateway_handler(gateway, admin_token=admin_token))
@@ -0,0 +1,96 @@
1
+ """Reference LLM providers for the gateway — including a key-less offline provider.
2
+
3
+ The gateway's provider seam is a ``ProviderAdapterFactory`` (``LlmGatewayBackend
4
+ .provider_adapter_factory``). When none is supplied the gateway hard-defaults to
5
+ ``OpenAIModelAdapter``, so a from-scratch local run needs a real OpenAI key. This module is the
6
+ LLM-side counterpart of ``reference/web_gateway/providers.py`` (``FakeWebProvider`` / ``--provider
7
+ fake``): :func:`offline_provider_factory` lets the gateway answer turns with **zero credentials**,
8
+ so the whole stack — chat, streaming, multi-turn — works offline for local dev and tests.
9
+
10
+ Reference example.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ from monoid_agent_kernel.core.spec import ModelConfig
19
+ from monoid_agent_kernel.providers.base import ModelAdapter, ModelRequest, ModelTurn
20
+ from monoid_agent_kernel.reference._shared.tokens import TokenClaims
21
+
22
+
23
+ def _content_text(content: Any) -> str:
24
+ """Best-effort plain text from a message ``content`` (str or content-part list)."""
25
+ if isinstance(content, str):
26
+ return content.strip()
27
+ if isinstance(content, (list, tuple)):
28
+ parts: list[str] = []
29
+ for part in content:
30
+ if isinstance(part, str):
31
+ parts.append(part)
32
+ elif isinstance(part, dict) and isinstance(part.get("text"), str):
33
+ parts.append(part["text"])
34
+ return "\n".join(parts).strip()
35
+ return ""
36
+
37
+
38
+ def _latest_user_text(request: ModelRequest) -> str:
39
+ """The newest user message — preferring the by-value log, then the instruction."""
40
+ if request.messages:
41
+ for message in reversed(request.messages):
42
+ if message.get("role") == "user":
43
+ text = _content_text(message.get("content"))
44
+ if text:
45
+ return text
46
+ return (request.instruction or "").strip()
47
+
48
+
49
+ def _estimate_tokens(text: str) -> int:
50
+ return max(1, len(text) // 4)
51
+
52
+
53
+ @dataclass
54
+ class EchoModelAdapter:
55
+ """A zero-dependency offline model: it turns the latest user message into a final-text
56
+ turn, so chat + streaming + multi-turn settle end to end with no network and no key.
57
+
58
+ It never emits tool calls, so a tool-bound run simply gets a conversational reply rather
59
+ than performing work — switch to a real provider to exercise the agentic tool path.
60
+ """
61
+
62
+ model: ModelConfig | None = None
63
+
64
+ def next_turn(self, request: ModelRequest) -> ModelTurn:
65
+ user_text = _latest_user_text(request)
66
+ if user_text:
67
+ body = (
68
+ "**Offline echo model** — no provider key is configured, so I can't do real "
69
+ "reasoning or run tools yet.\n\n"
70
+ f"You said:\n\n> {user_text}\n\n"
71
+ "Configure a real provider (OpenAI key, or your gateway) to get real answers."
72
+ )
73
+ else:
74
+ body = (
75
+ "I'm the offline echo model. Type a message and I'll repeat it back. "
76
+ "Configure a real provider to get real answers."
77
+ )
78
+ in_tokens = _estimate_tokens(user_text)
79
+ out_tokens = _estimate_tokens(body)
80
+ return ModelTurn(
81
+ final_text=body,
82
+ usage={
83
+ "input_tokens": in_tokens,
84
+ "output_tokens": out_tokens,
85
+ "total_tokens": in_tokens + out_tokens,
86
+ },
87
+ )
88
+
89
+
90
+ def offline_provider_factory(_claims: TokenClaims, config: ModelConfig) -> ModelAdapter:
91
+ """A ``ProviderAdapterFactory`` for ``LlmGatewayBackend`` serving the offline echo model.
92
+
93
+ Pass this as ``LlmGatewayBackend(provider_adapter_factory=offline_provider_factory)`` (or use
94
+ ``monoid llm-gateway serve --provider fake``) for a key-less gateway.
95
+ """
96
+ return EchoModelAdapter(model=config)