town 0.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.
town/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """morph: experimental morphing agent harness."""
2
+ from .config import Config
3
+ from .organism import Organism, Town
4
+
5
+ __all__ = ["Config", "Organism", "Town"]
town/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
town/auth.py ADDED
@@ -0,0 +1,73 @@
1
+ """`town auth`: interactive onboarding. Asks for the API base URL and key, validates them against the
2
+ models endpoint, and stores them in the user config file. `town auth status` shows what is in effect."""
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import os
7
+ import sys
8
+
9
+ from .config import ANTHROPIC_URL, CONFIG_FILE, Config, load_user_config, save_user_config
10
+
11
+
12
+ def _models(base_url: str, api_key: str) -> list[str]:
13
+ import anthropic
14
+
15
+ client = anthropic.Anthropic(api_key=api_key, base_url=base_url, max_retries=1, timeout=30)
16
+ return [m.id for m in client.models.list()]
17
+
18
+
19
+ def status() -> int:
20
+ cfg = Config()
21
+ src = "env" if os.environ.get("TOWN_API_KEY") or os.environ.get("ANTHROPIC_API_KEY") else "unset"
22
+ if src == "env" and load_user_config().get("TOWN_API_KEY") == os.environ.get("TOWN_API_KEY"):
23
+ src = str(CONFIG_FILE)
24
+ print(f"base url : {cfg.base_url}")
25
+ print(f"api key : {'set (' + cfg.api_key[:8] + '…) from ' + src if cfg.api_key else 'not set'}")
26
+ print(f"model : {cfg.model} helper: {cfg.helper_model}")
27
+ print(f"config : {CONFIG_FILE}{'' if CONFIG_FILE.exists() else ' (absent)'}")
28
+ if not cfg.api_key:
29
+ print("run `town auth` to set up")
30
+ return 1
31
+ try:
32
+ ms = _models(cfg.base_url, cfg.api_key)
33
+ print(f"gateway : ok, {len(ms)} models ({', '.join(ms[:6])}{'…' if len(ms) > 6 else ''})")
34
+ return 0
35
+ except Exception as e: # noqa: BLE001
36
+ print(f"gateway : FAILED ({type(e).__name__}: {str(e)[:160]})")
37
+ return 1
38
+
39
+
40
+ def onboard(non_interactive: bool = False) -> int:
41
+ print("town setup")
42
+ print("Every citizen calls the Claude API. Tell me where and with which key; env vars override this later.")
43
+ print()
44
+ if non_interactive or not sys.stdin.isatty():
45
+ print("No terminal to ask on. Set TOWN_API_KEY (and TOWN_BASE_URL for a gateway), or run `town auth` in a terminal.")
46
+ return 1
47
+ current = Config()
48
+ default_url = current.base_url or ANTHROPIC_URL
49
+ url = input(f"API base URL [{default_url}] (Anthropic directly, or a gateway such as LiteLLM): ").strip() or default_url
50
+ url = url.rstrip("/")
51
+ key = getpass.getpass("API key (input hidden; sk-ant-… for Anthropic, or your gateway key): ").strip()
52
+ if not key:
53
+ print("no key given; nothing saved")
54
+ return 1
55
+ print("checking…", end=" ", flush=True)
56
+ try:
57
+ ms = _models(url, key)
58
+ except Exception as e: # noqa: BLE001
59
+ print(f"failed: {type(e).__name__}: {str(e)[:200]}")
60
+ print("Nothing saved. Check the URL and key (a LiteLLM gateway takes its own key, not an Anthropic one).")
61
+ return 1
62
+ print(f"ok, {len(ms)} models available")
63
+ default_model = "claude-opus-5" if "claude-opus-5" in ms else (ms[0] if ms else "claude-opus-5")
64
+ model = input(f"default model for citizens [{default_model}]: ").strip() or default_model
65
+ if model not in ms:
66
+ print(f"note: {model!r} is not in the gateway's list; keeping it anyway")
67
+ path = save_user_config({"TOWN_BASE_URL": url, "TOWN_API_KEY": key, "TOWN_MODEL": model, "TOWN_HELPER_MODEL": model})
68
+ print(f"saved to {path} (mode 600)")
69
+ print()
70
+ print("try: town run \"Find out which stdlib module has the most public names. Tell me once.\"")
71
+ print(" town preset fork_merge # one of the built-in experiments")
72
+ print(" town watch traces/<run>.jsonl in a second terminal, to watch it live")
73
+ return 0
town/blocks.py ADDED
@@ -0,0 +1,73 @@
1
+ """The context data model.
2
+
3
+ A cell's context is NOT a chat transcript. It is an ordered, mutable list of Blocks that gets
4
+ re-rendered into a single document on every model call. Because we do not care about prompt
5
+ caching, every call can see a freshly rewritten context: blocks can be dropped, rewritten,
6
+ pinned, compressed into engrams, recalled from the memory store, or copied into forks.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import itertools
11
+ import time
12
+ from dataclasses import dataclass, field
13
+
14
+ _ids = itertools.count(1)
15
+
16
+ # Default salience per kind. 5 = pinned (never auto-compressed), 1 = first to go.
17
+ DEFAULT_SALIENCE = {
18
+ "task": 5, # the originating task / focus statement
19
+ "user": 5, # something the human said
20
+ "ask": 4, # a question we surfaced to the human
21
+ "msg": 4, # a message from another cell
22
+ "merge": 4, # synthesized result of merging other cells
23
+ "recall": 3, # an engram recalled from memory
24
+ "note": 3, # explicit note written by the cell via ctx.note()
25
+ "system": 3, # harness notices
26
+ "thought": 2, # visible text the model wrote (its monologue)
27
+ "result": 2, # python stdout/stderr/value
28
+ "code": 1, # python code executed
29
+ "think": 1, # summarized thinking (continuous inner voice)
30
+ "stub": 3, # compressed-block placeholder pointing at an engram
31
+ "rewind": 4, # record of a rewind: where from, where to, and why
32
+ }
33
+
34
+
35
+ @dataclass
36
+ class Block:
37
+ kind: str
38
+ text: str
39
+ salience: int = 0
40
+ id: int = field(default_factory=lambda: next(_ids))
41
+ t: float = field(default_factory=time.time)
42
+ step: int = 0
43
+ meta: dict = field(default_factory=dict)
44
+
45
+ def __post_init__(self) -> None:
46
+ if not self.salience:
47
+ self.salience = DEFAULT_SALIENCE.get(self.kind, 3)
48
+
49
+ @property
50
+ def pinned(self) -> bool:
51
+ return self.salience >= 5
52
+
53
+ def copy(self) -> "Block":
54
+ return Block(kind=self.kind, text=self.text, salience=self.salience, step=self.step, meta=dict(self.meta))
55
+
56
+ def copy_with_id(self) -> "Block":
57
+ b = self.copy()
58
+ b.id, b.t = self.id, self.t
59
+ return b
60
+
61
+ def render(self) -> str:
62
+ attrs = [f"id={self.id}", f"kind={self.kind}", f"s={self.salience}", f"step={self.step}"]
63
+ for k, v in self.meta.items():
64
+ if k in ("from", "engram", "cell", "to", "kept_from"):
65
+ attrs.append(f"{k}={v}")
66
+ return f"<b {' '.join(attrs)}>\n{self.text}\n</b>"
67
+
68
+ def to_json(self) -> dict:
69
+ return {"id": self.id, "kind": self.kind, "s": self.salience, "step": self.step, "text": self.text, "meta": self.meta}
70
+
71
+
72
+ def estimate_tokens(text: str, chars_per_token: float = 3.6) -> int:
73
+ return int(len(text) / chars_per_token) + 1