Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original

tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
This commit is contained in:
2026-10-10 19:25:02 +02:00
parent da51e44359
commit 8df5cf7a45
39 changed files with 19175 additions and 8582 deletions
+224
View File
@@ -0,0 +1,224 @@
"""Startup, CLI parsing, banner, REPL, and main entry."""
import argparse
import os
import sqlite3
import sys
import threading
from tai_pkg.agent import Agent
from tai_pkg.agents import REGISTRY
from tai_pkg.commands import Commands
from tai_pkg.compat import readline
from tai_pkg.config import Config
from tai_pkg.consts import Consts
from tai_pkg.errors import BackendError
from tai_pkg.install import Install
from tai_pkg.models import ModelScoring
from tai_pkg.plexer import NativePlexerBackend
from tai_pkg.release import Release
from tai_pkg.render import Ansi
from tai_pkg.render import Render
from tai_pkg.sandbox import Sandbox
from tai_pkg.scheduler import Scheduler
from tai_pkg.seal import Seal
from tai_pkg.seal import SealError
from tai_pkg.seal import SealManager
from tai_pkg.store import Store
from tai_pkg.telegram import TelegramBot
from tai_pkg.text import Text
__all__ = ["App"]
class App:
"""App."""
@staticmethod
def boot(args):
args.yes = args.yes or args.yolo or args.auto
config = Config(args)
passphrase, default_key = SealManager.resolve_passphrase()
try:
seal = Seal(config.home, passphrase)
except SealError as exc:
if default_key:
raise
try:
old = Seal(config.home, Consts.DEFAULT_PASSPHRASE)
except SealError:
raise exc
try:
seal = SealManager.rotate_seal(config, old, passphrase)
except (OSError, sqlite3.Error) as fail:
raise SealError(str(fail))
print("seal upgraded from default key to TAI_PASSPHRASE")
seal.default_key = default_key and seal.enabled
store = Store(config, seal)
if store.schema_notes:
print("vault schema upgraded: %s" % ", ".join(store.schema_notes))
try:
backed_up = Release.ensure_self_backup(config, store)
except (OSError, sqlite3.Error) as exc:
print("warning: self-backup failed: %s" % Text.short_error(exc))
backed_up = None
if backed_up is not None:
print("self-backup: %s" % os.path.basename(backed_up))
return Agent(config, store, yolo=args.yolo or args.auto, auto=args.auto)
@staticmethod
def build_parser():
parser = argparse.ArgumentParser(prog="tai", description="single-file autonomous agent, standard library only")
parser.add_argument("--profile", default=None)
parser.add_argument("--yes", action="store_true")
parser.add_argument("--yolo", action="store_true")
parser.add_argument("--auto", action="store_true")
parser.add_argument("--continue", dest="resume", action="store_true")
parser.add_argument("--env", default=None)
parser.add_argument("--version", action="store_true")
parser.add_argument("--telegram", action="store_true")
parser.add_argument("--scheduler", action="store_true")
parser.add_argument("--install", action="store_true")
parser.add_argument("--install-telegram", action="store_true")
parser.add_argument("--uninstall-telegram", action="store_true")
parser.add_argument("--plexer-wrap", action="store_true", help="internal: invoked by the bashrc plexer hook, not meant to be run by hand")
parser.add_argument("prompt", nargs="*")
return parser
@staticmethod
def banner(agent):
print(Render.paint("tai %s · profile %s" % (Consts.VERSION, agent.profile), Ansi.BOLD, Ansi.CYAN))
if agent.yolo:
print(Render.paint("YOLO MODE: all approvals off", Ansi.BOLD, Ansi.RED))
if agent.auto:
print(Render.paint("AUTO MODE: never asks, researches instead", Ansi.BOLD, Ansi.YELLOW))
seal_state = "off"
if agent.store.seal.enabled:
seal_state = "on (default key)" if agent.store.seal.default_key else "on (TAI_PASSPHRASE)"
print(" seal: %s" % seal_state)
for label, _chat_url, models_url, key, _model in agent.chat.backends():
print(" %s: %s" % (label, ModelScoring.probe_backend(models_url, key)))
if agent.store.load_turn_state(agent.profile, agent.bot).get("open"):
print(Render.paint("interrupted turn available, restart with --continue to pick it up", Ansi.YELLOW))
print(Render.paint("type /help for commands", Ansi.DIM))
@staticmethod
def repl(agent):
if readline is not None:
try:
readline.read_history_file(agent.config.history_path)
except OSError:
pass
readline.set_completer(Commands.complete_command)
readline.parse_and_bind("tab: complete")
scheduler_stop = Scheduler.start_scheduler(agent.config, agent.store.seal)
try:
while True:
try:
busy = REGISTRY.running_agents()
base = agent.profile + ("!" if agent.yolo else "")
tag = "%s+%d" % (base, busy) if busy else base
line = input(Render.paint("tai[%s]› " % tag, Ansi.BOLD, Ansi.GREEN))
except EOFError:
print()
break
except KeyboardInterrupt:
print()
continue
text = line.strip()
if not text:
continue
if text.startswith("/"):
if not Commands.handle_command(agent, text):
break
continue
try:
agent.run_turn(text)
except KeyboardInterrupt:
print(Render.paint("\ninterrupted", Ansi.YELLOW))
agent.checkpoint()
finally:
scheduler_stop.set()
if readline is not None:
try:
readline.write_history_file(agent.config.history_path)
except OSError:
pass
agent.store.save_session(agent.profile, agent.messages, agent.bot)
@staticmethod
def main(argv=None):
args = App.build_parser().parse_args(argv)
if args.install:
return Install.install_self()
if args.install_telegram:
return Install.install_telegram()
if args.uninstall_telegram:
return Install.uninstall_telegram()
if args.version:
print("tai %s" % Consts.VERSION)
return 0
if args.plexer_wrap:
home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai")
profile = args.profile or "default"
backend = NativePlexerBackend(home, profile)
return backend.run()
if args.profile is None:
args.profile = "telegram" if args.telegram else "default"
if not Consts.PROFILE_RE.match(args.profile):
print("invalid profile name", file=sys.stderr)
return 1
try:
agent = App.boot(args)
except KeyboardInterrupt:
print(Render.paint("\ninterrupted", Ansi.YELLOW))
return 130
except SealError as exc:
print("seal error: %s" % exc, file=sys.stderr)
return 2
if args.env is not None:
if args.env not in ("home", "sandbox"):
print("use --env home or --env sandbox", file=sys.stderr)
return 1
if args.env == "sandbox":
try:
Sandbox.ensure_box()
except BackendError as exc:
print("sandbox unavailable: %s" % exc, file=sys.stderr)
return 2
agent.env = args.env
if args.scheduler:
print("scheduler online, press Ctrl-C to stop")
try:
Scheduler.run_scheduler_loop(agent.config, agent.store.seal, threading.Event())
except KeyboardInterrupt:
print()
finally:
agent.store.close()
return 0
if args.telegram:
return TelegramBot.run_telegram_bot(agent)
if args.prompt:
try:
agent.run_turn(" ".join(args.prompt))
except KeyboardInterrupt:
print(Render.paint("\ninterrupted", Ansi.YELLOW))
return 130
finally:
try:
agent.checkpoint()
except Exception:
pass
try:
agent.store.close()
except Exception:
pass
return 0
if args.resume and not args.telegram and not args.scheduler:
Agent.resume_turn(agent)
App.banner(agent)
try:
App.repl(agent)
finally:
agent.store.close()
return 0