[project] name = "noeta" version = "1.2.3" description = "Open-source, Claude Code-compatible coding agent you can self-host on many workers: any worker continues any task, waits for approval hold no process. Terminal UI, CLI, ACP, HTTP API and Python SDK; Claude, DeepSeek or any OpenAI-compatible model." readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE"] authors = [{ name = "The Noeta Authors", email = "initxy0@gmail.com" }] keywords = ["ai-agent", "coding-agent", "ai-coding", "claude-code", "llm", "agentic-ai", "agent-sdk", "cli", "tui", "mcp", "acp", "anthropic", "claude", "deepseek", "openai", "self-hosted", "durable-execution", "event-sourcing", "human-in-the-loop", "multi-agent"] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development", "Topic :: Software Development :: Code Generators", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed", ] requires-python = ">=3.11" dependencies = [ "httpx[socks]>=0.28.1", "msgspec>=0.19", "pillow>=10.4", "pyyaml>=6.0", ] [project.optional-dependencies] postgres = ["psycopg[binary,pool]>=3.2"] server = ["starlette>=0.40", "uvicorn>=0.30", "websockets>=12"] acp = ["agent-client-protocol>=0.12,<0.13"] keyring = ["keyring>=24"] # The Workflow tool's embedded JavaScript engine (V8, about 80 MB installed). workflow = ["mini-racer>=0.14,<0.15"] # Released together, at the same version (docs/releasing.md). tui = ["noeta-tui==1.2.3"] [project.urls] Homepage = "https://github.com/initxy/noeta" Documentation = "https://initxy.github.io/noeta/" Source = "https://github.com/initxy/noeta" Issues = "https://github.com/initxy/noeta/issues" Changelog = "https://github.com/initxy/noeta/blob/main/CHANGELOG.md" [project.scripts] noeta = "noeta.cli.main:main" # Built-in plugins and backends register exactly as a third-party package # would (noeta/core/registry.py). [project.entry-points."noeta.plugins"] ask_user = "noeta.plugins.ask_user:AskUser" checkpoints = "noeta.plugins.checkpoints:Checkpoints" compaction = "noeta.plugins.compaction:Compaction" empty_reply = "noeta.plugins.empty_reply:EmptyReply" environment = "noeta.plugins.environment:Environment" findings = "noeta.plugins.findings:Findings" fs = "noeta.plugins.fs:FileTools" hooks = "noeta.plugins.hooks:Hooks" instructions = "noeta.plugins.instructions:Instructions" markdown_commands = "noeta.plugins.markdown_commands:MarkdownCommands" mcp = "noeta.plugins.mcp:Mcp" memory = "noeta.plugins.memory:Memory" permissions = "noeta.plugins.permissions:Permissions" plan_mode = "noeta.plugins.plan_mode:PlanMode" project_status = "noeta.plugins.project_status:ProjectStatus" budget = "noeta.plugins.budget:BudgetCaps" quiet_nudge = "noeta.plugins.quiet_nudge:QuietNudge" schedule = "noeta.plugins.schedule:Schedule" shell = "noeta.plugins.shell:Shell" skills = "noeta.plugins.skills:Skills" structured_output = "noeta.plugins.structured_output:StructuredOutput" title = "noeta.plugins.title:Title" todos = "noeta.plugins.todos:Todos" tool_search = "noeta.plugins.tool_search:ToolSearch" subtasks = "noeta.plugins.subtasks:Subtasks" web = "noeta.plugins.web:Web" workflow = "noeta.plugins.workflow:Orchestration" [project.entry-points."noeta.providers"] anthropic = "noeta.backends.providers:anthropic_provider" openai_compat = "noeta.backends.providers:openai_compat_provider" openai_responses = "noeta.backends.providers:openai_responses_provider" [project.entry-points."noeta.execenvs"] local = "noeta.backends.execenvs.local:LocalExecEnv" container = "noeta.backends.execenvs.container:open_env" [project.entry-points."noeta.stores"] sqlite = "noeta.backends.stores.sqlite:open_url" postgres = "noeta.backends.stores.postgres:open_url" postgresql = "noeta.backends.stores.postgres:open_url" [dependency-groups] dev = [ "pytest>=8.0", "pytest-asyncio>=0.24", "pytest-cov>=5.0", "pytest-xdist>=3.8", "mypy>=1.10", "respx>=0.23.1", "ruff>=0.6", "import-linter>=2.0", "types-pyyaml>=6.0", "pytest-timeout>=2.3", # The workflow extra, so the Workflow tests run whatever `uv sync` is given. "mini-racer>=0.14,<0.15", # The terminal UI (tui/), so `make check` runs its tests and lints it. "noeta-tui", ] [tool.uv.workspace] members = ["tui"] [tool.uv.sources] noeta-tui = { workspace = true } [build-system] requires = ["hatchling"] build-backend = "hatchling.build" # The built web app (make web-bundle) is not in the repository but goes in # both distributions, so a wheel built from the sdist carries it too. [tool.hatch.build] artifacts = ["/noeta/host/web_dist/"] [tool.hatch.build.targets.wheel] packages = ["noeta"] # A local coding tool's settings are not part of the source distribution. [tool.hatch.build.targets.sdist] exclude = ["/.claude"] [tool.mypy] strict = true files = ["noeta", "evals", "tui/src"] [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" addopts = "-ra -m 'not live and not cluster' --import-mode=importlib" # A hung test fails with every thread's stack instead of stalling the run: # faulthandler dumps the stacks at 120 s, pytest-timeout ends the test at 300 s. faulthandler_timeout = 120 timeout = 300 timeout_method = "thread" markers = [ "live: needs a real model backend; opt in with -m live", "cluster: worker processes on Postgres under faults; run with make cluster", ] [tool.coverage.run] source = ["noeta"] branch = true [tool.ruff] target-version = "py311" line-length = 100 [tool.ruff.lint] select = ["E", "F", "W", "I", "B", "UP", "RUF", "BLE"] # RUF012: msgspec Struct defaults are copied per instance. # UP007: module-level union aliases read better as Union[...] when long. ignore = ["RUF012", "UP007"] [tool.ruff.lint.isort] known-first-party = ["noeta", "noeta_tui"] # tui/tests' shared helpers, on its pytest pythonpath. known-local-folder = ["support"] [tool.ruff.lint.flake8-bugbear] extend-immutable-calls = ["noeta.core.store.Patch", "Patch"] [tool.ruff.lint.per-file-ignores] "{tests,tui/tests}/**" = ["B008", "E501"] # The terminal UI draws Claude Code's glyphs (❯ ⎿ ⏺ ✻). "tui/**" = ["RUF001", "RUF002", "RUF003"] # Dev tooling, not shipped: long table lines and prose are fine. "{evals,bench,scripts}/**" = ["E501", "RUF002", "RUF003", "B905"] [tool.importlinter] root_packages = ["noeta", "noeta_tui"] # The layers, top to bottom; a module may import only from layers below it. [[tool.importlinter.contracts]] name = "layers: cli > agents > plugins | backends | host > toolkit > core" type = "layers" layers = [ "noeta.cli", "noeta.agents", "noeta.plugins | noeta.backends | noeta.host", "noeta.toolkit", "noeta.core", ] # A plugin is written against the kernel only, so any one can be removed. [[tool.importlinter.contracts]] name = "plugins do not import each other" type = "independence" modules = ["noeta.plugins.*"] # Front ends and protocol adapters reach plugins only through the kernel (settings by name, the # question form, commands), so any plugin can be removed or replaced. [[tool.importlinter.contracts]] name = "front ends do not import plugins" type = "forbidden" source_modules = ["noeta.cli", "noeta.host"] forbidden_modules = ["noeta.plugins"] # The terminal UI (tui/) is a front end on noeta's host API: no plugin, no # backend, nothing of the noeta command (which hands it what it needs). [[tool.importlinter.contracts]] name = "the terminal UI imports no plugin, backend, agent or the noeta command" type = "forbidden" source_modules = ["noeta_tui"] forbidden_modules = ["noeta.plugins", "noeta.backends", "noeta.agents", "noeta.cli"] [[tool.importlinter.contracts]] name = "backends do not import each other" type = "independence" modules = ["noeta.backends.*"] [[tool.importlinter.contracts]] name = "the loop is pure: no runner, store or provider" type = "forbidden" source_modules = ["noeta.core.loop"] forbidden_modules = ["noeta.core.runner", "noeta.core.store", "noeta.core.provider"]