terrarium-python 0.1.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.
terrarium/__init__.py ADDED
@@ -0,0 +1,86 @@
1
+ """terrarium — Python client for the terrarium orchestrator API.
2
+
3
+ The surface mirrors the Claude Agent SDK (``query``, ``TerrariumOptions``, typed messages,
4
+ ``can_use_tool`` + ``PermissionResultAllow/Deny``) with extra parameters for Terrarium's own
5
+ features (personas, approval gating, egress profiles, shared memory, live model/permission
6
+ switching). Like the Claude Agent SDK it is **async-only** — it talks to a running
7
+ orchestrator over HTTP/SSE (the Claude SDK spawns a CLI instead).
8
+
9
+ import asyncio
10
+
11
+ # one-shot, Claude-Agent-SDK style
12
+ from terrarium import query, TerrariumOptions
13
+
14
+ async def main():
15
+ async for msg in query(prompt="What is 12 * 9?", options=TerrariumOptions(model="sonnet")):
16
+ print(msg)
17
+
18
+ asyncio.run(main())
19
+
20
+ # full client (agents, sessions, schedules, egress profiles, tokens)
21
+ from terrarium import TerrariumClient, TerrariumOptions
22
+
23
+ async def research():
24
+ async with TerrariumClient("http://127.0.0.1:8900") as client:
25
+ agent = await client.agents.create(name="Researcher", model="sonnet", system_mode="assistant")
26
+ async with client.session(options=TerrariumOptions(agent_id=agent["id"])) as s:
27
+ async for msg in s.receive_response("Summarise the repo"):
28
+ print(msg)
29
+
30
+ asyncio.run(research())
31
+ """
32
+
33
+ # Single source of truth is pyproject.toml; read it back from the installed distribution so the
34
+ # User-Agent can never drift from the published version. Falls back for an uninstalled checkout.
35
+ #
36
+ # The lookup key is the DISTRIBUTION name (`terrarium-python`), not this import package
37
+ # (`terrarium`). They differ, and using the import name here silently degrades every install to
38
+ # "0+unknown" — which is not an error anywhere, just a wrong User-Agent forever.
39
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
40
+
41
+ try:
42
+ __version__ = _pkg_version("terrarium-python")
43
+ except PackageNotFoundError: # running from a source tree that was never installed
44
+ __version__ = "0+unknown"
45
+ del _pkg_version, PackageNotFoundError
46
+
47
+ from .client import (
48
+ AgentsResource, EgressProfilesResource, EnvironmentsResource, SchedulesResource,
49
+ SecretsResource, Session, SessionsResource, TerrariumClient, TokensResource, query,
50
+ )
51
+ from .memory import MemoryStore, SqliteMemory, memory_tools
52
+ from .options import (
53
+ AgentDefinition, CanUseTool, ClientTool, PermissionResultAllow, PermissionResultDeny,
54
+ TerrariumOptions, ToolPermissionContext, tool,
55
+ )
56
+ from .messages import (
57
+ AssistantMessage, ContentBlock, Message, PermissionMessage, QuestionMessage,
58
+ ResultMessage, SystemMessage, TextBlock, ThinkingBlock, ToolResultBlock,
59
+ ToolUseBlock, UserMessage, parse_message,
60
+ )
61
+ from .errors import (
62
+ AuthError, ConflictError, NotFoundError, RateLimitError, ServerError,
63
+ TerrariumError, TransportError,
64
+ )
65
+
66
+ __all__ = [
67
+ # client + one-shot
68
+ "TerrariumClient", "Session", "query",
69
+ "AgentsResource", "SessionsResource", "SchedulesResource",
70
+ "EgressProfilesResource", "EnvironmentsResource", "SecretsResource", "TokensResource",
71
+ # options + permissions (Claude-SDK aligned)
72
+ "TerrariumOptions", "AgentDefinition", "CanUseTool", "ToolPermissionContext",
73
+ "PermissionResultAllow", "PermissionResultDeny",
74
+ # client-side custom tools (run in the dev's process, bridged to the sandbox)
75
+ "ClientTool", "tool",
76
+ # structured, retrieval-backed memory for long-running agents (terrarium.memory)
77
+ "MemoryStore", "SqliteMemory", "memory_tools",
78
+ # typed messages
79
+ "Message", "AssistantMessage", "UserMessage", "SystemMessage", "ResultMessage",
80
+ "QuestionMessage", "PermissionMessage", "ContentBlock",
81
+ "TextBlock", "ThinkingBlock", "ToolUseBlock", "ToolResultBlock", "parse_message",
82
+ # errors
83
+ "TerrariumError", "AuthError", "NotFoundError", "ConflictError",
84
+ "RateLimitError", "ServerError", "TransportError",
85
+ "__version__",
86
+ ]
terrarium/cli.py ADDED
@@ -0,0 +1,178 @@
1
+ """terra-cli — a thin terminal client for the Terrarium orchestrator.
2
+
3
+ Reads ``TERRA_URL`` (default http://127.0.0.1:8900) and ``TERRA_TOKEN`` from the
4
+ environment. Each command is a small wrapper over :class:`TerrariumClient`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import json
12
+ import os
13
+ import sys
14
+ from typing import Any
15
+
16
+ from .client import TerrariumClient
17
+ from .errors import TerrariumError
18
+
19
+
20
+ def _client() -> TerrariumClient:
21
+ return TerrariumClient(
22
+ os.environ.get("TERRA_URL", "http://127.0.0.1:8900"),
23
+ os.environ.get("TERRA_TOKEN") or None,
24
+ )
25
+
26
+
27
+ def _print(obj: Any) -> None:
28
+ print(json.dumps(obj, indent=2, default=str))
29
+
30
+
31
+ def _build_parser() -> argparse.ArgumentParser:
32
+ p = argparse.ArgumentParser(prog="terra-cli", description="Terrarium orchestrator client")
33
+ sub = p.add_subparsers(dest="cmd", required=True)
34
+
35
+ sub.add_parser("health", help="orchestrator health + readiness")
36
+ sub.add_parser("fleet", help="fleet summary (running/total/spend)")
37
+ sub.add_parser("templates", help="list built-in agent templates")
38
+ sub.add_parser("sessions", help="list sessions")
39
+
40
+ ve = sub.add_parser("verify-egress", help="verify a session's tamper-evident egress audit chain")
41
+ ve.add_argument("session_id")
42
+
43
+ ag = sub.add_parser("agents", help="manage agents")
44
+ agsub = ag.add_subparsers(dest="op", required=True)
45
+ agsub.add_parser("list")
46
+ agc = agsub.add_parser("create")
47
+ agc.add_argument("name")
48
+ agc.add_argument("--template")
49
+ agc.add_argument("--model")
50
+ agc.add_argument("--system-mode")
51
+ agc.add_argument("--memory-scope")
52
+ agd = agsub.add_parser("delete")
53
+ agd.add_argument("id")
54
+ agd.add_argument("--purge-memory", action="store_true")
55
+
56
+ run = sub.add_parser("run", help="run a one-shot prompt and stream the reply")
57
+ run.add_argument("prompt")
58
+ run.add_argument("--agent")
59
+ run.add_argument("--model")
60
+ run.add_argument("--budget", type=float)
61
+ run.add_argument("--keep", action="store_true", help="don't delete the session afterwards")
62
+
63
+ sc = sub.add_parser("schedules", help="manage recurring agents")
64
+ scsub = sc.add_subparsers(dest="op", required=True)
65
+ scsub.add_parser("list")
66
+ sca = scsub.add_parser("add")
67
+ sca.add_argument("prompt")
68
+ sca.add_argument("--agent", required=True)
69
+ sca.add_argument("--cron", required=True, help="5-field cron, e.g. '0 7 * * *'")
70
+ sca.add_argument("--name", default="schedule")
71
+ sca.add_argument("--budget", type=float)
72
+ scr = scsub.add_parser("run")
73
+ scr.add_argument("id")
74
+ scd = scsub.add_parser("rm")
75
+ scd.add_argument("id")
76
+
77
+ tk = sub.add_parser("tokens", help="manage scoped API tokens (admin)")
78
+ tksub = tk.add_subparsers(dest="op", required=True)
79
+ tksub.add_parser("list")
80
+ tka = tksub.add_parser("add")
81
+ tka.add_argument("name")
82
+ tka.add_argument("--scope", action="append", default=[], help="repeatable: read|run|admin (default run)")
83
+ tkr = tksub.add_parser("rm")
84
+ tkr.add_argument("id")
85
+ return p
86
+
87
+
88
+ async def _do_run(c: TerrariumClient, args: argparse.Namespace) -> None:
89
+ kw: dict[str, Any] = {}
90
+ if args.model:
91
+ kw["model"] = args.model
92
+ if args.budget:
93
+ kw["max_budget_usd"] = args.budget
94
+ sess = c.session(agent_id=args.agent, title="terra-cli run", **kw)
95
+ await sess.connect()
96
+ try:
97
+ async for ev in sess.ask(args.prompt):
98
+ t = ev["type"]
99
+ if t == "assistant_text":
100
+ sys.stdout.write(ev["text"])
101
+ sys.stdout.flush()
102
+ elif t == "tool_use":
103
+ sys.stderr.write(f"\n[tool: {ev.get('name')}]\n")
104
+ elif t == "result":
105
+ sys.stderr.write(f"\n[done · ${ev.get('total_cost_usd')}]\n")
106
+ elif t == "error":
107
+ sys.stderr.write(f"\n[error: {ev.get('message')}]\n")
108
+ sys.stdout.write("\n")
109
+ finally:
110
+ if not args.keep:
111
+ await sess.close()
112
+
113
+
114
+ async def _amain(args: argparse.Namespace) -> int:
115
+ c = _client()
116
+ try:
117
+ if args.cmd == "health":
118
+ _print(await c.health())
119
+ elif args.cmd == "fleet":
120
+ _print(await c.fleet())
121
+ elif args.cmd == "templates":
122
+ _print(await c.templates())
123
+ elif args.cmd == "sessions":
124
+ _print(await c.sessions.list())
125
+ elif args.cmd == "verify-egress":
126
+ res = await c.sessions.verify_egress(args.session_id)
127
+ _print(res)
128
+ if not res.get("ok"):
129
+ return 2 # chain broken/incomplete — distinct from a transport error (1)
130
+ elif args.cmd == "agents":
131
+ if args.op == "list":
132
+ _print(await c.agents.list())
133
+ elif args.op == "create":
134
+ kw = {k: v for k, v in (("model", args.model), ("system_mode", args.system_mode)) if v}
135
+ _print(await c.agents.create(args.name, template=args.template, memory_scope=args.memory_scope, **kw))
136
+ elif args.op == "delete":
137
+ _print(await c.agents.delete(args.id, purge_memory=args.purge_memory))
138
+ elif args.cmd == "schedules":
139
+ if args.op == "list":
140
+ _print(await c.schedules.list())
141
+ elif args.op == "add":
142
+ _print(await c.schedules.create(name=args.name, agent_id=args.agent, prompt=args.prompt,
143
+ cron=args.cron, max_budget_usd=args.budget))
144
+ elif args.op == "run":
145
+ _print(await c.schedules.run(args.id))
146
+ elif args.op == "rm":
147
+ await c.schedules.delete(args.id)
148
+ print("deleted")
149
+ elif args.cmd == "tokens":
150
+ if args.op == "list":
151
+ _print(await c.tokens.list())
152
+ elif args.op == "add":
153
+ _print(await c.tokens.create(args.name, args.scope or ["run"]))
154
+ elif args.op == "rm":
155
+ await c.tokens.delete(args.id)
156
+ print("deleted")
157
+ elif args.cmd == "run":
158
+ await _do_run(c, args)
159
+ except TerrariumError as exc:
160
+ # The SDK raises typed TerrariumError (not raw httpx) for every API/transport
161
+ # failure, so this is the ONE handler that keeps the CLI from dumping a
162
+ # traceback on a 401/403/404/409/429/5xx or a lost connection.
163
+ prefix = f"error {exc.status}: " if exc.status else "error: "
164
+ sys.stderr.write(f"{prefix}{exc}\n")
165
+ return 1
166
+ finally:
167
+ await c.aclose()
168
+ return 0
169
+
170
+
171
+ def main(argv: list[str] | None = None) -> int:
172
+ """Sync entry point — the SDK is async-only, so the CLI bridges via ``asyncio.run``."""
173
+ args = _build_parser().parse_args(argv)
174
+ return asyncio.run(_amain(args))
175
+
176
+
177
+ if __name__ == "__main__":
178
+ sys.exit(main())