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 +86 -0
- terrarium/cli.py +178 -0
- terrarium/client.py +893 -0
- terrarium/errors.py +66 -0
- terrarium/memory.py +171 -0
- terrarium/messages.py +137 -0
- terrarium/options.py +212 -0
- terrarium/py.typed +0 -0
- terrarium_python-0.1.1.dist-info/METADATA +397 -0
- terrarium_python-0.1.1.dist-info/RECORD +13 -0
- terrarium_python-0.1.1.dist-info/WHEEL +4 -0
- terrarium_python-0.1.1.dist-info/entry_points.txt +2 -0
- terrarium_python-0.1.1.dist-info/licenses/LICENSE +202 -0
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())
|