Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ Then restart the server and ask:

The answer survives because `.agent-mini/state.db` is the source of truth.

## Local API contract

The cockpit calls a local JSON API, which is also useful when trying the agent from a script:

```bash
curl -X POST http://127.0.0.1:8787/api/run \
-H 'Content-Type: application/json' \
-d '{"message":"Calculate 8 * 9","mode":"demo"}'
```

`message` must be a non-empty string of at most 4,000 characters. Invalid requests return
HTTP 400 before an agent turn, model call, or trace entry is created. The only supported modes
are `demo` and `live`; `live` additionally requires `AGENT_API_KEY` and `AGENT_MODEL`.

## The four pieces

```mermaid
Expand Down
10 changes: 9 additions & 1 deletion agent_system/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

STATIC_ROOT = Path(__file__).parent / "static"
MAX_BODY_BYTES = 16_384
MAX_MESSAGE_CHARS = 4_000


def load_dotenv(path: Path = Path(".env")) -> None:
Expand Down Expand Up @@ -68,6 +69,11 @@ def live(self) -> AgentSystem:
def run(self, message: str, mode: str) -> dict:
if mode not in {"demo", "live"}:
raise ValueError("mode must be demo or live")
message = message.strip()
if not message:
raise ValueError("message must not be empty")
if len(message) > MAX_MESSAGE_CHARS:
raise ValueError(f"message must be at most {MAX_MESSAGE_CHARS} characters")
agent = self.demo if mode == "demo" else self.live()
return agent.run(message).to_dict()

Expand Down Expand Up @@ -155,7 +161,9 @@ def do_POST(self) -> None:
return
try:
payload = self._read_json()
message = str(payload.get("message", ""))
message = payload.get("message", "")
if not isinstance(message, str):
raise TypeError("message must be a string")
mode = str(payload.get("mode", "demo"))
self._json(self.app.run(message, mode))
except (TypeError, ValueError) as exc:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,30 @@ def test_live_mode_requires_configuration(tmp_path, monkeypatch):
server.shutdown()
server.server_close()
thread.join(timeout=3)


def test_run_rejects_empty_non_string_and_oversized_messages(tmp_path):
server = create_server(port=0, home=tmp_path / "agent-home")
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_port}"
try:
for message, expected in [
(" ", "message must not be empty"),
(["not", "a", "string"], "message must be a string"),
("x" * 4_001, "message must be at most 4000 characters"),
]:
try:
request_json(
f"{base}/api/run",
payload={"message": message, "mode": "demo"},
)
except urllib.error.HTTPError as exc:
assert exc.code == 400
assert expected in json.loads(exc.read())["error"]
else:
raise AssertionError("invalid message should return HTTP 400")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
Loading